From 605d94f201449bc328b3de695f91710bb5d256d3 Mon Sep 17 00:00:00 2001 From: David Terpay Date: Mon, 14 Aug 2023 18:18:23 -0400 Subject: [PATCH] init --- blockbuster/lanes/auction/abci.go | 429 +++-- blockbuster/lanes/auction/auction_test.go | 2 - blockbuster/lanes/auction/factory.go | 11 + blockbuster/lanes/auction/lane.go | 78 +- blockbuster/lanes/auction/mempool.go | 110 +- blockbuster/mempool.go | 56 +- blockbuster/mempool_test.go | 35 +- blockbuster/proposals/abci_test.go | 1747 +++++++++------------ blockbuster/utils/ante.go | 2 +- tests/app/app.go | 36 +- x/builder/ante/ante_test.go | 7 +- 11 files changed, 1081 insertions(+), 1432 deletions(-) diff --git a/blockbuster/lanes/auction/abci.go b/blockbuster/lanes/auction/abci.go index 9a1f62b..0773c82 100644 --- a/blockbuster/lanes/auction/abci.go +++ b/blockbuster/lanes/auction/abci.go @@ -7,257 +7,247 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/blockbuster/utils" + "github.com/skip-mev/pob/x/builder/types" ) -// PrepareLane will attempt to select the highest bid transaction that is valid +// PrepareLaneHandler will attempt to select the highest bid transaction that is valid // and whose bundled transactions are valid and include them in the proposal. It -// will return an empty partial proposal if no valid bids are found. -func (l *TOBLane) PrepareLane( - ctx sdk.Context, - proposal blockbuster.BlockProposal, - maxTxBytes int64, - next blockbuster.PrepareLanesHandler, -) (blockbuster.BlockProposal, error) { - // Define all of the info we need to select transactions for the partial proposal. - var ( - txs [][]byte - txsToRemove = make(map[sdk.Tx]struct{}, 0) - ) +// will return no transactions if no valid bids are found. If any of the bids are invalid, +// it will return them and will only remove the bids and not the bundled transactions. +func (l *TOBLane) PrepareLaneHandler() blockbuster.PrepareLaneHandler { + return func(ctx sdk.Context, proposal blockbuster.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + // Define all of the info we need to select transactions for the partial proposal. + var ( + txs [][]byte + txsToRemove []sdk.Tx + ) - // Attempt to select the highest bid transaction that is valid and whose - // bundled transactions are valid. - bidTxIterator := l.Select(ctx, nil) -selectBidTxLoop: - for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { - cacheCtx, write := ctx.CacheContext() - tmpBidTx := bidTxIterator.Tx() + // Attempt to select the highest bid transaction that is valid and whose + // bundled transactions are valid. + bidTxIterator := l.Select(ctx, nil) + selectBidTxLoop: + for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { + cacheCtx, write := ctx.CacheContext() + tmpBidTx := bidTxIterator.Tx() - bidTxBz, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tmpBidTx) - if err != nil { - l.Logger().Info("failed to get hash of auction bid tx", "err", err) + bidTxBz, hash, err := utils.GetTxHashStr(l.TxEncoder(), tmpBidTx) + if err != nil { + l.Logger().Info("failed to get hash of auction bid tx", "err", err) - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(bidTxBz) { + l.Logger().Info( + "failed to select auction bid tx for lane; tx is already in proposal", + "tx_hash", hash, + ) + + continue selectBidTxLoop + } + + bidTxSize := int64(len(bidTxBz)) + if bidTxSize <= maxTxBytes { + // Build the partial proposal by selecting the bid transaction and all of + // its bundled transactions. + bidInfo, err := l.GetAuctionBidInfo(tmpBidTx) + if err != nil { + l.Logger().Info( + "failed to get auction bid info", + "tx_hash", hash, + "err", err, + ) + + // Some transactions in the bundle may be malformed or invalid, so we + // remove the bid transaction and try the next top bid. + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // Verify the bid transaction and all of its bundled transactions. + if err := l.VerifyTx(cacheCtx, tmpBidTx, bidInfo); err != nil { + l.Logger().Info( + "failed to verify auction bid tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal + bundledTxBz := make([][]byte, len(bidInfo.Transactions)) + for index, rawRefTx := range bidInfo.Transactions { + sdkTx, err := l.WrapBundleTransaction(rawRefTx) + if err != nil { + l.Logger().Info( + "failed to wrap bundled tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + sdkTxBz, _, err := utils.GetTxHashStr(l.TxEncoder(), sdkTx) + if err != nil { + l.Logger().Info( + "failed to get hash of bundled tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(sdkTxBz) { + l.Logger().Info( + "failed to select auction bid tx for lane; tx is already in proposal", + "tx_hash", hash, + ) + + continue selectBidTxLoop + } + + bundleTxBz := make([]byte, len(sdkTxBz)) + copy(bundleTxBz, sdkTxBz) + bundledTxBz[index] = sdkTxBz + } + + // At this point, both the bid transaction itself and all the bundled + // transactions are valid. So we select the bid transaction along with + // all the bundled transactions. We also mark these transactions as seen and + // update the total size selected thus far. + txs = append(txs, bidTxBz) + txs = append(txs, bundledTxBz...) + + // Write the cache context to the original context when we know we have a + // valid top of block bundle. + write() + + break selectBidTxLoop + } - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(bidTxBz) { l.Logger().Info( - "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, + "failed to select auction bid tx for lane; tx size is too large", + "tx_size", bidTxSize, + "max_size", maxTxBytes, ) - - continue selectBidTxLoop } - bidTxSize := int64(len(bidTxBz)) - if bidTxSize <= maxTxBytes { - // Verify the bid transaction and all of its bundled transactions. - if err := l.VerifyTx(cacheCtx, tmpBidTx); err != nil { - l.Logger().Info( - "failed to verify auction bid tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // Build the partial proposal by selecting the bid transaction and all of - // its bundled transactions. - bidInfo, err := l.GetAuctionBidInfo(tmpBidTx) - if bidInfo == nil || err != nil { - l.Logger().Info( - "failed to get auction bid info", - "tx_hash", hash, - "err", err, - ) - - // Some transactions in the bundle may be malformed or invalid, so we - // remove the bid transaction and try the next top bid. - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal - bundledTxBz := make([][]byte, len(bidInfo.Transactions)) - for index, rawRefTx := range bidInfo.Transactions { - sdkTx, err := l.WrapBundleTransaction(rawRefTx) - if err != nil { - l.Logger().Info( - "failed to wrap bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - sdkTxBz, _, err := utils.GetTxHashStr(l.Cfg.TxEncoder, sdkTx) - if err != nil { - l.Logger().Info( - "failed to get hash of bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(sdkTxBz) { - l.Logger().Info( - "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, - ) - - continue selectBidTxLoop - } - - bundleTxBz := make([]byte, len(sdkTxBz)) - copy(bundleTxBz, sdkTxBz) - bundledTxBz[index] = sdkTxBz - } - - // At this point, both the bid transaction itself and all the bundled - // transactions are valid. So we select the bid transaction along with - // all the bundled transactions. We also mark these transactions as seen and - // update the total size selected thus far. - txs = append(txs, bidTxBz) - txs = append(txs, bundledTxBz...) - - // Write the cache context to the original context when we know we have a - // valid top of block bundle. - write() - - break selectBidTxLoop - } - - l.Logger().Info( - "failed to select auction bid tx for lane; tx size is too large", - "tx_size", bidTxSize, - "max_size", maxTxBytes, - ) + return txs, txsToRemove, nil } - - // Remove all transactions that were invalid during the creation of the partial proposal. - if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { - l.Logger().Error( - "failed to remove transactions from lane", - "lane", l.Name(), - "err", err, - ) - - return proposal, err - } - - // Update the proposal with the selected transactions. This will only return an error - // if the invarient checks are not passed. In the case when this errors, the original proposal - // will be returned (without the selected transactions from this lane). - if err := proposal.UpdateProposal(l, txs); err != nil { - return proposal, err - } - - return next(ctx, proposal) } -// ProcessLane will ensure that block proposals that include transactions from +// ProcessLaneHandler will ensure that block proposals that include transactions from // the top-of-block auction lane are valid. -func (l *TOBLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next blockbuster.ProcessLanesHandler) (sdk.Context, error) { - if len(txs) == 0 { - return next(ctx, txs) - } +func (l *TOBLane) ProcessLaneHandler() blockbuster.ProcessLaneHandler { + return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { + if len(txs) == 0 { + return txs, nil + } - bidTx := txs[0] - if !l.Match(ctx, bidTx) { - return next(ctx, txs) - } + bidTx := txs[0] + if !l.Match(ctx, bidTx) { + return txs, nil + } - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return ctx, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) - } + bidInfo, err := l.GetAuctionBidInfo(bidTx) + if err != nil { + return nil, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + } - if err := l.VerifyTx(ctx, bidTx); err != nil { - return ctx, fmt.Errorf("invalid bid tx: %w", err) - } + if err := l.VerifyTx(ctx, bidTx, bidInfo); err != nil { + return nil, fmt.Errorf("invalid bid tx: %w", err) + } - return next(ctx, txs[len(bidInfo.Transactions)+1:]) + return txs[len(bidInfo.Transactions)+1:], nil + } } -// ProcessLaneBasic ensures that if a bid transaction is present in a proposal, +// CheckOrderHandler ensures that if a bid transaction is present in a proposal, // - it is the first transaction in the partial proposal // - all of the bundled transactions are included after the bid transaction in the order // they were included in the bid transaction. // - there are no other bid transactions in the proposal -func (l *TOBLane) ProcessLaneBasic(ctx sdk.Context, txs []sdk.Tx) error { - if len(txs) == 0 { - return nil - } +// - transactions from other lanes are not interleaved with transactions from the bid +// transaction. +func (l *TOBLane) CheckOrderHandler() blockbuster.CheckOrderHandler { + return func(ctx sdk.Context, txs []sdk.Tx) error { + if len(txs) == 0 { + return nil + } - bidTx := txs[0] + bidTx := txs[0] - // If there is a bid transaction, it must be the first transaction in the block proposal. - if !l.Match(ctx, bidTx) { - for _, tx := range txs[1:] { + // If there is a bid transaction, it must be the first transaction in the block proposal. + if !l.Match(ctx, bidTx) { + for _, tx := range txs[1:] { + if l.Match(ctx, tx) { + return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) + } + } + + return nil + } + + bidInfo, err := l.GetAuctionBidInfo(bidTx) + if err != nil { + return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + } + + if len(txs) < len(bidInfo.Transactions)+1 { + return fmt.Errorf( + "invalid number of transactions in lane %s; expected at least %d, got %d", + l.Name(), + len(bidInfo.Transactions)+1, + len(txs), + ) + } + + // Ensure that the order of transactions in the bundle is preserved. + for i, bundleTx := range txs[1 : len(bidInfo.Transactions)+1] { + if l.Match(ctx, bundleTx) { + return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) + } + + txBz, err := l.TxEncoder()(bundleTx) + if err != nil { + return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) + } + + if !bytes.Equal(txBz, bidInfo.Transactions[i]) { + return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) + } + } + + // Ensure that there are no more bid transactions in the block proposal. + for _, tx := range txs[len(bidInfo.Transactions)+1:] { if l.Match(ctx, tx) { - return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) + return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) } } return nil } - - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) - } - - if len(txs) < len(bidInfo.Transactions)+1 { - return fmt.Errorf("invalid number of transactions in lane %s; expected at least %d, got %d", l.Name(), len(bidInfo.Transactions)+1, len(txs)) - } - - // Ensure that the order of transactions in the bundle is preserved. - for i, bundleTx := range txs[1 : len(bidInfo.Transactions)+1] { - if l.Match(ctx, bundleTx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) - } - - txBz, err := l.Cfg.TxEncoder(bundleTx) - if err != nil { - return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) - } - - if !bytes.Equal(txBz, bidInfo.Transactions[i]) { - return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) - } - } - - // Ensure that there are no more bid transactions in the block proposal. - for _, tx := range txs[len(bidInfo.Transactions)+1:] { - if l.Match(ctx, tx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) - } - } - - return nil } // VerifyTx will verify that the bid transaction and all of its bundled // transactions are valid. It will return an error if any of the transactions // are invalid. -func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error { - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return fmt.Errorf("failed to get auction bid info: %w", err) +func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *types.BidInfo) (err error) { + if bidInfo == nil { + return fmt.Errorf("bid info is nil") } // verify the top-level bid transaction - ctx, err = l.verifyTx(ctx, bidTx) - if err != nil { + if ctx, err = l.AnteVerifyTx(ctx, bidTx, false); err != nil { return fmt.Errorf("invalid bid tx; failed to execute ante handler: %w", err) } @@ -268,27 +258,10 @@ func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error { return fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) } - // bid txs cannot be included in bundled txs - bidInfo, _ := l.GetAuctionBidInfo(bundledTx) - if bidInfo != nil { - return fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx") - } - - if ctx, err = l.verifyTx(ctx, bundledTx); err != nil { + if ctx, err = l.AnteVerifyTx(ctx, bundledTx, false); err != nil { return fmt.Errorf("invalid bid tx; failed to execute bundled transaction: %w", err) } } return nil } - -// verifyTx will execute the ante handler on the transaction and return the -// resulting context and error. -func (l *TOBLane) verifyTx(ctx sdk.Context, tx sdk.Tx) (sdk.Context, error) { - if l.Cfg.AnteHandler != nil { - newCtx, err := l.Cfg.AnteHandler(ctx, tx, false) - return newCtx, err - } - - return ctx, nil -} diff --git a/blockbuster/lanes/auction/auction_test.go b/blockbuster/lanes/auction/auction_test.go index 7e2eec4..ffe71ae 100644 --- a/blockbuster/lanes/auction/auction_test.go +++ b/blockbuster/lanes/auction/auction_test.go @@ -18,7 +18,6 @@ type IntegrationTestSuite struct { encCfg testutils.EncodingConfig config auction.Factory - mempool auction.Mempool ctx sdk.Context random *rand.Rand accounts []testutils.Account @@ -33,7 +32,6 @@ func (suite *IntegrationTestSuite) SetupTest() { // Mempool setup suite.encCfg = testutils.CreateTestEncodingConfig() suite.config = auction.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder()) - suite.mempool = auction.NewMempool(suite.encCfg.TxConfig.TxEncoder(), 0, suite.config) suite.ctx = sdk.NewContext(nil, cmtproto.Header{}, false, log.NewTestLogger(suite.T())) // Init accounts diff --git a/blockbuster/lanes/auction/factory.go b/blockbuster/lanes/auction/factory.go index 37c9738..6032aa9 100644 --- a/blockbuster/lanes/auction/factory.go +++ b/blockbuster/lanes/auction/factory.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/x/builder/types" ) @@ -21,6 +22,9 @@ type ( // GetAuctionBidInfo defines a function that returns the bid info from an auction transaction. GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) + + // MatchHandler defines a function that checks if a transaction matches the auction lane. + MatchHandler() blockbuster.MatchHandler } // DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions. @@ -91,6 +95,13 @@ func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*types.BidInf }, nil } +func (config *DefaultAuctionFactory) MatchHandler() blockbuster.MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + bidInfo, err := config.GetAuctionBidInfo(tx) + return bidInfo != nil && err == nil + } +} + // getBundleSigners defines a default function that returns the signers of all transactions in // a bundle. In the default case, each bundle transaction will be an sdk.Tx and the // signers are the signers of each sdk.Msg in the transaction. diff --git a/blockbuster/lanes/auction/lane.go b/blockbuster/lanes/auction/lane.go index 15be52d..4be6982 100644 --- a/blockbuster/lanes/auction/lane.go +++ b/blockbuster/lanes/auction/lane.go @@ -1,9 +1,10 @@ package auction import ( + "context" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/lanes/base" ) const ( @@ -12,8 +13,7 @@ const ( ) var ( - _ blockbuster.Lane = (*TOBLane)(nil) - _ Factory = (*TOBLane)(nil) + _ TOBLaneI = (*TOBLane)(nil) ) // TOBLane defines a top-of-block auction lane. The top of block auction lane @@ -22,43 +22,53 @@ var ( // their bid price. The highest valid bid transaction is selected for inclusion in the // next block. The bundled transactions of the selected bid transaction are also // included in the next block. -type TOBLane struct { - // Mempool defines the mempool for the lane. - Mempool +type ( + // TOBLaneI defines the interface for the top-of-block auction lane. This interface + // is utilized by both the x/builder module and the checkTx handler. + TOBLaneI interface { + blockbuster.Lane + Factory + GetTopAuctionTx(ctx context.Context) sdk.Tx + } - // LaneConfig defines the base lane configuration. - *base.DefaultLane + TOBLane struct { + // LaneConfig defines the base lane configuration. + *blockbuster.LaneConstructor[string] - // Factory defines the API/functionality which is responsible for determining - // if a transaction is a bid transaction and how to extract relevant - // information from the transaction (bid, timeout, bidder, etc.). - Factory -} + // Factory defines the API/functionality which is responsible for determining + // if a transaction is a bid transaction and how to extract relevant + // information from the transaction (bid, timeout, bidder, etc.). + Factory + } +) // NewTOBLane returns a new TOB lane. func NewTOBLane( - cfg blockbuster.BaseLaneConfig, - maxTx int, - af Factory, + cfg blockbuster.LaneConfig, + factory Factory, ) *TOBLane { - if err := cfg.ValidateBasic(); err != nil { - panic(err) + lane := &TOBLane{ + LaneConstructor: blockbuster.NewLaneConstructor[string]( + cfg, + LaneName, + blockbuster.NewConstructorMempool[string]( + TxPriority(factory), + cfg.TxEncoder, + cfg.MaxTxs, + ), + factory.MatchHandler(), + ), + Factory: factory, } - return &TOBLane{ - Mempool: NewMempool(cfg.TxEncoder, maxTx, af), - DefaultLane: base.NewDefaultLane(cfg).WithName(LaneName), - Factory: af, - } -} - -// Match returns true if the transaction is a bid transaction. This is determined -// by the AuctionFactory. -func (l *TOBLane) Match(ctx sdk.Context, tx sdk.Tx) bool { - if l.MatchIgnoreList(ctx, tx) { - return false - } - - bidInfo, err := l.GetAuctionBidInfo(tx) - return bidInfo != nil && err == nil + // Set the prepare lane handler to the TOB one + lane.SetPrepareLaneHandler(lane.PrepareLaneHandler()) + + // Set the process lane handler to the TOB one + lane.SetProcessLaneHandler(lane.ProcessLaneHandler()) + + // Set the check order handler to the TOB one + lane.SetCheckOrderHandler(lane.CheckOrderHandler()) + + return lane } diff --git a/blockbuster/lanes/auction/mempool.go b/blockbuster/lanes/auction/mempool.go index 8c71798..68a3b81 100644 --- a/blockbuster/lanes/auction/mempool.go +++ b/blockbuster/lanes/auction/mempool.go @@ -2,48 +2,9 @@ package auction import ( "context" - "errors" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/utils" -) - -var _ Mempool = (*TOBMempool)(nil) - -type ( - // Mempool defines the interface of the auction mempool. - Mempool interface { - sdkmempool.Mempool - - // GetTopAuctionTx returns the highest bidding transaction in the auction mempool. - GetTopAuctionTx(ctx context.Context) sdk.Tx - - // Contains returns true if the transaction is contained in the mempool. - Contains(tx sdk.Tx) bool - } - - // TOBMempool defines an auction mempool. It can be seen as an extension of - // an SDK PriorityNonceMempool, i.e. a mempool that supports - // two-dimensional priority ordering, with the additional support of prioritizing - // and indexing auction bids. - TOBMempool struct { - // index defines an index of auction bids. - index sdkmempool.Mempool - - // txEncoder defines the sdk.Tx encoder that allows us to encode transactions - // to bytes. - txEncoder sdk.TxEncoder - - // txIndex is a map of all transactions in the mempool. It is used - // to quickly check if a transaction is already in the mempool. - txIndex map[string]struct{} - - // Factory implements the functionality required to process auction transactions. - Factory - } ) // TxPriority returns a TxPriority over auction bid transactions only. It @@ -89,78 +50,13 @@ func TxPriority(config Factory) blockbuster.TxPriority[string] { } } -// NewMempool returns a new auction mempool. -func NewMempool(txEncoder sdk.TxEncoder, maxTx int, config Factory) *TOBMempool { - return &TOBMempool{ - index: blockbuster.NewPriorityMempool( - blockbuster.PriorityNonceMempoolConfig[string]{ - TxPriority: TxPriority(config), - MaxTx: maxTx, - }, - ), - txEncoder: txEncoder, - txIndex: make(map[string]struct{}), - Factory: config, - } -} - -// Insert inserts a transaction into the auction mempool. -func (am *TOBMempool) Insert(ctx context.Context, tx sdk.Tx) error { - if err := am.index.Insert(ctx, tx); err != nil { - return fmt.Errorf("failed to insert tx into auction index: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return err - } - - am.txIndex[txHashStr] = struct{}{} - - return nil -} - -// Remove removes a transaction from the mempool based. -func (am *TOBMempool) Remove(tx sdk.Tx) error { - if err := am.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - return fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return fmt.Errorf("failed to get tx hash string: %w", err) - } - - delete(am.txIndex, txHashStr) - - return nil -} - // GetTopAuctionTx returns the highest bidding transaction in the auction mempool. -func (am *TOBMempool) GetTopAuctionTx(ctx context.Context) sdk.Tx { - iterator := am.index.Select(ctx, nil) +// This is primarily a helper function for the x/builder module. +func (l *TOBLane) GetTopAuctionTx(ctx context.Context) sdk.Tx { + iterator := l.Select(ctx, nil) if iterator == nil { return nil } return iterator.Tx() } - -func (am *TOBMempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { - return am.index.Select(ctx, txs) -} - -func (am *TOBMempool) CountTx() int { - return am.index.CountTx() -} - -// Contains returns true if the transaction is contained in the mempool. -func (am *TOBMempool) Contains(tx sdk.Tx) bool { - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return false - } - - _, ok := am.txIndex[txHashStr] - return ok -} diff --git a/blockbuster/mempool.go b/blockbuster/mempool.go index 82f997b..938d2b6 100644 --- a/blockbuster/mempool.go +++ b/blockbuster/mempool.go @@ -31,11 +31,15 @@ type ( GetLane(name string) (Lane, error) } - // Mempool defines the Blockbuster mempool implement. It contains a registry + // BBMempool defines the Blockbuster mempool implementation. It contains a registry // of lanes, which allows for customizable block proposal construction. BBMempool struct { + logger log.Logger + + // registry contains the lanes in the mempool. The lanes are ordered + // according to their priority. The first lane in the registry has the + // highest priority and the last lane has the lowest priority. registry []Lane - logger log.Logger } ) @@ -47,8 +51,10 @@ type ( // registry. Each transaction should only belong in one lane but this is NOT enforced. // To enforce that each transaction belong to a single lane, you must configure the // ignore list of each lane to include all preceding lanes. Basic mempool API will -// attempt to insert, remove transactions from all lanes it belongs to. -func NewMempool(logger log.Logger, lanes ...Lane) *BBMempool { +// attempt to insert, remove transactions from all lanes it belongs to. It is recommended, +// that mutex is set to true when creating the mempool. This will ensure that each +// transaction cannot be inserted into the lanes before it. +func NewMempool(logger log.Logger, mutex bool, lanes ...Lane) *BBMempool { mempool := &BBMempool{ logger: logger, registry: lanes, @@ -58,6 +64,16 @@ func NewMempool(logger log.Logger, lanes ...Lane) *BBMempool { panic(err) } + // Set the ignore list for each lane + if mutex { + registry := mempool.registry + for index, lane := range mempool.registry { + if index > 0 { + lane.SetIgnoreList(registry[:index]) + } + } + } + return mempool } @@ -85,7 +101,14 @@ func (m *BBMempool) GetTxDistribution() map[string]int { // Insert will insert a transaction into the mempool. It inserts the transaction // into the first lane that it matches. -func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) error { +func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Insert", "err", r) + err = fmt.Errorf("panic in Insert: %v", r) + } + }() + var errors []string unwrappedCtx := sdk.UnwrapSDKContext(ctx) @@ -118,7 +141,14 @@ func (m *BBMempool) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator { } // Remove removes a transaction from all of the lanes it is currently in. -func (m *BBMempool) Remove(tx sdk.Tx) error { +func (m *BBMempool) Remove(tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Remove", "err", r) + err = fmt.Errorf("panic in Remove: %v", r) + } + }() + var errors []string for _, lane := range m.registry { @@ -149,7 +179,14 @@ func (m *BBMempool) Remove(tx sdk.Tx) error { } // Contains returns true if the transaction is contained in any of the lanes. -func (m *BBMempool) Contains(tx sdk.Tx) bool { +func (m *BBMempool) Contains(tx sdk.Tx) (contains bool) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Contains", "err", r) + contains = false + } + }() + for _, lane := range m.registry { if lane.Contains(tx) { return true @@ -164,7 +201,10 @@ func (m *BBMempool) Registry() []Lane { return m.registry } -// ValidateBasic validates the mempools configuration. +// ValidateBasic validates the mempools configuration. ValidateBasic ensures +// the following: +// - The sum of the lane max block space percentages is less than or equal to 1. +// - There is no unused block space. func (m *BBMempool) ValidateBasic() error { sum := math.LegacyZeroDec() seenZeroMaxBlockSpace := false diff --git a/blockbuster/mempool_test.go b/blockbuster/mempool_test.go index 2384a1b..84e470c 100644 --- a/blockbuster/mempool_test.go +++ b/blockbuster/mempool_test.go @@ -27,9 +27,10 @@ type BlockBusterTestSuite struct { encodingConfig testutils.EncodingConfig // Define all of the lanes utilized in the test suite - tobLane *auction.TOBLane - baseLane *base.DefaultLane - freeLane *free.Lane + tobLane *auction.TOBLane + baseLane *base.DefaultLane + freeLane *free.FreeLane + gasTokenDenom string lanes []blockbuster.Lane mempool blockbuster.Mempool @@ -55,7 +56,8 @@ func (suite *BlockBusterTestSuite) SetupTest() { // Lanes configuration // // TOB lane set up - tobConfig := blockbuster.BaseLaneConfig{ + suite.gasTokenDenom = "stake" + tobConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -64,37 +66,30 @@ func (suite *BlockBusterTestSuite) SetupTest() { } suite.tobLane = auction.NewTOBLane( tobConfig, - 0, // No bound on the number of transactions in the lane auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), ) // Free lane set up - freeConfig := blockbuster.BaseLaneConfig{ + freeConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - suite.tobLane, - }, } suite.freeLane = free.NewFreeLane( freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), + blockbuster.DefaultTxPriority(), + free.DefaultMatchHandler(), ) // Base lane set up - baseConfig := blockbuster.BaseLaneConfig{ + baseConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - suite.tobLane, - suite.freeLane, - }, } suite.baseLane = base.NewDefaultLane( baseConfig, @@ -102,7 +97,7 @@ func (suite *BlockBusterTestSuite) SetupTest() { // Mempool set up suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) + suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), true, suite.lanes...) // Accounts set up suite.accounts = testutils.RandomAccounts(suite.random, 10) @@ -328,12 +323,12 @@ func (suite *BlockBusterTestSuite) fillBaseLane(numTxs int) { // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) + priority := suite.random.Int63n(100) + 1 + tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs, sdk.NewCoin(suite.gasTokenDenom, math.NewInt(priority))) suite.Require().NoError(err) // insert the tx into the lane and update the account suite.nonces[acc.Address.String()]++ - priority := suite.random.Int63n(100) + 1 suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx)) } } @@ -348,7 +343,7 @@ func (suite *BlockBusterTestSuite) fillTOBLane(numTxs int) { // create a randomized auction transaction nonce := suite.nonces[acc.Address.String()] bidAmount := math.NewInt(int64(suite.random.Intn(1000) + 1)) - bid := sdk.NewCoin("stake", bidAmount) + bid := sdk.NewCoin(suite.gasTokenDenom, bidAmount) tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, nil) suite.Require().NoError(err) @@ -367,7 +362,7 @@ func (suite *BlockBusterTestSuite) fillFreeLane(numTxs int) { // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] - tx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, "val1", sdk.NewCoin("stake", math.NewInt(100))) + tx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, "val1", sdk.NewCoin(suite.gasTokenDenom, math.NewInt(100)), sdk.NewCoin(suite.gasTokenDenom, math.NewInt(100))) suite.Require().NoError(err) // insert the tx into the lane and update the account diff --git a/blockbuster/proposals/abci_test.go b/blockbuster/proposals/abci_test.go index b778edf..ee62b73 100644 --- a/blockbuster/proposals/abci_test.go +++ b/blockbuster/proposals/abci_test.go @@ -1,1071 +1,802 @@ package proposals_test import ( + "crypto/sha256" + "encoding/hex" + "fmt" "math/rand" "testing" - "time" "cosmossdk.io/log" "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" + cometabci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/abci" "github.com/skip-mev/pob/blockbuster/lanes/auction" "github.com/skip-mev/pob/blockbuster/lanes/base" "github.com/skip-mev/pob/blockbuster/lanes/free" + "github.com/skip-mev/pob/blockbuster/proposals" testutils "github.com/skip-mev/pob/testutils" - "github.com/skip-mev/pob/x/builder/ante" - "github.com/skip-mev/pob/x/builder/keeper" - buildertypes "github.com/skip-mev/pob/x/builder/types" "github.com/stretchr/testify/suite" - - abcitypes "github.com/cometbft/cometbft/abci/types" ) -type ABCITestSuite struct { +type ProposalsTestSuite struct { suite.Suite ctx sdk.Context + key *storetypes.KVStoreKey - // Define basic tx configuration encodingConfig testutils.EncodingConfig - - // Define all of the lanes utilized in the test suite - tobConfig blockbuster.BaseLaneConfig - tobLane *auction.TOBLane - - freeConfig blockbuster.BaseLaneConfig - freeLane *free.Lane - - baseConfig blockbuster.BaseLaneConfig - baseLane *base.DefaultLane - - lanes []blockbuster.Lane - mempool blockbuster.Mempool - - // Proposal handler set up - proposalHandler *abci.ProposalHandler - - // account set up - accounts []testutils.Account - random *rand.Rand - nonces map[string]uint64 - - // Keeper set up - builderKeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper - builderDecorator ante.BuilderDecorator + random *rand.Rand + accounts []testutils.Account + gasTokenDenom string } func TestBlockBusterTestSuite(t *testing.T) { - suite.Run(t, new(ABCITestSuite)) + suite.Run(t, new(ProposalsTestSuite)) } -func (suite *ABCITestSuite) SetupTest() { - // General config for transactions and randomness for the test suite - suite.encodingConfig = testutils.CreateTestEncodingConfig() - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - key := storetypes.NewKVStoreKey(buildertypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx.WithBlockHeight(1) +func (s *ProposalsTestSuite) SetupTest() { + // Set up basic TX encoding config. + s.encodingConfig = testutils.CreateTestEncodingConfig() - // Lanes configuration - // Top of block lane set up - suite.tobConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - } - suite.tobLane = auction.NewTOBLane( - suite.tobConfig, - 0, // No bound on the number of transactions in the lane - auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) + // Create a few random accounts + s.random = rand.New(rand.NewSource(1)) + s.accounts = testutils.RandomAccounts(s.random, 5) + s.gasTokenDenom = "stake" - // Free lane set up - suite.freeConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - IgnoreList: []blockbuster.Lane{suite.tobLane}, - } - suite.freeLane = free.NewFreeLane( - suite.freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Base lane set up - suite.baseConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - IgnoreList: []blockbuster.Lane{suite.tobLane, suite.freeLane}, - } - suite.baseLane = base.NewDefaultLane( - suite.baseConfig, - ) - - // Mempool set up - suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) - - // Accounts set up - suite.accounts = testutils.RandomAccounts(suite.random, 10) - suite.nonces = make(map[string]uint64) - for _, acc := range suite.accounts { - suite.nonces[acc.Address.String()] = 0 - } - - // Set up the keepers and decorators - // Mock keepers set up - ctrl := gomock.NewController(suite.T()) - suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) - suite.accountKeeper.EXPECT().GetModuleAddress(buildertypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes() - suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) - suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) - suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) - - // Builder keeper / decorator set up - suite.builderKeeper = keeper.NewKeeper( - suite.encodingConfig.Codec, - key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - sdk.AccAddress([]byte("authority")).String(), - ) - - // Set the default params for the builder keeper - err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) - suite.Require().NoError(err) - - // Set up the ante handler - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - // Proposal handler set up - suite.proposalHandler = abci.NewProposalHandler(log.NewTestLogger(suite.T()), suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool) + s.key = storetypes.NewKVStoreKey("test") + testCtx := testutil.DefaultContextWithDB(s.T(), s.key, storetypes.NewTransientStoreKey("transient_test")) + s.ctx = testCtx.Ctx.WithIsCheckTx(true) } -func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { - suite.bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), "stake").AnyTimes().Return( - sdk.NewCoin("stake", math.NewInt(100000000000000)), - ) +func (s *ProposalsTestSuite) TestPrepareProposal() { + s.Run("can prepare a proposal with no transactions", func() { + // Set up the default lane with no transactions + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), nil) - next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { - return ctx, nil - } + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() - return suite.builderDecorator.AnteHandle(ctx, tx, false, next) -} + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(0, len(resp.Txs)) + }) -func (suite *ABCITestSuite) resetLanesWithNewConfig() { - // Top of block lane set up - suite.tobLane = auction.NewTOBLane( - suite.tobConfig, - 0, // No bound on the number of transactions in the lane - auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Free lane set up - suite.freeLane = free.NewFreeLane( - suite.freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Base lane set up - suite.baseLane = base.NewDefaultLane( - suite.baseConfig, - ) - - suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) -} - -func (suite *ABCITestSuite) TestPrepareProposal() { - var ( - // the modified transactions cannot exceed this size - maxTxBytes int64 = 1000000000000000000 - - // mempool configuration - txs []sdk.Tx - auctionTxs []sdk.Tx - winningBidTx sdk.Tx - insertBundledTxs = false - - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - minBidIncrement = sdk.NewCoin("stake", math.NewInt(100)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - expectedNumberProposalTxs int - expectedMempoolDistribution map[string]int - }{ - { - "empty mempool", - func() { - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{} - winningBidTx = nil - insertBundledTxs = false - }, + s.Run("can build a proposal with a single tx from the lane", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], 0, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "maxTxBytes is less than any transaction in the mempool", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[2] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - maxTxBytes = 10 - }, - 0, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "valid tob tx but maxTxBytes is less for the tob lane so only the free tx should be included", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - maxTxBytes = tobSize + freeSize - suite.tobConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.1") - - txs = []sdk.Tx{freeTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - }, 1, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "valid tob tx with sufficient space for only tob tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - maxTxBytes = tobSize*2 + freeSize - 1 - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.freeConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.1") - - txs = []sdk.Tx{freeTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "tob, free, and normal tx but only space for tob and normal tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - // Get the size of the normal tx - normalTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(normalTx) - suite.Require().NoError(err) - normalSize := int64(len(normalTxBytes)) - - maxTxBytes = tobSize*2 + freeSize + normalSize + 1 - - // Tob can take up as much space as it wants - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - - // Free can take up less space than the tx - suite.freeConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.01") - - // Default can take up as much space as it wants - suite.baseConfig.MaxBlockSpace = math.LegacyZeroDec() - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 4, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "single valid tob transaction in the mempool", - func() { - // reset the configs - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.freeConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.baseConfig.MaxBlockSpace = math.LegacyZeroDec() - - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - maxTxBytes = 1000000000000000000 - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "single invalid tob transaction in the mempool", - func() { - bidder := suite.accounts[0] - bid := reserveFee.Sub(sdk.NewCoin("stake", math.NewInt(1))) // bid is less than the reserve fee - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - }, 0, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "normal transactions in the mempool", - func() { - account := suite.accounts[0] - nonce := suite.nonces[account.Address.String()] - timeout := uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{} - winningBidTx = nil - insertBundledTxs = false - }, + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal with multiple txs from the lane", func() { + // Create a random transaction that will be inserted into the default lane + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, 1, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "normal transactions and tob transactions in the mempool", - func() { - // Create a valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - // Create a valid default transaction - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] + 1 - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + // Create a second random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 3, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is invalid", - func() { - // Create an invalid tob transaction (frontrunning) - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder, bidder, suite.accounts[1]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + // Set up the default lane with both transactions passing + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx1: true, tx2: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - winningBidTx = bidTx2 - insertBundledTxs = false - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is valid", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposal := s.getTxBytes(tx2, tx1) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + s.Run("can build a proposal with single tx with other that fails", func() { + // Create a random transaction that will be inserted into the default lane + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - winningBidTx = bidTx - insertBundledTxs = false - }, - 3, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 2, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is valid and bundle is inserted into mempool", - func() { - frontRunningProtection = false + // Create a second random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), + ) + s.Require().NoError(err) - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + // Set up the default lane with both transactions passing + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx1: true, tx2: false}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = true - }, - 6, - map[string]int{ - base.LaneName: 5, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "valid tob, free, and normal tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) + proposal := s.getTxBytes(tx1) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + s.Run("can build a proposal an empty proposal with multiple lanes", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), nil) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 5, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - } + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() - suite.resetLanesWithNewConfig() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) + s.Require().NoError(err) + s.Require().NotNil(resp) - // Insert all of the normal transactions into the default lane - for _, tx := range txs { - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } + s.Require().Equal(0, len(resp.Txs)) + }) - // Insert all of the auction transactions into the TOB lane - for _, tx := range auctionTxs { - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } + s.Run("can build a proposal with transactions from a single lane given multiple lanes", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) - // Insert all of the bundled transactions into the TOB lane if desired - if insertBundledTxs { - for _, tx := range auctionTxs { - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - for _, txBz := range bidInfo.Transactions { - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz) - suite.Require().NoError(err) - - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } - } - } - - // Create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - for _, lane := range suite.lanes { - lane.SetAnteHandler(suite.anteHandler) - } - - // Create a new proposal handler - suite.proposalHandler = abci.NewProposalHandler(log.NewTestLogger(suite.T()), suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool) - handler := suite.proposalHandler.PrepareProposalHandler() - res, err := handler(suite.ctx, &abcitypes.RequestPrepareProposal{ - MaxTxBytes: maxTxBytes, - }) - suite.Require().NoError(err) - - // -------------------- Check Invariants -------------------- // - // 1. the number of transactions in the response must be equal to the number of expected transactions - suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs)) - - // 2. total bytes must be less than or equal to maxTxBytes - totalBytes := int64(0) - txIndex := 0 - for txIndex < len(res.Txs) { - totalBytes += int64(len(res.Txs[txIndex])) - - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[txIndex]) - suite.Require().NoError(err) - - suite.Require().Equal(true, suite.mempool.Contains(tx)) - - // In the case where we have a tob tx, we skip the other transactions in the bundle - // in order to not double count - switch { - case suite.tobLane.Match(suite.ctx, tx): - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - txIndex += len(bidInfo.Transactions) + 1 - default: - txIndex++ - } - } - - suite.Require().LessOrEqual(totalBytes, maxTxBytes) - - // 3. if there are auction transactions, the first transaction must be the top bid - // and the rest of the bundle must be in the response - if winningBidTx != nil { - auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0]) - suite.Require().NoError(err) - - bidInfo, err := suite.tobLane.GetAuctionBidInfo(auctionTx) - suite.Require().NoError(err) - - for index, tx := range bidInfo.Transactions { - suite.Require().Equal(tx, res.Txs[index+1]) - } - } else if len(res.Txs) > 0 { - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0]) - suite.Require().NoError(err) - - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - suite.Require().Nil(bidInfo) - } - - // 4. All of the transactions must be unique - uniqueTxs := make(map[string]bool) - for _, tx := range res.Txs { - suite.Require().False(uniqueTxs[string(tx)]) - uniqueTxs[string(tx)] = true - } - - // 5. The number of transactions in the mempool must be correct - suite.Require().Equal(tc.expectedMempoolDistribution, suite.mempool.GetTxDistribution()) - - // 6. The ordering of transactions must respect the ordering of the lanes - laneIndex := 0 - txIndex = 0 - for txIndex < len(res.Txs) { - sdkTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[txIndex]) - suite.Require().NoError(err) - - if suite.lanes[laneIndex].Match(suite.ctx, sdkTx) { - switch suite.lanes[laneIndex].Name() { - case suite.tobLane.Name(): - bidInfo, err := suite.tobLane.GetAuctionBidInfo(sdkTx) - suite.Require().NoError(err) - - txIndex += len(bidInfo.Transactions) + 1 - default: - txIndex++ - } - } else { - laneIndex++ - } - - suite.Require().Less(laneIndex, len(suite.lanes)) - } + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, }) - } + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx, bundleTxs[0]) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can ignore txs that are already included in a proposal", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx, bundleTxs[0]) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal where first lane has too large of a tx and second lane has a valid tx", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: false, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + // Even though this passes it should not include it in the proposal because it is in the ignore list + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(bundleTxs[0]) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal where first lane cannot fit txs but second lane can", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + // Even though this passes it should not include it in the proposal because it is in the ignore list + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + proposal := s.getTxBytes(tx, bundleTxs[0]) + size := int64(len(proposal[0]) - 1) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: size}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal[1:], resp.Txs) + }) + + s.Run("can build a proposal with single tx from middle lane", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), nil) + + freeTx, err := testutils.CreateFreeTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + "test", + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + freeTx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, freeTx)) + + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + freeTx: true, + }) + s.Require().NoError(freeLane.Insert(sdk.Context{}, freeTx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).PrepareProposalHandler() + + proposal := s.getTxBytes(freeTx) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("transaction from every lane", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:4], + ) + s.Require().NoError(err) + + // Create a normal tx + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Create a free tx + freeTx, err := testutils.CreateFreeTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + "test", + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + bundleTxs[1]: true, + bundleTxs[2]: true, + bundleTxs[3]: true, + }) + tobLane.Insert(sdk.Context{}, tx) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + normalTx: true, + }) + defaultLane.Insert(sdk.Context{}, normalTx) + + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + freeTx: true, + }) + freeLane.Insert(sdk.Context{}, freeTx) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).PrepareProposalHandler() + proposal := s.getTxBytes(tx, bundleTxs[0], bundleTxs[1], bundleTxs[2], bundleTxs[3], freeTx, normalTx) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(7, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) } -func (suite *ABCITestSuite) TestProcessProposal() { - var ( - // mempool configuration - txs []sdk.Tx - auctionTxs []sdk.Tx - insertRefTxs = false +func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { + s.Run("can build a proposal if a lane panics first", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - frontRunningProtection = true + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := proposals.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{panicLane, defaultLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if second lane panics", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := proposals.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{defaultLane, panicLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if multiple consecutive lanes panic", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + panicLane2 := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := proposals.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{panicLane, panicLane2, defaultLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if the last few lanes panic", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + panicLane2 := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := proposals.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{defaultLane, panicLane, panicLane2}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) +} + +func (s *ProposalsTestSuite) TestProcessProposal() { + s.Run("can process a valid empty proposal", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{}) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).ProcessProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: nil}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, resp) + }) + + s.Run("rejects a proposal with bad txs", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{}) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).ProcessProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{{0x01, 0x02, 0x03}}}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + + s.Run("rejects a proposal when a lane panics", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.0")) + + txbz, err := testutils.CreateRandomTxBz( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + ) + s.Require().NoError(err) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, panicLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{txbz}}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + + s.Run("can process a invalid proposal (out of order)", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Create a random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(tx, tx2)}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) + + s.Run("can process a invalid proposal where first lane is valid second is not", func() { + bidTx, bundle, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 1, + s.accounts[0:2], + ) + s.Require().NoError(err) + + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + normalTx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), + ) + s.Require().NoError(err) + + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) + defaultLane.SetProcessLaneHandler(blockbuster.NoOpProcessLaneHandler()) + + // Set up the TOB lane + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), nil) + tobLane.SetProcessLaneHandler(blockbuster.NoOpProcessLaneHandler()) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(bidTx, bundle[0], bundle[1], normalTx, normalTx2)}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) +} + +func (s *ProposalsTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk.AnteHandler { + txCache := make(map[string]bool) + for tx, pass := range expectedExecution { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + txCache[hashStr] = pass + } + + anteHandler := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + + pass, found := txCache[hashStr] + if !found { + return ctx, fmt.Errorf("tx not found") + } + + if pass { + return ctx, nil + } + + return ctx, fmt.Errorf("tx failed") + } + + return anteHandler +} + +func (s *ProposalsTestSuite) setUpDefaultLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *base.DefaultLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return base.NewDefaultLane(cfg) +} + +func (s *ProposalsTestSuite) setUpTOBLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *auction.TOBLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return auction.NewTOBLane(cfg, auction.NewDefaultAuctionFactory(cfg.TxDecoder)) +} + +func (s *ProposalsTestSuite) setUpFreeLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *free.FreeLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return free.NewFreeLane(cfg, blockbuster.DefaultTxPriority(), free.DefaultMatchHandler()) +} + +func (s *ProposalsTestSuite) setUpPanicLane(maxBlockSpace math.LegacyDec) *blockbuster.LaneConstructor[string] { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + MaxBlockSpace: maxBlockSpace, + } + + lane := blockbuster.NewLaneConstructor[string]( + cfg, + "panic", + blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), cfg.TxEncoder, 0), + blockbuster.DefaultMatchHandler(), ) - cases := []struct { - name string - malleate func() - response abcitypes.ResponseProcessProposal_ProposalStatus - }{ - { - "no normal tx, no tob tx", - func() { - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single default tx", - func() { - account := suite.accounts[0] - nonce := suite.nonces[account.Address.String()] - timeout := uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + lane.SetPrepareLaneHandler(blockbuster.PanicPrepareLaneHandler()) + lane.SetProcessLaneHandler(blockbuster.PanicProcessLaneHandler()) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{} - insertRefTxs = false - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single tob tx without bundled txs in proposal", - func() { - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = false - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single tob tx with bundled txs in proposal", - func() { - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[1], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single invalid tob tx (front-running)", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "multiple tob txs in the proposal", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single tob tx with front-running disabled and multiple other txs", - func() { - frontRunningProtection = false - - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a few other transactions - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - timeout = uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - timeout = uint64(100) - numberMsgs = uint64(3) - normalTx2, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{normalTx, normalTx2} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "tob, free, and default tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "tob, free, and default tx with default and free mixed", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{normalTx, freeTx} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - } - - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() - - // Insert all of the transactions into the proposal - proposalTxs := make([][]byte, 0) - for _, tx := range auctionTxs { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, txBz) - - if insertRefTxs { - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, bidInfo.Transactions...) - } - } - - for _, tx := range txs { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, txBz) - } - - // create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - handler := suite.proposalHandler.ProcessProposalHandler() - res, err := handler(suite.ctx, &abcitypes.RequestProcessProposal{ - Txs: proposalTxs, - }) - - // Check if the response is valid - suite.Require().Equal(tc.response, res.Status) - - if res.Status == abcitypes.ResponseProcessProposal_ACCEPT { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } + return lane +} + +func (s *ProposalsTestSuite) setUpProposalHandlers(lanes []blockbuster.Lane) *proposals.ProposalHandler { + mempool := blockbuster.NewMempool(log.NewTestLogger(s.T()), true, lanes...) + + return proposals.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + mempool.Registry(), + ) +} + +func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte { + txBytes := make([][]byte, len(txs)) + for i, tx := range txs { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + txBytes[i] = bz + } + return txBytes } diff --git a/blockbuster/utils/ante.go b/blockbuster/utils/ante.go index 418662e..fe9e12f 100644 --- a/blockbuster/utils/ante.go +++ b/blockbuster/utils/ante.go @@ -5,7 +5,7 @@ import ( ) type ( - // Lane defines the required functionality for a lane. The ignore decorator + // Lane defines the required API dependencies for the IgnoreDecorator. The ignore decorator // will check if a transaction belongs to a lane by calling the Match function. Lane interface { Match(ctx sdk.Context, tx sdk.Tx) bool diff --git a/tests/app/app.go b/tests/app/app.go index ab52db1..774d592 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -62,10 +62,10 @@ import ( stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/abci" "github.com/skip-mev/pob/blockbuster/lanes/auction" "github.com/skip-mev/pob/blockbuster/lanes/base" "github.com/skip-mev/pob/blockbuster/lanes/free" + "github.com/skip-mev/pob/blockbuster/proposals" buildermodule "github.com/skip-mev/pob/x/builder" builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) @@ -139,7 +139,7 @@ type TestApp struct { FeeGrantKeeper feegrantkeeper.Keeper // custom checkTx handler - checkTxHandler abci.CheckTx + checkTxHandler proposals.CheckTx } func init() { @@ -262,43 +262,39 @@ func New( // NOTE: The lanes are ordered by priority. The first lane is the highest priority // lane and the last lane is the lowest priority lane. // Top of block lane allows transactions to bid for inclusion at the top of the next block. - tobConfig := blockbuster.BaseLaneConfig{ + tobConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), - MaxBlockSpace: math.LegacyZeroDec(), + MaxBlockSpace: math.LegacyZeroDec(), // This means the lane has no limit on block space. + MaxTxs: 0, // This means the lane has no limit on the number of transactions it can store. } tobLane := auction.NewTOBLane( tobConfig, - 0, auction.NewDefaultAuctionFactory(app.txConfig.TxDecoder()), ) // Free lane allows transactions to be included in the next block for free. - freeConfig := blockbuster.BaseLaneConfig{ + freeConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - tobLane, - }, + MaxTxs: 0, } freeLane := free.NewFreeLane( freeConfig, - free.NewDefaultFreeFactory(app.txConfig.TxDecoder()), + blockbuster.DefaultTxPriority(), + free.DefaultMatchHandler(), ) // Default lane accepts all other transactions. - defaultConfig := blockbuster.BaseLaneConfig{ + defaultConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - tobLane, - freeLane, - }, + MaxTxs: 0, } defaultLane := base.NewDefaultLane(defaultConfig) @@ -308,7 +304,7 @@ func New( freeLane, defaultLane, } - mempool := blockbuster.NewMempool(app.Logger(), lanes...) + mempool := blockbuster.NewMempool(app.Logger(), true, lanes...) app.App.SetMempool(mempool) // Create a global ante handler that will be called on each transaction when @@ -338,16 +334,16 @@ func New( app.App.SetAnteHandler(anteHandler) // Set the proposal handlers on base app - proposalHandler := abci.NewProposalHandler( + proposalHandler := proposals.NewProposalHandler( app.Logger(), app.TxConfig().TxDecoder(), - mempool, + lanes, ) app.App.SetPrepareProposal(proposalHandler.PrepareProposalHandler()) app.App.SetProcessProposal(proposalHandler.ProcessProposalHandler()) // Set the custom CheckTx handler on BaseApp. - checkTxHandler := abci.NewCheckTxHandler( + checkTxHandler := proposals.NewCheckTxHandler( app.App, app.txConfig.TxDecoder(), tobLane, @@ -396,7 +392,7 @@ func (app *TestApp) CheckTx(req *cometabci.RequestCheckTx) (*cometabci.ResponseC } // SetCheckTx sets the checkTxHandler for the app. -func (app *TestApp) SetCheckTx(handler abci.CheckTx) { +func (app *TestApp) SetCheckTx(handler proposals.CheckTx) { app.checkTxHandler = handler } diff --git a/x/builder/ante/ante_test.go b/x/builder/ante/ante_test.go index 1c93700..5608d0b 100644 --- a/x/builder/ante/ante_test.go +++ b/x/builder/ante/ante_test.go @@ -83,7 +83,7 @@ func (suite *AnteTestSuite) SetupTest() { // Lanes configuration // // TOB lane set up - tobConfig := blockbuster.BaseLaneConfig{ + tobConfig := blockbuster.LaneConfig{ Logger: suite.ctx.Logger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -92,12 +92,11 @@ func (suite *AnteTestSuite) SetupTest() { } suite.tobLane = auction.NewTOBLane( tobConfig, - 0, // No bound on the number of transactions in the lane auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), ) // Base lane set up - baseConfig := blockbuster.BaseLaneConfig{ + baseConfig := blockbuster.LaneConfig{ Logger: suite.ctx.Logger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -109,7 +108,7 @@ func (suite *AnteTestSuite) SetupTest() { // Mempool set up suite.lanes = []blockbuster.Lane{suite.tobLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) + suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), true, suite.lanes...) } func (suite *AnteTestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {