block-sdk/tests/app/README.md
2024-06-25 10:48:18 -04:00

8.0 KiB

Block SDK Enabled Test App (10 min)

Overview

This readme describes how to build a test app that uses the Block SDK. This assumes that you have already installed the Block SDK and have a working development environment. To install the Block SDK, please run the following:

go get github.com/skip-mev/block-sdk

Building the Test App

There are fix critical steps to building a test app that uses the Block SDK:

  1. Set up the signer extractor.
  2. Create the lane configurations for each individual lane i.e. LaneConfig.
  3. Configure the match handlers for each lane i.e. MatchHandler.
  4. Creating the Block SDK mempool i.e. LanedMempool.
  5. Setting the antehandlers - used for transaction validation - for each lane.
  6. Setting the proposal handlers - used for block creation and verification - for the application to utilize the Block SDK's Prepare and Process Proposal handlers.

IMPORTANT NOTE: It is recommended that applications use the NewDefaultProposalHandler when constructing their Prepare and Process Proposal handlers. This is because the priority nonce mempool - the underlying storage device of transactions - has non-deterministic ordering of transactions on chains with multiple fees. The use of the NewDefaultProposalHandler ensures that the ordering of transactions on proposal construction follows the ordering logic of the lanes, which is deterministic, and that process proposal optimistically assumes that the ordering of transactions is correct.

1. Signer Extractor

The signer extractor is responsible for extracting signers and relevant information about who is signing the transaction. We recommend using the default implementation provided by the Block SDK.

signerAdapter := signerextraction.NewDefaultAdapter()

2. Lane Configurations

This controls how many transactions can be stored by each lane, how much block space is allocated to each lane, how to extract transacation information such as signers, fees, and more. Each lane should have a separate LaneConfig object.

For example, in lanes.go we see the following:

mevConfig := base.LaneConfig{
	Logger:          app.Logger(),
	TxEncoder:       app.txConfig.TxEncoder(),
	TxDecoder:       app.txConfig.TxDecoder(),
	MaxBlockSpace:   math.LegacyMustNewDecFromStr("0.2"),
	SignerExtractor: signerAdapter,
	MaxTxs:          1000,
}

Following the example above:

  • Logger: This is the logger that will be utilized by the lane when outputting information as blocks are being processed and constructed.
  • TxEncoder: This is the encoder that will be used to encode transactions.
  • TxDecoder: This is the decoder that will be used to decode transactions.
  • MaxBlockSpace: This is the maximum amount of block space that can be allocated to this lane. In this case, we allocate 20% of the block space to this lane.
  • SignerExtractor: This is the signer extractor that will be used to extract signers from transactions. In this case, we utilize the default signer extractor provided by the Block SDK. This is the recommended approach.
  • MaxTxs: This is the maximum number of transactions that can be stored in this lane. In this case, we allow up to 1000 transactions to be stored in this lane at any given time.

Match Handlers

Match handlers are responsible for matching transactions to lanes. Each lane should have a unique match handler. By default, we recommend that the default lane be the last lane in your application. This is because the default lane matches all transactions that do not match to any of the other lanes. If you want to have a lane after the default lane, please see the section below.

(OPTIONAL) Having Lanes after the Default Lane

If you want to have lanes after the default lane, you will need to utilize the base.NewMatchHandler function. This function allows you to construct a match handler that can ignore other lane's match handlers.

For example, if we wanted the free and MEV lanes to be processed after the default lane - default, MEV, free - we can do the following:

// Create the final match handler for the default lane.
defaultMatchHandler := base.NewMatchHandler(
	base.DefaultMatchHandler(),
	factory.MatchHandler(),
	freelane.DefaultMatchHandler(),
)

Following the example, we can see the following:

  • base.DefaultMatchHandler(): This is the default match handler provided by the Block SDK. This matches all transactions to the lane.
  • factory.MatchHandler(): This is the MEV lane's match handler. This is passed as a parameter to the base.NewMatchHandler function - which means that all transactions that match to the MEV lane will be ignored by the default match handler.
  • freelane.DefaultMatchHandler(): This is the default match handler for the free lane. This is passed as a parameter to the base.NewMatchHandler function - which means that all transactions that match to the free lane will be ignored by the default match handler.

This will allow the default match handler to only match transactions that do not match to the MEV lane or the free lane.

Block SDK Mempool

After constructing the lanes, we can create the Block SDK mempool - LanedMempool. This object is responsible for managing the lanes and processing transactions.

// STEP 1: Create the Block SDK lanes.
mevLane, freeLane, defaultLane := CreateLanes(app)

// STEP 2: Construct a mempool based off the lanes.
mempool, err := block.NewLanedMempool(
	app.Logger(),
	[]block.Lane{mevLane, freeLane, defaultLane},
)
if err != nil {
	panic(err)
}

// STEP 3: Set the mempool on the app.
app.App.SetMempool(mempool)

Note that we pass the lanes to the block.NewLanedMempool function. The order of the lanes is important. Proposals will be constructed based on the order of lanes passed to the block.NewLanedMempool function. In the example above, the MEV lane will be processed first, followed by the free lane, and finally the default lane.

AnteHandlers

AnteHandlers are responsible for validating transactions. We recommend that developers utilize the same antehandler chain that is used by the application. In the example test app, we construct the AnteHandler with NewBSDKAnteHandler. In the case where the certain ante decorators should ignore certain lanes, we can wrap a Decorator with the block.NewIgnoreDecorator function as seen in ante.go.

After constructing the AnteHandler, we can set it on the application and on the lanes.

// STEP 4: Create a global ante handler that will be called on each transaction when
// proposals are being built and verified. Note that this step must be done before
// setting the ante handler on the lanes.
handlerOptions := ante.HandlerOptions{
	AccountKeeper:   app.AccountKeeper,
	BankKeeper:      app.BankKeeper,
	FeegrantKeeper:  app.FeeGrantKeeper,
	SigGasConsumer:  ante.DefaultSigVerificationGasConsumer,
	SignModeHandler: app.txConfig.SignModeHandler(),
}
options := BSDKHandlerOptions{
	BaseOptions:   handlerOptions,
	auctionkeeper: app.auctionkeeper,
	TxDecoder:     app.txConfig.TxDecoder(),
	TxEncoder:     app.txConfig.TxEncoder(),
	FreeLane:      freeLane,
	MEVLane:       mevLane,
}
anteHandler := NewBSDKAnteHandler(options)
app.App.SetAnteHandler(anteHandler)

// Set the AnteHandlers on the lanes.
mevLane.SetAnteHandler(anteHandler)
freeLane.SetAnteHandler(anteHandler)
defaultLane.SetAnteHandler(anteHandler)

Proposal Handlers

The proposal handlers - PrepareProposal and ProcessProposal - are responsible for building and verifying block proposals. To add it to your application, follow the example below:

// Step 5: Create the proposal handler and set it on the app.
proposalHandler := abci.NewProposalHandler(
	app.Logger(),
	app.TxConfig().TxDecoder(),
	app.TxConfig().TxEncoder(),
	mempool,
)
app.App.SetPrepareProposal(proposalHandler.PrepareProposalHandler())
app.App.SetProcessProposal(proposalHandler.ProcessProposalHandler())

Conclusion

Adding the Block SDK to your application is a simple 6 step process. If you have any questions, please feel free to reach out to the Skip team. We are happy to help!