diff --git a/abci/README.md b/abci/README.md new file mode 100644 index 00000000..ea035d6e --- /dev/null +++ b/abci/README.md @@ -0,0 +1,222 @@ +# Block SDK Proposal Construction & Verification + +## Overview + +This readme describes how the Block SDK constructs and verifies proposals. To get a high level overview of the Block SDK, please see the [Block SDK Overview](../README.md). + +The Block SDK is a set of Cosmos SDK and ABCI++ primitives that allow chains to fully customize blocks for specific use cases. It turns your chain's blocks into a **`highway`** consisting of individual **`lanes`** with their own special functionality. The mempool is no longer a single black box, but rather a set of lanes that can be customized to fit your application's needs. Each lane is meant to deal with specific a set of transactions, and can be configured to fit your application's needs. + +Proposal construction / verification is done via the `PrepareProposal` and `ProcessProposal` handlers defined in [`abci.go`](./abci.go), respectively. Each block proposal built by the Block SDK enforces that a block is comprised of contiguous sections of transactions that belong to a single lane. + +For example, if your application has 3 lanes, `A`, `B`, and `C`, and the order of lanes is `A -> B -> C`, then the proposal will be built as follows: + +```golang +blockProposal := { + Tx1, (Lane A) + Tx2, (Lane A) + Tx3, (Lane A) + Tx4, (Lane B) + Tx5, (Lane B) + Tx6, (Lane B) + Tx7, (Lane C) + Tx8, (Lane C) + Tx9, (Lane C) +} +``` + +## Proposal Construction + +The `PrepareProposal` handler is called by the ABCI++ application when a new block is requested by the network for the given proposer. At runtime, the `PrepareProposal` handler will do the following: + +1. Determine the order of lanes it wants to construct the proposal from. If the application is configured to utilize the Block SDK module, it will fetch the governance configured order of lanes and use that. Otherwise, it will use the order defined in your [`app.go`](../tests/app/app.go) file (see our test app as an example). +2. After determining the order, it will chain together all of the lane's `PrepareLane` methods into a single `PrepareProposal` method. +3. Each lane will select transactions from its mempool, verify them according to its own rules, and return a list of valid transactions to add and a list of transactions to remove i.e. see [`PrepareLane`](../block/base/abci.go). +4. The transactions will only be added to the current proposal being built _iff_ the transactions are under the block gas and size limit for that lane. If the transactions are over the limit, they will NOT be added to the proposal and the next lane will be called. +5. A final proposal is returned with all of the valid transactions from each lane. + +If any of the lanes fail during `PrepareLane`, the next lane will be called and the proposal will be built from the remaining lanes. This is a fail-safe mechanism to ensure that the proposal is always built, even if one of the lanes fails to prepare. Additionally, state is mutated _iff_ the lane is successful in preparing its portion of the proposal. + +To customize how much block-space a given lane consumes, you have to configure the `MaxBlockSpace` variable in your lane configuration object (`LaneConfig`). Please visit [`lanes.go`](../tests/app/lanes.go) for an example. This variable is a map of lane name to the maximum block space that lane can consume. Note that if the Block SDK module is utilized, the `MaxBlockSpace` variable will be overwritten by the governance configured value. + +### Proposal Construction Example + +> Let's say your application has 3 lanes, `A`, `B`, and `C`, and the order of lanes is `A -> B -> C`. +> +> * Lane `A` has a `MaxBlockSpace` of 1000 bytes and 500 gas limit. +> * Lane `B` has a `MaxBlockSpace` of 2000 bytes and 1000 gas limit. +> * Lane `C` has a `MaxBlockSpace` of 3000 bytes and 1500 gas limit. + +Lane `A` currently contains 4 transactions: + +* Tx1: 100 bytes, 100 gas +* Tx2: 800 bytes, 300 gas +* Tx3: 200 bytes, 100 gas +* Tx4: 100 bytes, 100 gas + +Lane `B` currently contains 4 transactions: + +* Tx5: 1000 bytes, 500 gas +* Tx6: 1200 bytes, 600 gas +* Tx7: 1500 bytes, 300 gas +* Tx8: 1000 bytes, 400 gas + +Lane `C` currently contains 4 transactions: + +* Tx9: 1000 bytes, 500 gas +* Tx10: 1200 bytes, 600 gas +* Tx11: 1500 bytes, 300 gas +* Tx12: 100 bytes, 400 gas + +Assuming all transactions are valid according to their respective lanes, the proposal will be built as follows (this is pseudo-code): + +```golang +// Somewhere in abci.go a new proposal is created: +blockProposal := proposals.NewProposal(...) +... +// First lane to be called is lane A, this will return the following transactions to add after PrepareLane is called: +partialProposalFromLaneA := { + Tx1, // 100 bytes, 100 gas + Tx2, // 800 bytes, 300 gas + Tx3, // 200 bytes, 100 gas +} +... +// First Lane A will update the proposal. +if err := blockProposal.Update(partialProposalFromLaneA); err != nil { + return err +} +... +// Next, lane B will be called with the following transactions to add after PrepareLane is called: +// +// Note: Here, Tx6 is excluded because it and Tx5's gas would exceed the lane gas limit. Tx7 is similarly excluded because it and Tx5's size would exceed the lane block size limit. Note that Tx5 will always be included first because it is ordered first and it is valid given the lane's constraints. +partialProposalFromLaneB := { + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas +} +... +// Next, lane B will update the proposal. +if err := blockProposal.Update(partialProposalFromLaneB); err != nil { + return err +} +... +// Finally, lane C will be called with the following transactions to add after PrepareLane is called: +partialProposalFromLaneC := { + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +... +// Finally, lane C will update the proposal. +if err := blockProposal.Update(partialProposalFromLaneC); err != nil { + return err +} +... +// The final proposal will be: +blockProposal := { + Tx1, // 100 bytes, 100 gas + Tx2, // 800 bytes, 300 gas + Tx3, // 200 bytes, 100 gas + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +``` + +If any transactions are invalid, they will not be included in the lane's partial proposal. + +## Proposal Verification + +The `ProcessProposal` handler is called by the ABCI++ application when a new block has been proposed by the proposer and needs to be verified by the network. At runtime, the `ProcessProposal` handler will do the following: + +1. Determine the order of lanes it wants to verify the proposal with. If the application is configured to utilize the Block SDK module, it will fetch the governance configured order of lanes and use that. Otherwise, it will use the order defined in your [`app.go`](../tests/app/app.go) file (see our test app as an example). +2. After determining the order, it will chain together all of the lane's `ProcessLane` methods into a single `ProcessProposal` method. +3. Given that the proposal contains contiguous sections of transactions from a given lane, each lane will verify the transactions that belong to it and return the remaining transactions to verify for the next lane - see [`ProcessLane`](../block/base/abci.go). +4. After determining and verifiying the transactions that belong to it, the lane will attempt to update the current proposal - so as to replicate the exact same steps done in `PrepareProposal`. If the lane is unable to update the proposal, it will return an error and the proposal will be rejected. +5. Once all lanes have been called and no transactions are left to verify, the proposal outputted by `ProcessProposal` should be the same as the proposal outputted by `PrepareProposal`! + +If any of the lanes fail during `ProcessLane`, the entire proposal is rejected. There ensures that there is always parity between the proposal built and the proposal verified. + +### Proposal Verification Example + +Following the example above, let's say we recieve the same proposal from the network: + +```golang +blockProposal := { + Tx1, // 100 bytes, 100 gas + Tx2, // 800 bytes, 300 gas + Tx3, // 200 bytes, 100 gas + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +``` + +The proposal will be verified as follows (this is pseudo-code): + +```golang +// Somewhere in abci.go a new proposal is created: +blockProposal := proposals.NewProposal(...) +... +// First lane to be called is lane A, this will return the following transactions that it verified and the remaining transactions to verify after calling ProcessLane: +verifiedTransactionsFromLaneA, remainingTransactions := { + Tx1, // 100 bytes, 100 gas + Tx2, // 800 bytes, 300 gas + Tx3, // 200 bytes, 100 gas +}, { + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +... +// First Lane A will update the proposal. +if err := blockProposal.Update(verifiedTransactionsFromLaneA); err != nil { + return err +} +... +// Next, lane B will be called with the following transactions to verify and the remaining transactions to verify after calling ProcessLane: +verifiedTransactionsFromLaneB, remainingTransactions := { + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas +}, { + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +... +// Next, lane B will update the proposal. +if err := blockProposal.Update(verifiedTransactionsFromLaneB); err != nil { + return err +} +... +// Finally, lane C will be called with the following transactions to verify and the remaining transactions to verify after calling ProcessLane: +verifiedTransactionsFromLaneC, remainingTransactions := { + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +}, {} +... +// Finally, lane C will update the proposal. +if err := blockProposal.Update(verifiedTransactionsFromLaneC); err != nil { + return err +} +... +// The final proposal will be: +blockProposal := { + Tx1, // 100 bytes, 100 gas + Tx2, // 800 bytes, 300 gas + Tx3, // 200 bytes, 100 gas + Tx5, // 1000 bytes, 500 gas + Tx8, // 1000 bytes, 400 gas + Tx9, // 1000 bytes, 500 gas + Tx10, // 1200 bytes, 600 gas + Tx12, // 100 bytes, 400 gas +} +``` + +As we can see, in the process of verifying a proposal, the proposal is updated to reflect the exact same steps done in `PrepareProposal`. + diff --git a/adapters/signer_extraction_adapter/README.md b/adapters/signer_extraction_adapter/README.md new file mode 100644 index 00000000..7affb848 --- /dev/null +++ b/adapters/signer_extraction_adapter/README.md @@ -0,0 +1,31 @@ +# Signer Extraction Adapter (SEA) + +## Overview + +The Signer Extraction Adapter is utilized to retrieve the signature information of a given transaction. This is purposefully built to allow application developers to retrieve signer information in the case where the default Cosmos SDK signature information is not applicable. + +## Utilization within the Block SDK + +Each lane can configure it's own Signer Extraction Adapter (SEA). However, for almost all cases each lane will have the same SEA. The SEA is utilized to retrieve the address of the signer and nonce of the transaction. It's utilized by each lane's mempool to retrieve signer information as transactions are being inserted and for logging purposes as a proposal is being created / verified. + +## Configuration + +To extend and implement a new SEA, the following interface must be implemented: + +```go +// Adapter is an interface used to determine how the signers of a transaction should be extracted +// from the transaction. +type Adapter interface { + GetSigners(sdk.Tx) ([]SignerData, error) +} +``` + +The `GetSigners` method is responsible for extracting the signer information from the transaction. The `SignerData` struct is defined as follows: + +```go +type SignerData struct { + Signer sdk.AccAddress + Sequence uint64 +} +``` + diff --git a/adapters/signer_extraction_adapter/signer_extraction_adapter.go b/adapters/signer_extraction_adapter/signer_extraction_adapter.go index e905c5b3..c7f0d6da 100644 --- a/adapters/signer_extraction_adapter/signer_extraction_adapter.go +++ b/adapters/signer_extraction_adapter/signer_extraction_adapter.go @@ -12,7 +12,20 @@ type SignerData struct { Sequence uint64 } -// SignerExtractionAdapter is an interface used to determine how the signers of a transaction should be extracted +// NewSignerData returns a new SignerData instance. +func NewSignerData(signer sdk.AccAddress, sequence uint64) SignerData { + return SignerData{ + Signer: signer, + Sequence: sequence, + } +} + +// String implements the fmt.Stringer interface. +func (s SignerData) String() string { + return fmt.Sprintf("SignerData{Signer: %s, Sequence: %d}", s.Signer, s.Sequence) +} + +// Adapter is an interface used to determine how the signers of a transaction should be extracted // from the transaction. type Adapter interface { GetSigners(sdk.Tx) ([]SignerData, error) diff --git a/block/README.md b/block/README.md new file mode 100644 index 00000000..f3715f9b --- /dev/null +++ b/block/README.md @@ -0,0 +1,103 @@ +# Block SDK Mempool & Lanes + +## Overview + +> This document describes how the Block SDK mempool and lanes operate at a high level. To learn more about how to construct lanes, please visit the [build my own lane readme](../lanes/build-your-own/README.md) and/or the [base lane documentation](./base/README.md). To read about how proposals are construct, visit the [abci readme](../abci/README.md). + +Mempools are traditionally used to temporarily store transactions before they are added to a block. The Block SDK mempool is no different. However, instead of treating each transaction the same, the Block SDK allows for developers to create `Lanes` that permit transactions to be ordered differently based on the properties of the transaction itself. + +What was once a single monolithic data structure, is now a collection of sub-mempools that can be configured to order transactions in a way that makes sense for the application. + +## Lanes + +Lanes are utilized to allow developers to create custom transaction order, validation, and execution logic. Each lane is responsible for maintaining its own mempool - ordering transactions as it desires only for the transactions it wants to accept. For example, a lane may only accept transactions that are staking related, such as the free lane. The free lane may then order the transactions based on the user's on-chain stake. + +When proposals are constructed, the transactions from a given lane are selected based on highest to lowest priority, validated according to the lane's verfication logic, and included in the proposal. + +Each lane must implement the `Lane` interface, although it is highly recommended that developers extend the [base lane](./base/README.md) to create new lanes. + +```go +// LaneMempool defines the interface a lane's mempool should implement. The basic API +// is the same as the sdk.Mempool, but it also includes a Compare function that is used +// to determine the relative priority of two transactions belonging in the same lane. +type LaneMempool interface { + sdkmempool.Mempool + + // Compare determines the relative priority of two transactions belonging in the same lane. Compare + // will return -1 if this transaction has a lower priority than the other transaction, 0 if they have + // the same priority, and 1 if this transaction has a higher priority than the other transaction. + Compare(ctx sdk.Context, this, other sdk.Tx) (int, error) + + // Contains returns true if the transaction is contained in the mempool. + Contains(tx sdk.Tx) bool + + // Priority returns the priority of a transaction that belongs to this lane. + Priority(ctx sdk.Context, tx sdk.Tx) any +} + +// Lane defines an interface used for matching transactions to lanes, storing transactions, +// and constructing partial blocks. +type Lane interface { + LaneMempool + + // PrepareLane builds a portion of the block. It inputs the current context, proposal, and a + // function to call the next lane in the chain. This handler should update the context as needed + // and add transactions to the proposal. Note, the lane should only add transactions up to the + // max block space for the lane. + PrepareLane( + ctx sdk.Context, + proposal proposals.Proposal, + next PrepareLanesHandler, + ) (proposals.Proposal, error) + + // ProcessLane verifies this lane's portion of a proposed block. It inputs the current context, + // proposal, transactions that belong to this lane, and a function to call the next lane in the + // chain. This handler should update the context as needed and add transactions to the proposal. + // The entire process lane chain should end up constructing the same proposal as the prepare lane + // chain. + ProcessLane( + ctx sdk.Context, + proposal proposals.Proposal, + txs []sdk.Tx, + next ProcessLanesHandler, + ) (proposals.Proposal, error) + + // GetMaxBlockSpace returns the max block space for the lane as a relative percentage. + GetMaxBlockSpace() math.LegacyDec + + // SetMaxBlockSpace sets the max block space for the lane as a relative percentage. + SetMaxBlockSpace(math.LegacyDec) + + // Name returns the name of the lane. + Name() string + + // SetAnteHandler sets the lane's antehandler. + SetAnteHandler(antehander sdk.AnteHandler) + + // Match determines if a transaction belongs to this lane. + Match(ctx sdk.Context, tx sdk.Tx) bool + + // GetTxInfo returns various information about the transaction that + // belongs to the lane including its priority, signer's, sequence number, + // size and more. + GetTxInfo(ctx sdk.Context, tx sdk.Tx) (utils.TxWithInfo, error) +} +``` + +## Lane Priorities + +Each lane has a priority that is used to determine the order in which lanes are processed. The higher the priority, the earlier the lane is processed. For example, if we have three lanes - MEV, free, and default - proposals will be constructed in the following order: + +1. MEV +2. Free +3. Default + +Proposals will then be verified in the same order. Please see the [readme above](../abci/README.md) for more information on how proposals are constructed using lanes. + +The ordering of lane's priorities is determined based on the order passed into the constructor of the Block SDK mempool i.e. `LanedMempool`. + +## Block SDK mempool + +The `LanedMempool` is a wrapper on top of the collection of lanes. It is solely responsible for adding transactions to the appropriate lanes. Transactions are always inserted / removed to the first lane that accepts / matches the transactions. **Transactions should only match to one lane.**. **In the case where a transaction can match to multiple lanes, the transaction will be inserted into the lane that has the highest priority.** + +To read more about the underlying implementation of the Block SDK mempool, please see the implementation [here](./mempool.go). diff --git a/block/base/README.md b/block/base/README.md new file mode 100644 index 00000000..9dd2a4d9 --- /dev/null +++ b/block/base/README.md @@ -0,0 +1,145 @@ +# Base Lane + +## Overview + +> The base lane is purposefully built to be a simple lane that can be extended (inherited) by other lanes. It provides the basic functionality that is required by all lanes with the option to override any of the methods. + +The base lane implements the lane interface and provides a few critical methods that allow application developers to create a lane that has custom transaction ordering and execution logic. The most important things you need to know in order to build a custom lane are: + +* `MatchHandler`: This method is responsible for determining if a given transaction should be accepted by the lane. +* `PrepareLaneHandler`: This method is responsible for reaping transactions from the mempool, validating them, re-ordering (if necessary), and returning them to be included in a block proposal. +* `ProcessLaneHandler`: This method is responsible verifying the matched transactions that were included in a block proposal. +* `LaneMempool`: This allows developers to have the freedom to implement their own mempools with custom transaction ordering logic. +* `LaneConfig`: This allows developers to customize how the lane behaves in terms of max block space, max transaction count, and more. + +## MatchHandler + +MatchHandler is utilized to determine if a transaction should be included in the lane. This function can be a stateless or stateful check on the transaction. The function signature is as follows: + +```go +MatchHandler func(ctx sdk.Context, tx sdk.Tx) bool +``` + +To create a custom lane with a custom `MatchHandler`, you must implement this function and pass it into the constructor for the base lane. For example, the [free lane](../../lanes/free/lane.go) inherits all the base lane functionality but overrides the `MatchHandler` to only accept staking related transactions. + +```go +// DefaultMatchHandler returns the default match handler for the free lane. The +// default implementation matches transactions that are staking related. In particular, +// any transaction that is a MsgDelegate, MsgBeginRedelegate, or MsgCancelUnbondingDelegation. +func DefaultMatchHandler() base.MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + for _, msg := range tx.GetMsgs() { + switch msg.(type) { + case *types.MsgDelegate: + return true + case *types.MsgBeginRedelegate: + return true + case *types.MsgCancelUnbondingDelegation: + return true + } + } + + return false + } +} +``` + +The default `MatchHandler` is implemented in the [base lane](./handlers.go) and matches all transactions. + +## PrepareLaneHandler + +The `PrepareLaneHandler` is responsible for reaping transactions from the mempool, validating them, re-ordering (if necessary), and returning them to be included in a block proposal. If any of the transactions were invalid, it should return them alongside the transactions it wants to include in the proposal. The invalid transactions will subsequently be removed from the lane's mempool. The function signature is as follows: + +```go +PrepareLaneHandler func( + ctx sdk.Context, + proposal proposals.Proposal, + limit proposals.LaneLimits, +) (txsToInclude []sdk.Tx, txsToRemove []sdk.Tx, err error) +``` + +To create a custom lane with a custom `PrepareLaneHandler`, you must implement this function and set it on the lane after it has been created. Please visit the [MEV lane's](../../lanes/mev/abci.go) `PrepareLaneHandler` for an example of how to implement this function. + +The default `PrepareLaneHandler` is implemented in the [base lane](./handlers.go). It reaps transactions from the mempool, validates them, ensures that the lane's block space limit is not exceeded, and returns the transactions to be included in the block and the ones that need to be removed. + +## ProcessLaneHandler + +The `ProcessLaneHandler` is responsible for verifying the transactions that belong to a given lane that were included in a block proposal and returning those that did not to the next lane. The function signature is as follows: + +```go +ProcessLaneHandler func(ctx sdk.Context, partialProposal []sdk.Tx) ( + txsFromLane []sdk.Tx, + remainingTxs []sdk.Tx, + err error, +) +``` + +Note that block proposals built using the Block SDK contain contiguous sections of transactions in the block that belong to a given lane, to read more about how proposals are constructed relative to other lanes, please visit the [abci section](../../abci/README.md). As such, a given lane will recieve some transactions in (partialProposal) that belong to it and some that do not. The transactions that belong to it must be contiguous from the start, and the transactions that do not belong to it must be contiguous from the end. The lane must return the transactions that belong to it and the transactions that do not belong to it. The transactions that do not belong to it will be passed to the next lane in the proposal. The default `ProcessLaneHandler` is implemented in the [base lane](./handlers.go). It verifies the transactions that belong to the lane and returns them alongside the transactions that do not belong to the lane. + +Please visit the [MEV lane's](../../lanes/mev/abci.go) `ProcessLaneHandler` for an example of how to implement a custom handler. + +## LaneMempool + +The lane mempool is the data structure that is responsible for storing transactions that belong to a given lane, before they are included in a block proposal. The lane mempool input's a `TxPriority` object that allows developers to customize how they want to order transactions within their mempool. Additionally, it also accepts a signer extrator adapter that allows for custom signature schemes to be used (although the default covers Cosmos SDK transactions). To read more about the signer extractor adapter, please visit the [signer extractor section](../../adapters/signer_extraction_adapter/README.md). + +### TxPriority + +The `TxPriority` object is responsible for ordering transactions within the mempool. The definition of the `TxPriority` object is as follows: + +```go +// TxPriority defines a type that is used to retrieve and compare transaction +// priorities. Priorities must be comparable. +TxPriority[C comparable] struct { + // GetTxPriority returns the priority of the transaction. A priority must be + // comparable via Compare. + GetTxPriority func(ctx context.Context, tx sdk.Tx) C + + // CompareTxPriority compares two transaction priorities. The result should be + // 0 if a == b, -1 if a < b, and +1 if a > b. + Compare func(a, b C) int + + // MinValue defines the minimum priority value, e.g. MinInt64. This value is + // used when instantiating a new iterator and comparing weights. + MinValue C +} +``` + +The default implementation can be found in the [base lane](./mempool.go). It orders transactions by their gas price in descending order. The `TxPriority` object is passed into the lane mempool constructor. Please visit the [MEV lane's](../../lanes/mev/mempool.go) `TxPriority` for an example of how to implement a custom `TxPriority`. + +## LaneConfig + +The lane config is the object that is responsible for configuring the lane. It allows developers to customize how the lane behaves in terms of max block space, max transaction count, and more. The definition of the `LaneConfig` object is as follows: + +```go +// LaneConfig defines the basic configurations needed for a lane. +type LaneConfig struct { + Logger log.Logger + TxEncoder sdk.TxEncoder + TxDecoder sdk.TxDecoder + AnteHandler sdk.AnteHandler + + // SignerExtractor defines the interface used for extracting the expected signers of a transaction + // from the transaction. + SignerExtractor signer_extraction.Adapter + + // MaxBlockSpace defines the relative percentage of block space that can be + // used by this lane. NOTE: If this is set to zero, then there is no limit + // on the number of transactions that can be included in the block for this + // lane (up to maxTxBytes as provided by the request). This is useful for the default lane. + MaxBlockSpace math.LegacyDec + + // MaxTxs sets the maximum number of transactions allowed in the mempool with + // the semantics: + // - if MaxTx == 0, there is no cap on the number of transactions in the mempool + // - if MaxTx > 0, the mempool will cap the number of transactions it stores, + // and will prioritize transactions by their priority and sender-nonce + // (sequence number) when evicting transactions. + // - if MaxTx < 0, `Insert` is a no-op. + MaxTxs int +} +``` + +Each lane must define its own custom `LaneConfig` in order to be properly instantiated. Please visit [`app.go`](../../tests/app/app.go) for an example of how to implement a custom `LaneConfig`. + + + diff --git a/block/base/abci.go b/block/base/abci.go index 3ac25394..1da77d0f 100644 --- a/block/base/abci.go +++ b/block/base/abci.go @@ -42,9 +42,26 @@ func (l *BaseLane) PrepareLane( ) } + // Get the transaction info for each transaction that was selected. + txsWithInfo := make([]utils.TxWithInfo, len(txsToInclude)) + for i, tx := range txsToInclude { + txInfo, err := l.GetTxInfo(ctx, tx) + if err != nil { + l.Logger().Error( + "failed to get tx info", + "lane", l.Name(), + "err", err, + ) + + return proposal, err + } + + txsWithInfo[i] = txInfo + } + // Update the proposal with the selected transactions. This fails if the lane attempted to add // more transactions than the allocated max block space for the lane. - if err := proposal.UpdateProposal(l, txsToInclude); err != nil { + if err := proposal.UpdateProposal(l, txsWithInfo); err != nil { l.Logger().Error( "failed to update proposal", "lane", l.Name(), @@ -102,8 +119,25 @@ func (l *BaseLane) ProcessLane( return proposal, err } + // Retrieve the transaction info for each transaction that belongs to the lane. + txsWithInfo := make([]utils.TxWithInfo, len(txsFromLane)) + for i, tx := range txsFromLane { + txInfo, err := l.GetTxInfo(ctx, tx) + if err != nil { + l.Logger().Error( + "failed to get tx info", + "lane", l.Name(), + "err", err, + ) + + return proposal, err + } + + txsWithInfo[i] = txInfo + } + // Optimistically update the proposal with the partial proposal. - if err := proposal.UpdateProposal(l, txsFromLane); err != nil { + if err := proposal.UpdateProposal(l, txsWithInfo); err != nil { l.Logger().Error( "failed to update proposal", "lane", l.Name(), diff --git a/block/base/handlers.go b/block/base/handlers.go index 717f7847..9308c525 100644 --- a/block/base/handlers.go +++ b/block/base/handlers.go @@ -6,7 +6,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block/proposals" - "github.com/skip-mev/block-sdk/block/utils" ) // DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It @@ -27,7 +26,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { tx := iterator.Tx() - txInfo, err := utils.GetTxInfo(l.TxEncoder(), tx) + txInfo, err := l.GetTxInfo(ctx, tx) if err != nil { l.Logger().Info("failed to get hash of tx", "err", err) diff --git a/block/base/mempool.go b/block/base/mempool.go index 0ec52675..17cf56d6 100644 --- a/block/base/mempool.go +++ b/block/base/mempool.go @@ -102,19 +102,24 @@ func NewMempool[C comparable](txPriority TxPriority[C], txEncoder sdk.TxEncoder, } } +// Priority returns the priority of the transaction. +func (cm *Mempool[C]) Priority(ctx sdk.Context, tx sdk.Tx) any { + return cm.txPriority.GetTxPriority(ctx, tx) +} + // Insert inserts a transaction into the mempool. func (cm *Mempool[C]) Insert(ctx context.Context, tx sdk.Tx) error { if err := cm.index.Insert(ctx, tx); err != nil { return fmt.Errorf("failed to insert tx into auction index: %w", err) } - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { cm.Remove(tx) return err } - cm.txCache[txInfo.Hash] = struct{}{} + cm.txCache[hash] = struct{}{} return nil } @@ -125,12 +130,12 @@ func (cm *Mempool[C]) Remove(tx sdk.Tx) error { return fmt.Errorf("failed to remove transaction from the mempool: %w", err) } - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { return fmt.Errorf("failed to get tx hash string: %w", err) } - delete(cm.txCache, txInfo.Hash) + delete(cm.txCache, hash) return nil } @@ -150,12 +155,12 @@ func (cm *Mempool[C]) CountTx() int { // Contains returns true if the transaction is contained in the mempool. func (cm *Mempool[C]) Contains(tx sdk.Tx) bool { - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { return false } - _, ok := cm.txCache[txInfo.Hash] + _, ok := cm.txCache[hash] return ok } diff --git a/block/base/tx_info.go b/block/base/tx_info.go new file mode 100644 index 00000000..3de609ec --- /dev/null +++ b/block/base/tx_info.go @@ -0,0 +1,41 @@ +package base + +import ( + "encoding/hex" + "fmt" + "strings" + + comettypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/utils" +) + +// GetTxInfo returns various information about the transaction that +// belongs to the lane including its priority, signer's, sequence number, +// size and more. +func (l *BaseLane) GetTxInfo(ctx sdk.Context, tx sdk.Tx) (utils.TxWithInfo, error) { + txBytes, err := l.cfg.TxEncoder(tx) + if err != nil { + return utils.TxWithInfo{}, fmt.Errorf("failed to encode transaction: %w", err) + } + + // TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc. + gasTx, ok := tx.(sdk.FeeTx) + if !ok { + return utils.TxWithInfo{}, fmt.Errorf("failed to cast transaction to gas tx") + } + + signers, err := l.cfg.SignerExtractor.GetSigners(tx) + if err != nil { + return utils.TxWithInfo{}, err + } + + return utils.TxWithInfo{ + Hash: strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBytes).Hash())), + Size: int64(len(txBytes)), + GasLimit: gasTx.GetGas(), + TxBytes: txBytes, + Priority: l.LaneMempool.Priority(ctx, tx), + Signers: signers, + }, nil +} diff --git a/block/lane.go b/block/lane.go index 212f0f6d..5606e6ec 100644 --- a/block/lane.go +++ b/block/lane.go @@ -6,6 +6,7 @@ import ( sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/utils" ) // LaneMempool defines the interface a lane's mempool should implement. The basic API @@ -23,6 +24,9 @@ type LaneMempool interface { // Contains returns true if the transaction is contained in the mempool. Contains(tx sdk.Tx) bool + + // Priority returns the priority of a transaction that belongs to this lane. + Priority(ctx sdk.Context, tx sdk.Tx) any } // Lane defines an interface used for matching transactions to lanes, storing transactions, @@ -65,4 +69,9 @@ type Lane interface { // Match determines if a transaction belongs to this lane. Match(ctx sdk.Context, tx sdk.Tx) bool + + // GetTxInfo returns various information about the transaction that + // belongs to the lane including its priority, signer's, sequence number, + // size and more. + GetTxInfo(ctx sdk.Context, tx sdk.Tx) (utils.TxWithInfo, error) } diff --git a/block/mocks/lane.go b/block/mocks/lane.go index 0ea53eb0..c2cf8be4 100644 --- a/block/mocks/lane.go +++ b/block/mocks/lane.go @@ -16,6 +16,8 @@ import ( proposals "github.com/skip-mev/block-sdk/block/proposals" types "github.com/cosmos/cosmos-sdk/types" + + utils "github.com/skip-mev/block-sdk/block/utils" ) // Lane is an autogenerated mock type for the Lane type @@ -89,6 +91,30 @@ func (_m *Lane) GetMaxBlockSpace() math.LegacyDec { return r0 } +// GetTxInfo provides a mock function with given fields: ctx, tx +func (_m *Lane) GetTxInfo(ctx types.Context, tx types.Tx) (utils.TxWithInfo, error) { + ret := _m.Called(ctx, tx) + + var r0 utils.TxWithInfo + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) (utils.TxWithInfo, error)); ok { + return rf(ctx, tx) + } + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) utils.TxWithInfo); ok { + r0 = rf(ctx, tx) + } else { + r0 = ret.Get(0).(utils.TxWithInfo) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.Tx) error); ok { + r1 = rf(ctx, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Insert provides a mock function with given fields: _a0, _a1 func (_m *Lane) Insert(_a0 context.Context, _a1 types.Tx) error { ret := _m.Called(_a0, _a1) @@ -155,6 +181,22 @@ func (_m *Lane) PrepareLane(ctx types.Context, proposal proposals.Proposal, next return r0, r1 } +// Priority provides a mock function with given fields: ctx, tx +func (_m *Lane) Priority(ctx types.Context, tx types.Tx) interface{} { + ret := _m.Called(ctx, tx) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) interface{}); ok { + r0 = rf(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + // ProcessLane provides a mock function with given fields: ctx, proposal, txs, next func (_m *Lane) ProcessLane(ctx types.Context, proposal proposals.Proposal, txs []types.Tx, next block.ProcessLanesHandler) (proposals.Proposal, error) { ret := _m.Called(ctx, proposal, txs, next) diff --git a/block/mocks/lane_mempool.go b/block/mocks/lane_mempool.go index cecf96f4..7e0ca4ce 100644 --- a/block/mocks/lane_mempool.go +++ b/block/mocks/lane_mempool.go @@ -82,6 +82,22 @@ func (_m *LaneMempool) Insert(_a0 context.Context, _a1 types.Tx) error { return r0 } +// Priority provides a mock function with given fields: ctx, tx +func (_m *LaneMempool) Priority(ctx types.Context, tx types.Tx) interface{} { + ret := _m.Called(ctx, tx) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) interface{}); ok { + r0 = rf(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + // Remove provides a mock function with given fields: _a0 func (_m *LaneMempool) Remove(_a0 types.Tx) error { ret := _m.Called(_a0) diff --git a/block/proposals/README.md b/block/proposals/README.md new file mode 100644 index 00000000..33962677 --- /dev/null +++ b/block/proposals/README.md @@ -0,0 +1,21 @@ +# Proposals + +## Overview + +> The proposal type - `proposals.Proposal` - is utilized to represent a block proposal. It contains information about the total gas utilization, block size, number of transactions, raw transactions, and much more. It is recommended that you read the [proposal construction and verification](../../abci/README.md) section before continuing. + +## Proposal + +After a given lane executes its `PrepareLaneHandler` or `ProcessLaneHandler`, it will return a set of transactions that need to be added to the current proposal that is being constructed. To update the proposal, `Update` is called with the lane that needs to add transactions to the proposal as well as the transactions that need to be added. + +Proposals are updated _iff_: + +1. The total gas utilization of the partial proposal (i.e. the transactions it wants to add) are under the limits allocated for the lane and are less than the maximum gas utilization of the proposal. +2. The total size in bytes of the partial proposal is under the limits allocated for the lane and is less than the maximum size of the proposal. +3. The transactions have not already been added to the proposal. +4. The lane has not already attempted to add transactions to the proposal. + +If any of these conditions fail, the proposal will not be updated and the transactions will not be added to the proposal. The lane will be marked as having attempted to add transactions to the proposal. + +The proposal is responsible for determining the `LaneLimits` for a given lane. The `LaneLimits` are the maximum gas utilization and size in bytes that a given lane can utilize in a block proposal. This is a function of the max gas utilization and size defined by the application, the current gas utilization and size of the proposal, and the `MaxBlockSpace` allocated to the lane as defined by its `LaneConfig`. To read more about how `LaneConfigs` are defined, please visit the [lane config section](../base/README.md#laneconfig) or see an example implementation in [`app.go`](../../tests/app/app.go). + diff --git a/block/proposals/proposals_test.go b/block/proposals/proposals_test.go index f0c4ad9a..29c54089 100644 --- a/block/proposals/proposals_test.go +++ b/block/proposals/proposals_test.go @@ -7,14 +7,15 @@ import ( "cosmossdk.io/log" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - signerextraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" "github.com/stretchr/testify/require" + signerextraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" "github.com/skip-mev/block-sdk/block/base" "github.com/skip-mev/block-sdk/block/mocks" "github.com/skip-mev/block-sdk/block/proposals" "github.com/skip-mev/block-sdk/block/proposals/types" "github.com/skip-mev/block-sdk/block/utils" + defaultlane "github.com/skip-mev/block-sdk/lanes/base" "github.com/skip-mev/block-sdk/testutils" ) @@ -65,7 +66,10 @@ func TestUpdateProposal(t *testing.T) { gasLimit := 100 proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is not empty. @@ -111,7 +115,10 @@ func TestUpdateProposal(t *testing.T) { proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), gasLimit) - err = proposal.UpdateProposal(lane, txs) + txsWithInfo, err := getTxsWithInfo(txs) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is not empty. @@ -148,7 +155,10 @@ func TestUpdateProposal(t *testing.T) { gasLimit := uint64(100) proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), size, gasLimit) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is empty. @@ -163,8 +173,11 @@ func TestUpdateProposal(t *testing.T) { otherlane.On("Name").Return("test").Maybe() otherlane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe() + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + // Attempt to add the same transaction again. - err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx}) + err = proposal.UpdateProposal(otherlane, txsWithInfo) require.Error(t, err) require.Equal(t, 1, len(proposal.Txs)) @@ -208,10 +221,16 @@ func TestUpdateProposal(t *testing.T) { gasLimit := 200 proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) + require.NoError(t, err) + + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx2}) require.NoError(t, err) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx2}) + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is not empty. @@ -251,7 +270,10 @@ func TestUpdateProposal(t *testing.T) { lane.On("Name").Return("test").Maybe() lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -289,7 +311,10 @@ func TestUpdateProposal(t *testing.T) { lane.On("Name").Return("test").Maybe() lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -322,7 +347,10 @@ func TestUpdateProposal(t *testing.T) { gasLimit := 100 proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size)-1, uint64(gasLimit)) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -355,7 +383,10 @@ func TestUpdateProposal(t *testing.T) { gasLimit := 100 proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)-1) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -396,14 +427,20 @@ func TestUpdateProposal(t *testing.T) { proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), 10000, 10000) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) otherlane := mocks.NewLane(t) otherlane.On("Name").Return("test2") otherlane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("1.0")) - err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx2}) + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx2}) + require.NoError(t, err) + + err = proposal.UpdateProposal(otherlane, txsWithInfo) require.NoError(t, err) size := len(txBzs[0]) + len(txBzs[1]) diff --git a/block/proposals/update.go b/block/proposals/update.go index 0280586a..dfaf1858 100644 --- a/block/proposals/update.go +++ b/block/proposals/update.go @@ -4,15 +4,14 @@ import ( "fmt" "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block/utils" ) // Lane defines the contract interface for a lane. type Lane interface { - GetMaxBlockSpace() math.LegacyDec Name() string + GetMaxBlockSpace() math.LegacyDec } // UpdateProposal updates the proposal with the given transactions and lane limits. There are a @@ -25,7 +24,7 @@ type Lane interface { // the lane. // 5. The lane must not have already prepared a partial proposal. // 6. The transaction must not already be in the proposal. -func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error { +func (p *Proposal) UpdateProposal(lane Lane, partialProposal []utils.TxWithInfo) error { if len(partialProposal) == 0 { return nil } @@ -42,29 +41,26 @@ func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error { partialProposalGasLimit := uint64(0) for index, tx := range partialProposal { - txInfo, err := utils.GetTxInfo(p.TxEncoder, tx) - if err != nil { - return fmt.Errorf("err retrieving transaction info: %s", err) - } - p.Logger.Info( "updating proposal with tx", - "index", index, + "index", index+len(p.Txs), "lane", lane.Name(), - "tx_hash", txInfo.Hash, - "tx_size", txInfo.Size, - "tx_gas_limit", txInfo.GasLimit, + "hash", tx.Hash, + "size", tx.Size, + "gas_limit", tx.GasLimit, + "signers", tx.Signers, + "priority", tx.Priority, ) // invariant check: Ensure that the transaction is not already in the proposal. - if _, ok := p.Cache[txInfo.Hash]; ok { - return fmt.Errorf("transaction %s is already in the proposal", txInfo.Hash) + if _, ok := p.Cache[tx.Hash]; ok { + return fmt.Errorf("transaction %s is already in the proposal", tx.Hash) } - hashes[txInfo.Hash] = struct{}{} - partialProposalSize += txInfo.Size - partialProposalGasLimit += txInfo.GasLimit - txs[index] = txInfo.TxBytes + hashes[tx.Hash] = struct{}{} + partialProposalSize += tx.Size + partialProposalGasLimit += tx.GasLimit + txs[index] = tx.TxBytes } // invariant check: Ensure that the partial proposal is not too large. diff --git a/block/utils/utils.go b/block/utils/utils.go index a0826f2e..c0ecd187 100644 --- a/block/utils/utils.go +++ b/block/utils/utils.go @@ -6,47 +6,62 @@ import ( "strings" comettypes "github.com/cometbft/cometbft/types" - sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + signerextraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" ) -type ( - // TxInfo contains the information required for a transaction to be - // included in a proposal. - TxInfo struct { - // Hash is the hex-encoded hash of the transaction. - Hash string - // Size is the size of the transaction in bytes. - Size int64 - // GasLimit is the gas limit of the transaction. - GasLimit uint64 - // TxBytes is the bytes of the transaction. - TxBytes []byte - } -) +// TxWithInfo contains the information required for a transaction to be +// included in a proposal. +type TxWithInfo struct { + // Hash is the hex-encoded hash of the transaction. + Hash string + // Size is the size of the transaction in bytes. + Size int64 + // GasLimit is the gas limit of the transaction. + GasLimit uint64 + // TxBytes is the bytes of the transaction. + TxBytes []byte + // Priority defines the priority of the transaction. + Priority any + // Signers defines the signers of a transaction. + Signers []signerextraction.SignerData +} -// GetTxHashStr returns the TxInfo of a given transaction. -func GetTxInfo(txEncoder sdk.TxEncoder, tx sdk.Tx) (TxInfo, error) { - txBz, err := txEncoder(tx) - if err != nil { - return TxInfo{}, fmt.Errorf("failed to encode transaction: %w", err) +// NewTxInfo returns a new TxInfo instance. +func NewTxInfo( + hash string, + size int64, + gasLimit uint64, + txBytes []byte, + priority any, + signers []signerextraction.SignerData, +) TxWithInfo { + return TxWithInfo{ + Hash: hash, + Size: size, + GasLimit: gasLimit, + TxBytes: txBytes, + Priority: priority, + Signers: signers, } +} - txHashStr := strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBz).Hash())) +// String implements the fmt.Stringer interface. +func (t TxWithInfo) String() string { + return fmt.Sprintf("TxWithInfo{Hash: %s, Size: %d, GasLimit: %d, Priority: %s, Signers: %v}", + t.Hash, t.Size, t.GasLimit, t.Priority, t.Signers) +} - // TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc. - gasTx, ok := tx.(sdk.FeeTx) - if !ok { - return TxInfo{}, fmt.Errorf("failed to cast transaction to GasTx") +// GetTxHash returns the string hash representation of a transaction. +func GetTxHash(encoder sdk.TxEncoder, tx sdk.Tx) (string, error) { + txBz, err := encoder(tx) + if err != nil { + return "", fmt.Errorf("failed to encode transaction: %w", err) } - return TxInfo{ - Hash: txHashStr, - Size: int64(len(txBz)), - GasLimit: gasTx.GetGas(), - TxBytes: txBz, - }, nil + txHashStr := strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBz).Hash())) + return txHashStr, nil } // GetDecodedTxs returns the decoded transactions from the given bytes. diff --git a/lanes/base/abci_test.go b/lanes/base/abci_test.go index 3e154d4b..27b0c0b4 100644 --- a/lanes/base/abci_test.go +++ b/lanes/base/abci_test.go @@ -509,7 +509,10 @@ func (s *BaseTestSuite) TestPrepareLane() { mockLane.On("Name").Return("test") mockLane.On("GetMaxBlockSpace").Return(math.LegacyOneDec()) - err = emptyProposal.UpdateProposal(mockLane, []sdk.Tx{tx}) + txWithInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + + err = emptyProposal.UpdateProposal(mockLane, []utils.TxWithInfo{txWithInfo}) s.Require().NoError(err) finalProposal, err := lane.PrepareLane(s.ctx, emptyProposal, block.NoOpPrepareLanesHandler()) diff --git a/lanes/base/tx_info_test.go b/lanes/base/tx_info_test.go new file mode 100644 index 00000000..a5abb069 --- /dev/null +++ b/lanes/base/tx_info_test.go @@ -0,0 +1,94 @@ +package base_test + +import ( + "math/rand" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/testutils" +) + +func (s *BaseTestSuite) TestGetTxInfo() { + accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) + lane := s.initLane(math.LegacyOneDec(), nil) + + s.Run("can retrieve information for a default tx", func() { + signer := accounts[0] + nonce := uint64(1) + fee := sdk.NewCoins(sdk.NewCoin(s.gasTokenDenom, math.NewInt(100))) + gasLimit := uint64(100) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + signer, + nonce, + 1, + 0, + gasLimit, + fee..., + ) + s.Require().NoError(err) + + txInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + s.Require().NotEmpty(txInfo.Hash) + + // Verify the signers + s.Require().Len(txInfo.Signers, 1) + s.Require().Equal(signer.Address.String(), txInfo.Signers[0].Signer.String()) + s.Require().Equal(nonce, txInfo.Signers[0].Sequence) + + // Verify the priority + actualfee, err := sdk.ParseCoinsNormalized(txInfo.Priority.(string)) + s.Require().NoError(err) + s.Require().Equal(fee, actualfee) + + // Verify the gas limit + s.Require().Equal(gasLimit, txInfo.GasLimit) + + // Verify the bytes + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + s.Require().Equal(txBz, txInfo.TxBytes) + + // Verify the size + s.Require().Equal(int64(len(txBz)), txInfo.Size) + }) + + s.Run("can retrieve information with different fees", func() { + signer := accounts[1] + nonce := uint64(10) + fee := sdk.NewCoins(sdk.NewCoin(s.gasTokenDenom, math.NewInt(20000))) + gasLimit := uint64(10000000) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + signer, + nonce, + 10, + 0, + gasLimit, + fee..., + ) + s.Require().NoError(err) + + txInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + s.Require().NotEmpty(txInfo.Hash) + + // Verify the signers + s.Require().Len(txInfo.Signers, 1) + s.Require().Equal(signer.Address.String(), txInfo.Signers[0].Signer.String()) + s.Require().Equal(nonce, txInfo.Signers[0].Sequence) + + // Verify the priority + actualfee, err := sdk.ParseCoinsNormalized(txInfo.Priority.(string)) + s.Require().NoError(err) + s.Require().Equal(fee, actualfee) + + // Verify the bytes + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + s.Require().Equal(txBz, txInfo.TxBytes) + }) +} diff --git a/lanes/mev/abci.go b/lanes/mev/abci.go index 5b03a605..5910f214 100644 --- a/lanes/mev/abci.go +++ b/lanes/mev/abci.go @@ -8,7 +8,6 @@ import ( "github.com/skip-mev/block-sdk/block/base" "github.com/skip-mev/block-sdk/block/proposals" - "github.com/skip-mev/block-sdk/block/utils" ) // PrepareLaneHandler will attempt to select the highest bid transaction that is valid @@ -35,7 +34,9 @@ func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { continue } - bundle, err := l.VerifyBidBasic(bidTx, proposal, limit) + cacheCtx, write := ctx.CacheContext() + + bundle, err := l.VerifyBidBasic(cacheCtx, bidTx, proposal, limit) if err != nil { l.Logger().Info( "failed to select auction bid tx for lane; tx is invalid", @@ -46,7 +47,6 @@ func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { continue } - cacheCtx, write := ctx.CacheContext() if err := l.VerifyBidTx(cacheCtx, bidTx, bundle); err != nil { l.Logger().Info( "failed to select auction bid tx for lane; tx is invalid", @@ -161,6 +161,7 @@ func (l *MEVLane) ProcessLaneHandler() base.ProcessLaneHandler { // VerifyBidBasic will verify that the bid transaction and all of its bundled // transactions respect the basic invariants of the lane (e.g. size, gas limit). func (l *MEVLane) VerifyBidBasic( + ctx sdk.Context, bidTx sdk.Tx, proposal proposals.Proposal, limit proposals.LaneLimits, @@ -175,7 +176,7 @@ func (l *MEVLane) VerifyBidBasic( return nil, fmt.Errorf("bid info is nil") } - txInfo, err := utils.GetTxInfo(l.TxEncoder(), bidTx) + txInfo, err := l.GetTxInfo(ctx, bidTx) if err != nil { return nil, fmt.Errorf("err retrieving transaction info: %s", err) } @@ -196,7 +197,7 @@ func (l *MEVLane) VerifyBidBasic( return nil, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) } - bundledTxInfo, err := utils.GetTxInfo(l.TxEncoder(), bundledTx) + bundledTxInfo, err := l.GetTxInfo(ctx, bundledTx) if err != nil { return nil, fmt.Errorf("err retrieving transaction info: %s", err) } diff --git a/lanes/mev/abci_test.go b/lanes/mev/abci_test.go index b9f15251..3e4635c1 100644 --- a/lanes/mev/abci_test.go +++ b/lanes/mev/abci_test.go @@ -547,7 +547,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - bundle, err := lane.VerifyBidBasic(bidTx, proposal, limits) + bundle, err := lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().NoError(err) s.compare(bundle, expectedBundle) }) @@ -563,7 +563,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(tx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, tx, proposal, limits) s.Require().Error(err) }) @@ -579,7 +579,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) @@ -599,7 +599,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { proposal := proposals.NewProposal(log.NewTestLogger(s.T()), s.encCfg.TxConfig.TxEncoder(), size-1, 100) limits := proposal.GetLaneLimits(lane.GetMaxBlockSpace()) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) @@ -624,7 +624,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) } diff --git a/lanes/mev/mempool.go b/lanes/mev/mempool.go index cae04e62..a7e8cdcc 100644 --- a/lanes/mev/mempool.go +++ b/lanes/mev/mempool.go @@ -14,8 +14,8 @@ func TxPriority(config Factory) base.TxPriority[string] { return base.TxPriority[string]{ GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { bidInfo, err := config.GetAuctionBidInfo(tx) - if err != nil { - panic(err) + if err != nil || bidInfo == nil || bidInfo.Bid.IsNil() { + return "" } return bidInfo.Bid.String() diff --git a/lanes/terminator/lane.go b/lanes/terminator/lane.go index 5fee04e1..e74f3036 100644 --- a/lanes/terminator/lane.go +++ b/lanes/terminator/lane.go @@ -11,6 +11,7 @@ import ( "github.com/skip-mev/block-sdk/block" "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/utils" ) const ( @@ -70,6 +71,11 @@ func (t Terminator) Name() string { return LaneName } +// GetTxInfo is a no-op +func (t Terminator) GetTxInfo(_ sdk.Context, _ sdk.Tx) (utils.TxWithInfo, error) { + return utils.TxWithInfo{}, fmt.Errorf("terminator lane should not have any transactions") +} + // SetAnteHandler is a no-op func (t Terminator) SetAnteHandler(sdk.AnteHandler) {} @@ -106,7 +112,12 @@ func (t Terminator) Select(context.Context, [][]byte) sdkmempool.Iterator { return nil } -// HasHigherPriority is a no-op +// Compare is a no-op func (t Terminator) Compare(sdk.Context, sdk.Tx, sdk.Tx) (int, error) { return 0, nil } + +// Priority is a no-op +func (t Terminator) Priority(sdk.Context, sdk.Tx) any { + return 0 +} diff --git a/tests/integration/block_sdk_suite.go b/tests/integration/block_sdk_suite.go index d67ba527..4e401abe 100644 --- a/tests/integration/block_sdk_suite.go +++ b/tests/integration/block_sdk_suite.go @@ -109,7 +109,8 @@ func (s *IntegrationTestSuite) SetupSubTest() { // query height height, err := s.chain.(*cosmos.CosmosChain).Height(context.Background()) require.NoError(s.T(), err) - WaitForHeight(s.T(), s.chain.(*cosmos.CosmosChain), height+1) + WaitForHeight(s.T(), s.chain.(*cosmos.CosmosChain), height+3) + s.T().Logf("reached height %d", height+2) } func (s *IntegrationTestSuite) TestQueryParams() { @@ -1354,7 +1355,7 @@ func (s *IntegrationTestSuite) TestNetwork() { } }) - amountToTest.Reset(5 * time.Minute) + amountToTest.Reset(3 * time.Minute) s.Run("can produce blocks with all types of transactions", func() { for { select { diff --git a/tests/integration/chain_setup.go b/tests/integration/chain_setup.go index f6bd4574..1d2d9c9b 100644 --- a/tests/integration/chain_setup.go +++ b/tests/integration/chain_setup.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "io" + "math/rand" "os" "path" "strings" @@ -183,7 +184,7 @@ func (s *IntegrationTestSuite) CreateDummyFreeTx( return Tx{ User: user, Msgs: []sdk.Msg{delegateMsg}, - GasPrice: 1000, + GasPrice: rand.Int63n(150000), SequenceIncrement: sequenceOffset, SkipInclusionCheck: true, IgnoreChecks: true,