From b7780a3140a0b28332912e8a9ed198c836823c4e Mon Sep 17 00:00:00 2001 From: David Terpay <35130517+davidterpay@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:47:34 -0400 Subject: [PATCH] feat(bb): Defer in proposal handlers, more interfaces for lanes, clean up (#165) --- abci/abci.go | 285 ------- abci/abci_test.go | 766 ++++------------- abci/{v2 => }/auction_test.go | 8 +- abci/{v2 => }/proposal_auction.go | 119 ++- abci/proposals.go | 152 ++++ abci/proposals_test.go | 776 ++++++++++++++++++ abci/{v2 => }/types.go | 2 +- abci/v2/abci_test.go | 336 -------- abci/v2/proposals.go | 237 ------ abci/v2/proposals_test.go | 766 ----------------- abci/{v2 => }/vote_extensions.go | 85 +- abci/{v2 => }/vote_extensions_test.go | 102 ++- blockbuster/abci/abci.go | 99 ++- {abci => blockbuster/abci}/check_tx.go | 36 +- blockbuster/lane.go | 62 +- blockbuster/lanes/auction/abci.go | 176 ++-- blockbuster/lanes/auction/auction_test.go | 47 ++ blockbuster/lanes/auction/factory.go | 16 +- .../lanes/auction/factory_test.go | 2 +- blockbuster/lanes/auction/lane.go | 27 +- blockbuster/lanes/auction/mempool.go | 16 +- .../lanes/auction}/utils_test.go | 10 +- blockbuster/lanes/base/abci.go | 66 +- blockbuster/lanes/base/lane.go | 28 +- blockbuster/lanes/base/mempool.go | 12 +- blockbuster/lanes/terminator/lane.go | 21 +- blockbuster/mempool.go | 28 +- {mempool => blockbuster}/priority_nonce.go | 2 +- blockbuster/{ => utils}/utils.go | 14 +- mempool/auction_bid.go | 128 --- mempool/mempool.go | 240 ------ mempool/mempool_test.go | 240 ------ mempool/utils.go | 35 - tests/app/ante.go | 7 +- tests/app/app.go | 57 +- x/builder/ante/ante.go | 21 +- x/builder/ante/ante_test.go | 66 +- x/builder/keeper/auction.go | 4 +- x/builder/keeper/auction_test.go | 7 +- x/builder/keeper/keeper_test.go | 5 - x/builder/types/bid_info.go | 12 + 41 files changed, 1798 insertions(+), 3320 deletions(-) delete mode 100644 abci/abci.go rename abci/{v2 => }/auction_test.go (98%) rename abci/{v2 => }/proposal_auction.go (71%) create mode 100644 abci/proposals.go create mode 100644 abci/proposals_test.go rename abci/{v2 => }/types.go (99%) delete mode 100644 abci/v2/abci_test.go delete mode 100644 abci/v2/proposals.go delete mode 100644 abci/v2/proposals_test.go rename abci/{v2 => }/vote_extensions.go (67%) rename abci/{v2 => }/vote_extensions_test.go (72%) rename {abci => blockbuster/abci}/check_tx.go (88%) create mode 100644 blockbuster/lanes/auction/auction_test.go rename mempool/auction_bid_test.go => blockbuster/lanes/auction/factory_test.go (99%) rename {mempool => blockbuster/lanes/auction}/utils_test.go (79%) rename {mempool => blockbuster}/priority_nonce.go (99%) rename blockbuster/{ => utils}/utils.go (75%) delete mode 100644 mempool/auction_bid.go delete mode 100644 mempool/mempool.go delete mode 100644 mempool/mempool_test.go delete mode 100644 mempool/utils.go create mode 100644 x/builder/types/bid_info.go diff --git a/abci/abci.go b/abci/abci.go deleted file mode 100644 index 89d0644..0000000 --- a/abci/abci.go +++ /dev/null @@ -1,285 +0,0 @@ -package abci - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - - abci "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/log" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" - "github.com/skip-mev/pob/mempool" -) - -type ( - Mempool interface { - sdkmempool.Mempool - - // The AuctionFactory interface is utilized to retrieve, validate, and wrap bid - // information into the block proposal. - mempool.AuctionFactory - - // AuctionBidSelect returns an iterator that iterates over the top bid - // transactions in the mempool. - AuctionBidSelect(ctx context.Context) sdkmempool.Iterator - } - - ProposalHandler struct { - mempool Mempool - logger log.Logger - anteHandler sdk.AnteHandler - txEncoder sdk.TxEncoder - txDecoder sdk.TxDecoder - } -) - -func NewProposalHandler( - mp Mempool, - logger log.Logger, - anteHandler sdk.AnteHandler, - txEncoder sdk.TxEncoder, - txDecoder sdk.TxDecoder, -) *ProposalHandler { - return &ProposalHandler{ - mempool: mp, - logger: logger, - anteHandler: anteHandler, - txEncoder: txEncoder, - txDecoder: txDecoder, - } -} - -// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs -// top-of-block auctioning and general block proposal construction. -func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { - return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal { - var ( - selectedTxs [][]byte - totalTxBytes int64 - ) - - bidTxIterator := h.mempool.AuctionBidSelect(ctx) - txsToRemove := make(map[sdk.Tx]struct{}, 0) - seenTxs := make(map[string]struct{}, 0) - - // Attempt to select the highest bid transaction that is valid and whose - // bundled transactions are valid. - selectBidTxLoop: - for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { - cacheCtx, write := ctx.CacheContext() - tmpBidTx := bidTxIterator.Tx() - - bidTxBz, err := h.PrepareProposalVerifyTx(cacheCtx, tmpBidTx) - if err != nil { - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - bidTxSize := int64(len(bidTxBz)) - if bidTxSize <= req.MaxTxBytes { - bidInfo, err := h.mempool.GetAuctionBidInfo(tmpBidTx) - if err != nil { - // Some transactions in the bundle may be malformatted 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 - bundledTransactions := bidInfo.Transactions - sdkTxBytes := make([][]byte, len(bundledTransactions)) - - // Ensure that the bundled transactions are valid - for index, rawRefTx := range bundledTransactions { - refTx, err := h.mempool.WrapBundleTransaction(rawRefTx) - if err != nil { - // Malformed bundled transaction, so we remove the bid transaction - // and try the next top bid. - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - txBz, err := h.PrepareProposalVerifyTx(cacheCtx, refTx) - if err != nil { - // Invalid bundled transaction, so we remove the bid transaction - // and try the next top bid. - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - sdkTxBytes[index] = txBz - } - - // 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. - totalTxBytes += bidTxSize - selectedTxs = append(selectedTxs, bidTxBz) - selectedTxs = append(selectedTxs, sdkTxBytes...) - - for _, refTxRaw := range sdkTxBytes { - hash := sha256.Sum256(refTxRaw) - txHash := hex.EncodeToString(hash[:]) - seenTxs[txHash] = struct{}{} - } - - // Write the cache context to the original context when we know we have a - // valid top of block bundle. - write() - - break selectBidTxLoop - } - - txsToRemove[tmpBidTx] = struct{}{} - h.logger.Info( - "failed to select auction bid tx; tx size is too large", - "tx_size", bidTxSize, - "max_size", req.MaxTxBytes, - ) - } - - // Remove all invalid transactions from the mempool. - for tx := range txsToRemove { - h.RemoveTx(tx) - } - - iterator := h.mempool.Select(ctx, nil) - txsToRemove = map[sdk.Tx]struct{}{} - - // Select remaining transactions for the block proposal until we've reached - // size capacity. - selectTxLoop: - for ; iterator != nil; iterator = iterator.Next() { - memTx := iterator.Tx() - - // If the transaction is already included in the proposal, then we skip it. - txBz, err := h.txEncoder(memTx) - if err != nil { - txsToRemove[memTx] = struct{}{} - continue selectTxLoop - } - - hash := sha256.Sum256(txBz) - txHash := hex.EncodeToString(hash[:]) - if _, ok := seenTxs[txHash]; ok { - continue selectTxLoop - } - - txBz, err = h.PrepareProposalVerifyTx(ctx, memTx) - if err != nil { - txsToRemove[memTx] = struct{}{} - continue selectTxLoop - } - - txSize := int64(len(txBz)) - if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes { - selectedTxs = append(selectedTxs, txBz) - } else { - // We've reached capacity per req.MaxTxBytes so we cannot select any - // more transactions. - break selectTxLoop - } - } - - // Remove all invalid transactions from the mempool. - for tx := range txsToRemove { - h.RemoveTx(tx) - } - - return abci.ResponsePrepareProposal{Txs: selectedTxs} - } -} - -// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs -// block proposal verification. -func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { - return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal { - for index, txBz := range req.Txs { - tx, err := h.ProcessProposalVerifyTx(ctx, txBz) - if err != nil { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - bidInfo, err := h.mempool.GetAuctionBidInfo(tx) - if err != nil { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - // If the transaction is an auction bid, then we need to ensure that it is - // the first transaction in the block proposal and that the order of - // transactions in the block proposal follows the order of transactions in - // the bid. - if bidInfo != nil { - if index != 0 { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - bundledTransactions := bidInfo.Transactions - if len(req.Txs) < len(bundledTransactions)+1 { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - for i, refTxRaw := range bundledTransactions { - // Wrap and then encode the bundled transaction to ensure that the underlying - // reference transaction can be processed as an sdk.Tx. - wrappedTx, err := h.mempool.WrapBundleTransaction(refTxRaw) - if err != nil { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - refTxBz, err := h.txEncoder(wrappedTx) - if err != nil { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - if !bytes.Equal(refTxBz, req.Txs[i+1]) { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - } - } - } - - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT} - } -} - -// PrepareProposalVerifyTx encodes a transaction and verifies it. -func (h *ProposalHandler) PrepareProposalVerifyTx(ctx sdk.Context, tx sdk.Tx) ([]byte, error) { - txBz, err := h.txEncoder(tx) - if err != nil { - return nil, err - } - - return txBz, h.verifyTx(ctx, tx) -} - -// ProcessProposalVerifyTx decodes a transaction and verifies it. -func (h *ProposalHandler) ProcessProposalVerifyTx(ctx sdk.Context, txBz []byte) (sdk.Tx, error) { - tx, err := h.txDecoder(txBz) - if err != nil { - return nil, err - } - - return tx, h.verifyTx(ctx, tx) -} - -// VerifyTx verifies a transaction against the application's state. -func (h *ProposalHandler) verifyTx(ctx sdk.Context, tx sdk.Tx) error { - if h.anteHandler != nil { - _, err := h.anteHandler(ctx, tx, false) - return err - } - - return nil -} - -func (h *ProposalHandler) RemoveTx(tx sdk.Tx) { - if err := h.mempool.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) - } -} diff --git a/abci/abci_test.go b/abci/abci_test.go index 4968338..2d698b2 100644 --- a/abci/abci_test.go +++ b/abci/abci_test.go @@ -1,22 +1,20 @@ package abci_test import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "fmt" "math/rand" "testing" "time" - abcitypes "github.com/cometbft/cometbft/abci/types" + comettypes "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/log" storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" "github.com/skip-mev/pob/abci" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/auction" + "github.com/skip-mev/pob/blockbuster/lanes/base" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/ante" "github.com/skip-mev/pob/x/builder/keeper" @@ -28,17 +26,15 @@ type ABCITestSuite struct { suite.Suite ctx sdk.Context - // mempool setup - mempool *mempool.AuctionMempool - logger log.Logger - encodingConfig testutils.EncodingConfig - proposalHandler *abci.ProposalHandler - config mempool.AuctionFactory - txs map[string]struct{} + // mempool and lane set up + mempool blockbuster.Mempool + tobLane *auction.TOBLane + baseLane *base.DefaultLane - // auction bid setup - auctionBidAmount sdk.Coin - minBidIncrement sdk.Coin + logger log.Logger + encodingConfig testutils.EncodingConfig + proposalHandler *abci.ProposalHandler + voteExtensionHandler *abci.VoteExtensionHandler // builder setup builderKeeper keeper.Keeper @@ -68,13 +64,34 @@ func (suite *ABCITestSuite) SetupTest() { suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey) testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) suite.ctx = testCtx.Ctx.WithBlockHeight(1) + suite.logger = log.NewNopLogger() + + // Lanes configuration + // + // TOB lane set up + config := blockbuster.BaseLaneConfig{ + Logger: suite.logger, + TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: suite.anteHandler, + MaxBlockSpace: sdk.ZeroDec(), + } + suite.tobLane = auction.NewTOBLane( + config, + 0, // No bound on the number of transactions in the lane + auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), + ) + + // Base lane set up + suite.baseLane = base.NewDefaultLane( + config, + ) // Mempool set up - suite.config = mempool.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()) - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) - suite.txs = make(map[string]struct{}) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(1000000000)) - suite.minBidIncrement = sdk.NewCoin("foo", sdk.NewInt(1000)) + suite.mempool = blockbuster.NewMempool( + suite.tobLane, + suite.baseLane, + ) // Mock keepers set up ctrl := gomock.NewController(suite.T()) @@ -97,10 +114,10 @@ func (suite *ABCITestSuite) SetupTest() { ) err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) suite.Require().NoError(err) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) // Accounts set up - suite.accounts = testutils.RandomAccounts(suite.random, 1) + suite.accounts = testutils.RandomAccounts(suite.random, 10) suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000))) suite.nonces = make(map[string]uint64) for _, acc := range suite.accounts { @@ -108,663 +125,172 @@ func (suite *ABCITestSuite) SetupTest() { } // Proposal handler set up - suite.logger = log.NewNopLogger() - suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) + suite.proposalHandler = abci.NewProposalHandler( + []blockbuster.Lane{suite.baseLane}, // only the base lane is used for proposal handling + suite.tobLane, + suite.logger, + suite.encodingConfig.TxConfig.TxEncoder(), + suite.encodingConfig.TxConfig.TxDecoder(), + ) + suite.voteExtensionHandler = abci.NewVoteExtensionHandler( + suite.tobLane, + suite.encodingConfig.TxConfig.TxDecoder(), + suite.encodingConfig.TxConfig.TxEncoder(), + ) } -func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { +func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { signer := tx.GetMsgs()[0].GetSigners()[0] suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances) - next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil } - ctx, err := suite.builderDecorator.AnteHandle(ctx, tx, false, next) - if err != nil { - return ctx, err - } - - bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - if err != nil { - return ctx, err - } - - if !simulate { - hash := sha256.Sum256(bz) - txHash := hex.EncodeToString(hash[:]) - if _, ok := suite.txs[txHash]; ok { - return ctx, fmt.Errorf("tx already in mempool") - } - suite.txs[txHash] = struct{}{} - } - - return ctx, nil + return suite.builderDecorator.AnteHandle(ctx, tx, false, next) } -func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int { - // Insert a bunch of normal transactions into the global mempool - for i := 0; i < numNormalTxs; i++ { +// fillBaseLane fills the base lane with numTxs transactions that are randomly created. +func (suite *ABCITestSuite) fillBaseLane(numTxs int) { + for i := 0; i < numTxs; i++ { // randomly select an account to create the tx randomIndex := suite.random.Intn(len(suite.accounts)) acc := suite.accounts[randomIndex] - // create a few random msgs - randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - + // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] - randomTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) + randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) + tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) 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), randomTx)) + suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx)) } +} - suite.Require().Equal(numNormalTxs, suite.mempool.CountTx()) - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) - +// fillTOBLane fills the TOB lane with numTxs transactions that are randomly created. +func (suite *ABCITestSuite) fillTOBLane(numTxs int, numBundledTxs int) { // Insert a bunch of auction transactions into the global mempool and auction mempool - for i := 0; i < numAuctionTxs; i++ { + for i := 0; i < numTxs; i++ { // randomly select a bidder to create the tx randomIndex := suite.random.Intn(len(suite.accounts)) acc := suite.accounts[randomIndex] - // create a new auction bid msg with numBundledTxs bundled transactions + // create a randomized auction transaction nonce := suite.nonces[acc.Address.String()] - bidMsg, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, acc, suite.auctionBidAmount, nonce, numBundledTxs) - suite.nonces[acc.Address.String()] += uint64(numBundledTxs) - suite.Require().NoError(err) + bidAmount := sdk.NewInt(int64(suite.random.Intn(1000) + 1)) + bid := sdk.NewCoin("foo", bidAmount) - // create the auction tx - nonce = suite.nonces[acc.Address.String()] - auctionTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg}) + signers := []testutils.Account{} + for j := 0; j < numBundledTxs; j++ { + signers = append(signers, suite.accounts[0]) + } + + tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, signers) suite.Require().NoError(err) // insert the auction tx into the global mempool - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx)) + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) suite.nonces[acc.Address.String()]++ - - if insertRefTxs { - for _, refRawTx := range bidMsg.GetTransactions() { - refTx, err := suite.encodingConfig.TxConfig.TxDecoder()(refRawTx) - suite.Require().NoError(err) - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx)) - } - } - - // decrement the bid amount for the next auction tx - suite.auctionBidAmount = suite.auctionBidAmount.Sub(suite.minBidIncrement) } - - numSeenGlobalTxs := 0 - for iterator := suite.mempool.Select(suite.ctx, nil); iterator != nil; iterator = iterator.Next() { - numSeenGlobalTxs++ - } - - numSeenAuctionTxs := 0 - for iterator := suite.mempool.AuctionBidSelect(suite.ctx); iterator != nil; iterator = iterator.Next() { - numSeenAuctionTxs++ - } - - var totalNumTxs int - suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx()) - if insertRefTxs { - totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs) - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } else { - totalNumTxs = numNormalTxs - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } - - suite.Require().Equal(numAuctionTxs, numSeenAuctionTxs) - - return totalNumTxs } -func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte { - txs := make([][]byte, 0) - seenTxs := make(map[string]bool) +func (suite *ABCITestSuite) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal { + voteExtensions := make([]comettypes.ExtendedVoteInfo, 0) - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) + auctionIterator := suite.tobLane.Select(suite.ctx, nil) for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { - auctionTx := auctionIterator.Tx() - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx) + tx := auctionIterator.Tx() + + txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) suite.Require().NoError(err) - txs = append(txs, txBz) - - if exportRefTxs { - for _, refRawTx := range auctionTx.GetMsgs()[0].(*buildertypes.MsgAuctionBid).GetTransactions() { - txs = append(txs, refRawTx) - seenTxs[string(refRawTx)] = true - } - } - - seenTxs[string(txBz)] = true + voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{ + VoteExtension: txBz, + }) } - iterator := suite.mempool.Select(suite.ctx, nil) - for ; iterator != nil; iterator = iterator.Next() { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx()) + return comettypes.RequestPrepareProposal{ + MaxTxBytes: maxBytes, + LocalLastCommit: comettypes.ExtendedCommitInfo{ + Votes: voteExtensions, + }, + } +} + +func (suite *ABCITestSuite) createExtendedCommitInfoFromTxs(txs []sdk.Tx) comettypes.ExtendedCommitInfo { + voteExtensions := make([][]byte, 0) + for _, tx := range txs { + bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) suite.Require().NoError(err) - if !seenTxs[string(txBz)] { - txs = append(txs, txBz) + voteExtensions = append(voteExtensions, bz) + } + + return suite.createExtendedCommitInfo(voteExtensions) +} + +func (suite *ABCITestSuite) createExtendedVoteInfo(voteExtensions [][]byte) []comettypes.ExtendedVoteInfo { + commitInfo := make([]comettypes.ExtendedVoteInfo, 0) + for _, voteExtension := range voteExtensions { + info := comettypes.ExtendedVoteInfo{ + VoteExtension: voteExtension, } + + commitInfo = append(commitInfo, info) } - return txs + return commitInfo } -func (suite *ABCITestSuite) TestPrepareProposal() { - var ( - // the modified transactions cannot exceed this size - maxTxBytes int64 = 1000000000000000000 - - // mempool configuration - numNormalTxs = 100 - numAuctionTxs = 100 - numBundledTxs = 3 - insertRefTxs = false - - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - expectedNumberProposalTxs int - expectedNumberTxsInMempool int - isTopBidValid bool - }{ - { - "single bundle in the mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - }, - 4, - 3, - true, - }, - { - "single bundle in the mempool, no ref txs in mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = false - }, - 4, - 0, - true, - }, - { - "single bundle in the mempool, not valid", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - }, - 0, - 0, - false, - }, - { - "single bundle in the mempool, not valid with ref txs in mempool", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - }, - 3, - 3, - false, - }, - { - "multiple bundles in the mempool, no normal txs + no ref txs in mempool", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000000)) - numNormalTxs = 0 - numAuctionTxs = 10 - numBundledTxs = 3 - insertRefTxs = false - }, - 4, - 0, - true, - }, - { - "multiple bundles in the mempool, no normal txs + ref txs in mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 10 - numBundledTxs = 3 - insertRefTxs = true - }, - 31, - 30, - true, - }, - { - "normal txs only", - func() { - numNormalTxs = 1 - numAuctionTxs = 0 - numBundledTxs = 0 - }, - 1, - 1, - false, - }, - { - "many normal txs only", - func() { - numNormalTxs = 100 - numAuctionTxs = 0 - numBundledTxs = 0 - }, - 100, - 100, - false, - }, - { - "single normal tx, single auction tx", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 0 - }, - 2, - 1, - true, - }, - { - "single normal tx, single auction tx with ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = false - }, - 5, - 1, - true, - }, - { - "single normal tx, single failing auction tx with ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000)) // this will fail the ante handler - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000000000)) - }, - 4, - 4, - false, - }, - { - "many normal tx, single auction tx with no ref txs", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000000)) - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 0 - }, - 101, - 100, - true, - }, - { - "many normal tx, single auction tx with ref txs", - func() { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - }, - 104, - 103, - true, - }, - { - "many normal tx, single auction tx with ref txs", - func() { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = false - }, - 104, - 100, - true, - }, - { - "many normal tx, many auction tx with ref txs", - func() { - numNormalTxs = 100 - numAuctionTxs = 100 - numBundledTxs = 1 - insertRefTxs = true - }, - 201, - 200, - true, - }, +func (suite *ABCITestSuite) createExtendedCommitInfo(voteExtensions [][]byte) comettypes.ExtendedCommitInfo { + commitInfo := comettypes.ExtendedCommitInfo{ + Votes: suite.createExtendedVoteInfo(voteExtensions), } - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() + return commitInfo +} - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) +func (suite *ABCITestSuite) createExtendedCommitInfoFromTxBzs(txs [][]byte) []byte { + voteExtensions := make([]comettypes.ExtendedVoteInfo, 0) - // create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: suite.minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) - - handler := suite.proposalHandler.PrepareProposalHandler() - res := handler(suite.ctx, abcitypes.RequestPrepareProposal{ - MaxTxBytes: maxTxBytes, - }) - - // -------------------- Check Invariants -------------------- // - // 1. The auction tx must fail if we know it is invalid - suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid()) - - // 2. total bytes must be less than or equal to maxTxBytes - totalBytes := int64(0) - if suite.isTopBidValid() { - totalBytes += int64(len(res.Txs[0])) - - for _, tx := range res.Txs[1+numBundledTxs:] { - totalBytes += int64(len(tx)) - } - } else { - for _, tx := range res.Txs { - totalBytes += int64(len(tx)) - } - } - suite.Require().LessOrEqual(totalBytes, maxTxBytes) - - // 3. the number of transactions in the response must be equal to the number of expected transactions - suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs)) - - // 4. 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 suite.isTopBidValid() { - auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0]) - suite.Require().NoError(err) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTx) - suite.Require().NoError(err) - - for index, tx := range bidInfo.Transactions { - suite.Require().Equal(tx, res.Txs[index+1]) - } - } - - // 5. 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 - } - - // 6. The number of transactions in the mempool must be correct - suite.Require().Equal(tc.expectedNumberTxsInMempool, suite.mempool.CountTx()) + for _, txBz := range txs { + voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{ + VoteExtension: txBz, }) } -} -func (suite *ABCITestSuite) TestProcessProposal() { - var ( - // mempool set up - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - exportRefTxs = true - frontRunningTx sdk.Tx - - // auction set up - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - isTopBidValid bool - response abcitypes.ResponseProcessProposal_ProposalStatus - }{ - { - "single normal tx, no auction tx", - func() { - numNormalTxs = 1 - numAuctionTxs = 0 - numBundledTxs = 0 - }, - false, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single auction tx, no normal txs", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 0 - }, - true, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single auction tx, single auction tx", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 0 - }, - true, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single auction tx, single auction tx with ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 4 - }, - true, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single auction tx, single auction tx with no ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 4 - insertRefTxs = false - exportRefTxs = false - }, - true, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "multiple auction txs, single normal tx", - func() { - numNormalTxs = 1 - numAuctionTxs = 2 - numBundledTxs = 4 - insertRefTxs = true - exportRefTxs = true - }, - true, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single auction txs, multiple normal tx", - func() { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 4 - }, - true, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single invalid auction tx, multiple normal tx", - func() { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 4 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000000000000000)) - insertRefTxs = true - }, - false, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single valid auction txs but missing ref txs", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 4 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - insertRefTxs = false - exportRefTxs = false - }, - true, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single valid auction txs but missing ref txs, with many normal txs", - func() { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 4 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - insertRefTxs = false - exportRefTxs = false - }, - true, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "auction tx with frontrunning", - func() { - randomAccount := testutils.RandomAccounts(suite.random, 1)[0] - bidder := suite.accounts[0] - bid := sdk.NewCoin("foo", sdk.NewInt(696969696969)) - nonce := suite.nonces[bidder.Address.String()] - frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount}) - suite.Require().NotNil(frontRunningTx) - - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 4 - insertRefTxs = true - exportRefTxs = true - }, - false, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "auction tx with frontrunning, but frontrunning protection disabled", - func() { - randomAccount := testutils.RandomAccounts(suite.random, 1)[0] - bidder := suite.accounts[0] - bid := sdk.NewCoin("foo", sdk.NewInt(696969696969)) - nonce := suite.nonces[bidder.Address.String()] - frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount}) - suite.Require().NotNil(frontRunningTx) - - numAuctionTxs = 0 - frontRunningProtection = false - }, - true, - abcitypes.ResponseProcessProposal_ACCEPT, - }, + commitInfo := comettypes.ExtendedCommitInfo{ + Votes: voteExtensions, } - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() + commitInfoBz, err := commitInfo.Marshal() + suite.Require().NoError(err) - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - if frontRunningTx != nil { - suite.Require().NoError(suite.mempool.Insert(suite.ctx, frontRunningTx)) - } - - // create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: suite.minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) - suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid()) - - txs := suite.exportMempool(exportRefTxs) - - if frontRunningTx != nil { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(frontRunningTx) - suite.Require().NoError(err) - - suite.Require().True(bytes.Equal(txs[0], txBz)) - } - - handler := suite.proposalHandler.ProcessProposalHandler() - res := handler(suite.ctx, abcitypes.RequestProcessProposal{ - Txs: txs, - }) - - // Check if the response is valid - suite.Require().Equal(tc.response, res.Status) - }) - } + return commitInfoBz } -// isTopBidValid returns true if the top bid is valid. We purposefully insert invalid -// auction transactions into the mempool to test the handlers. -func (suite *ABCITestSuite) isTopBidValid() bool { - iterator := suite.mempool.AuctionBidSelect(suite.ctx) - if iterator == nil { - return false +func (suite *ABCITestSuite) createAuctionInfoFromTxBzs(txs [][]byte, numTxs uint64) []byte { + auctionInfo := abci.AuctionInfo{ + ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs), + NumTxs: numTxs, + MaxTxBytes: int64(len(txs[0])), } - // check if the top bid is valid - _, err := suite.anteHandler(suite.ctx, iterator.Tx(), true) - return err == nil + auctionInfoBz, err := auctionInfo.Marshal() + suite.Require().NoError(err) + + return auctionInfoBz +} + +func (suite *ABCITestSuite) getAuctionBidInfoFromTxBz(txBz []byte) *buildertypes.BidInfo { + tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz) + suite.Require().NoError(err) + + bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) + suite.Require().NoError(err) + + return bidInfo } diff --git a/abci/v2/auction_test.go b/abci/auction_test.go similarity index 98% rename from abci/v2/auction_test.go rename to abci/auction_test.go index cfeb487..c76bc85 100644 --- a/abci/v2/auction_test.go +++ b/abci/auction_test.go @@ -1,4 +1,4 @@ -package v2_test +package abci_test import ( sdk "github.com/cosmos/cosmos-sdk/types" @@ -481,17 +481,17 @@ func (suite *ABCITestSuite) TestBuildTOB() { proposal := suite.proposalHandler.BuildTOB(suite.ctx, commitInfo, tc.maxBytes) // Size of the proposal should be less than or equal to the max bytes - suite.Require().LessOrEqual(proposal.Size, tc.maxBytes) + suite.Require().LessOrEqual(proposal.TotalTxBytes, tc.maxBytes) if winningBid == nil { suite.Require().Len(proposal.Txs, 0) - suite.Require().Equal(proposal.Size, int64(0)) + suite.Require().Equal(proposal.TotalTxBytes, int64(0)) } else { // Get info about the winning bid winningBidBz, err := suite.encodingConfig.TxConfig.TxEncoder()(winningBid) suite.Require().NoError(err) - auctionBidInfo, err := suite.mempool.GetAuctionBidInfo(winningBid) + auctionBidInfo, err := suite.tobLane.GetAuctionBidInfo(winningBid) suite.Require().NoError(err) // Verify that the size of the proposal is the size of the winning bid diff --git a/abci/v2/proposal_auction.go b/abci/proposal_auction.go similarity index 71% rename from abci/v2/proposal_auction.go rename to abci/proposal_auction.go index cd4a797..629e378 100644 --- a/abci/v2/proposal_auction.go +++ b/abci/proposal_auction.go @@ -1,40 +1,20 @@ -package v2 +package abci import ( - "crypto/sha256" - "encoding/hex" "fmt" "reflect" "sort" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" - pobabci "github.com/skip-mev/pob/abci" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/utils" ) -// TopOfBlock contains information about how the top of block should be built. -type TopOfBlock struct { - // Txs contains the transactions that should be included in the top of block. - Txs [][]byte - - // Size is the total size of the top of block. - Size int64 - - // Cache is the cache of transactions that were seen, stored in order to ignore them - // when building the rest of the block. - Cache map[string]struct{} -} - -func NewTopOfBlock() TopOfBlock { - return TopOfBlock{ - Cache: make(map[string]struct{}), - } -} - // BuildTOB inputs all of the vote extensions and outputs a top of block proposal // that includes the highest bidding valid transaction along with all the bundled // transactions. -func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.ExtendedCommitInfo, maxBytes int64) TopOfBlock { +func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.ExtendedCommitInfo, maxBytes int64) *blockbuster.Proposal { // Get the bid transactions from the vote extensions. sortedBidTxs := h.GetBidsFromVoteExtensions(voteExtensionInfo.Votes) @@ -43,14 +23,14 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten // Attempt to select the highest bid transaction that is valid and whose // bundled transactions are valid. - var topOfBlock TopOfBlock + topOfBlock := blockbuster.NewProposal(maxBytes) for _, bidTx := range sortedBidTxs { // Cache the context so that we can write it back to the original context // when we know we have a valid top of block bundle. cacheCtx, write := ctx.CacheContext() // Attempt to build the top of block using the bid transaction. - proposal, err := h.buildTOB(cacheCtx, bidTx) + proposal, err := h.buildTOB(cacheCtx, bidTx, maxBytes) if err != nil { h.logger.Info( "vote extension auction failed to verify auction tx", @@ -60,27 +40,22 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten continue } - if proposal.Size <= maxBytes { - // 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 and apply the state changes to the cache - // context. - topOfBlock = proposal - write() + // 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 and apply the state changes to the cache + // context. + topOfBlock = proposal + write() - break - } - - h.logger.Info( - "failed to select auction bid tx; auction tx size is too large", - "tx_size", proposal.Size, - "max_size", maxBytes, - ) + break } // Remove all of the transactions that were not valid. - for tx := range txsToRemove { - h.RemoveTx(tx) + if err := utils.RemoveTxsFromLane(txsToRemove, h.tobLane); err != nil { + h.logger.Error( + "failed to remove transactions from lane", + "err", err, + ) } return topOfBlock @@ -88,14 +63,14 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten // VerifyTOB verifies that the set of vote extensions used in prepare proposal deterministically // produce the same top of block proposal. -func (h *ProposalHandler) VerifyTOB(ctx sdk.Context, proposalTxs [][]byte) (*pobabci.AuctionInfo, error) { +func (h *ProposalHandler) VerifyTOB(ctx sdk.Context, proposalTxs [][]byte) (*AuctionInfo, error) { // Proposal must include at least the auction info. if len(proposalTxs) < NumInjectedTxs { return nil, fmt.Errorf("proposal is too small; expected at least %d slots", NumInjectedTxs) } // Extract the auction info from the proposal. - auctionInfo := &pobabci.AuctionInfo{} + auctionInfo := &AuctionInfo{} if err := auctionInfo.Unmarshal(proposalTxs[AuctionInfoIndex]); err != nil { return nil, fmt.Errorf("failed to unmarshal auction info: %w", err) } @@ -141,12 +116,12 @@ func (h *ProposalHandler) GetBidsFromVoteExtensions(voteExtensions []abci.Extend // Sort the auction transactions by their bid amount in descending order. sort.Slice(bidTxs, func(i, j int) bool { // In the case of an error, we want to sort the transaction to the end of the list. - bidInfoI, err := h.mempool.GetAuctionBidInfo(bidTxs[i]) + bidInfoI, err := h.tobLane.GetAuctionBidInfo(bidTxs[i]) if err != nil { return false } - bidInfoJ, err := h.mempool.GetAuctionBidInfo(bidTxs[j]) + bidInfoJ, err := h.tobLane.GetAuctionBidInfo(bidTxs[j]) if err != nil { return true } @@ -161,16 +136,29 @@ func (h *ProposalHandler) GetBidsFromVoteExtensions(voteExtensions []abci.Extend // returns the transactions that should be included in the top of block, size // of the auction transaction and bundle, and a cache of all transactions that // should be ignored. -func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx) (TopOfBlock, error) { - proposal := NewTopOfBlock() +func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx, maxBytes int64) (*blockbuster.Proposal, error) { + proposal := blockbuster.NewProposal(maxBytes) - // Ensure that the bid transaction is valid - bidTxBz, err := h.PrepareProposalVerifyTx(ctx, bidTx) + // cache the bytes of the bid transaction + txBz, hash, err := utils.GetTxHashStr(h.txEncoder, bidTx) if err != nil { return proposal, err } - bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx) + proposal.Cache[hash] = struct{}{} + proposal.TotalTxBytes = int64(len(txBz)) + proposal.Txs = append(proposal.Txs, txBz) + + if int64(len(txBz)) > maxBytes { + return proposal, fmt.Errorf("bid transaction is too large; got %d, max %d", len(txBz), maxBytes) + } + + // Ensure that the bid transaction is valid + if err := h.tobLane.VerifyTx(ctx, bidTx); err != nil { + return proposal, err + } + + bidInfo, err := h.tobLane.GetAuctionBidInfo(bidTx) if err != nil { return proposal, err } @@ -181,34 +169,23 @@ func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx) (TopOfBlock, e // Ensure that the bundled transactions are valid for index, rawRefTx := range bidInfo.Transactions { // convert the bundled raw transaction to a sdk.Tx - refTx, err := h.mempool.WrapBundleTransaction(rawRefTx) + refTx, err := h.tobLane.WrapBundleTransaction(rawRefTx) if err != nil { - return TopOfBlock{}, err + return proposal, err } - txBz, err := h.PrepareProposalVerifyTx(ctx, refTx) + // convert the sdk.Tx to a hash and bytes + txBz, hash, err := utils.GetTxHashStr(h.txEncoder, refTx) if err != nil { - return TopOfBlock{}, err + return proposal, err } - hashBz := sha256.Sum256(txBz) - hash := hex.EncodeToString(hashBz[:]) - proposal.Cache[hash] = struct{}{} sdkTxBytes[index] = txBz } - // cache the bytes of the bid transaction - hashBz := sha256.Sum256(bidTxBz) - hash := hex.EncodeToString(hashBz[:]) - proposal.Cache[hash] = struct{}{} - - txs := [][]byte{bidTxBz} - txs = append(txs, sdkTxBytes...) - - // Set the top of block transactions and size. - proposal.Txs = txs - proposal.Size = int64(len(bidTxBz)) + // Add the bundled transactions to the proposal. + proposal.Txs = append(proposal.Txs, sdkTxBytes...) return proposal, nil } @@ -227,7 +204,7 @@ func (h *ProposalHandler) getAuctionTxFromVoteExtension(voteExtension []byte) (s } // Verify the auction transaction has bid information. - if bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil { + if bidInfo, err := h.tobLane.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil { return nil, fmt.Errorf("vote extension does not contain an auction transaction") } diff --git a/abci/proposals.go b/abci/proposals.go new file mode 100644 index 0000000..7c80ef5 --- /dev/null +++ b/abci/proposals.go @@ -0,0 +1,152 @@ +package abci + +import ( + "errors" + "fmt" + + cometabci "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" + 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/abci" + "github.com/skip-mev/pob/blockbuster/lanes/auction" +) + +const ( + // NumInjectedTxs is the minimum number of transactions that were injected into + // the proposal but are not actual transactions. In this case, the auction + // info is injected into the proposal but should be ignored by the application.ß + NumInjectedTxs = 1 + + // AuctionInfoIndex is the index of the auction info in the proposal. + AuctionInfoIndex = 0 +) + +type ( + // TOBLaneProposal is the interface that defines all of the dependencies that + // are required to interact with the top of block lane. + TOBLaneProposal interface { + sdkmempool.Mempool + + // 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.). + auction.Factory + + // VerifyTx is utilized to verify a bid transaction according to the preferences + // of the top of block lane. + VerifyTx(ctx sdk.Context, tx sdk.Tx) error + + // ProcessLaneBasic is utilized to verify the rest of the proposal according to + // the preferences of the top of block lane. This is used to verify that no + ProcessLaneBasic(txs [][]byte) error + } + + // ProposalHandler contains the functionality and handlers required to\ + // process, validate and build blocks. + ProposalHandler struct { + prepareLanesHandler blockbuster.PrepareLanesHandler + processLanesHandler blockbuster.ProcessLanesHandler + tobLane TOBLaneProposal + logger log.Logger + txEncoder sdk.TxEncoder + txDecoder sdk.TxDecoder + } +) + +// NewProposalHandler returns a ProposalHandler that contains the functionality and handlers +// required to process, validate and build blocks. +func NewProposalHandler( + lanes []blockbuster.Lane, + tobLane TOBLaneProposal, + logger log.Logger, + txEncoder sdk.TxEncoder, + txDecoder sdk.TxDecoder, +) *ProposalHandler { + return &ProposalHandler{ + prepareLanesHandler: abci.ChainPrepareLanes(lanes...), + processLanesHandler: abci.ChainProcessLanes(lanes...), + tobLane: tobLane, + logger: logger, + txEncoder: txEncoder, + txDecoder: txDecoder, + } +} + +// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs +// top-of-block auctioning and general block proposal construction. +func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { + return func(ctx sdk.Context, req cometabci.RequestPrepareProposal) cometabci.ResponsePrepareProposal { + // Build the top of block portion of the proposal given the vote extensions + // from the previous block. + topOfBlock := h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes) + + // If information is unable to be marshaled, we return an empty proposal. This will + // cause another proposal to be generated after it is rejected in ProcessProposal. + lastCommitInfo, err := req.LocalLastCommit.Marshal() + if err != nil { + h.logger.Error("failed to marshal last commit info", "err", err) + return cometabci.ResponsePrepareProposal{Txs: nil} + } + + auctionInfo := &AuctionInfo{ + ExtendedCommitInfo: lastCommitInfo, + MaxTxBytes: req.MaxTxBytes, + NumTxs: uint64(len(topOfBlock.Txs)), + } + + // Add the auction info and top of block transactions into the proposal. + auctionInfoBz, err := auctionInfo.Marshal() + if err != nil { + h.logger.Error("failed to marshal auction info", "err", err) + return cometabci.ResponsePrepareProposal{Txs: nil} + } + + topOfBlock.Txs = append([][]byte{auctionInfoBz}, topOfBlock.Txs...) + + // Prepare the proposal by selecting transactions from each lane according to + // each lane's selection logic. + proposal := h.prepareLanesHandler(ctx, topOfBlock) + + return cometabci.ResponsePrepareProposal{Txs: proposal.Txs} + } +} + +// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs +// block proposal verification. +func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { + return func(ctx sdk.Context, req cometabci.RequestProcessProposal) cometabci.ResponseProcessProposal { + proposal := req.Txs + + // Verify that the same top of block transactions can be built from the vote + // extensions included in the proposal. + auctionInfo, err := h.VerifyTOB(ctx, proposal) + if err != nil { + h.logger.Error("failed to verify top of block transactions", "err", err) + return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT} + } + + // Do a basic check of the rest of the proposal to make sure no auction transactions + // are included. + if err := h.tobLane.ProcessLaneBasic(proposal[NumInjectedTxs:]); err != nil { + h.logger.Error("failed to process proposal", "err", err) + return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT} + } + + // Verify that the rest of the proposal is valid according to each lane's verification logic. + if _, err = h.processLanesHandler(ctx, proposal[auctionInfo.NumTxs:]); err != nil { + h.logger.Error("failed to process proposal", "err", err) + return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT} + } + + return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT} + } +} + +// RemoveTx removes a transaction from the application-side mempool. +func (h *ProposalHandler) RemoveTx(tx sdk.Tx) { + if err := h.tobLane.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { + panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) + } +} diff --git a/abci/proposals_test.go b/abci/proposals_test.go new file mode 100644 index 0000000..f84e764 --- /dev/null +++ b/abci/proposals_test.go @@ -0,0 +1,776 @@ +package abci_test + +import ( + comettypes "github.com/cometbft/cometbft/abci/types" + "github.com/cometbft/cometbft/libs/log" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/abci" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/auction" + "github.com/skip-mev/pob/blockbuster/lanes/base" + testutils "github.com/skip-mev/pob/testutils" + "github.com/skip-mev/pob/x/builder/ante" + buildertypes "github.com/skip-mev/pob/x/builder/types" +) + +func (suite *ABCITestSuite) TestPrepareProposal() { + var ( + // the modified transactions cannot exceed this size + maxTxBytes int64 = 1000000000000000000 + + // mempool configuration + normalTxs []sdk.Tx + auctionTxs []sdk.Tx + winningBidTx sdk.Tx + insertBundledTxs = false + + // auction configuration + maxBundleSize uint32 = 10 + reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) + frontRunningProtection = true + ) + + cases := []struct { + name string + malleate func() + expectedNumberProposalTxs int + expectedMempoolDistribution map[string]int + }{ + { + "single valid tob transaction in the mempool", + func() { + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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) + + normalTxs = []sdk.Tx{} + auctionTxs = []sdk.Tx{bidTx} + winningBidTx = bidTx + insertBundledTxs = false + }, + 2, + map[string]int{ + base.LaneName: 0, + auction.LaneName: 1, + }, + }, + { + "single invalid tob transaction in the mempool", + func() { + bidder := suite.accounts[0] + bid := reserveFee.Sub(sdk.NewCoin("foo", sdk.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) + + normalTxs = []sdk.Tx{} + auctionTxs = []sdk.Tx{bidTx} + winningBidTx = nil + insertBundledTxs = false + }, + 0, + map[string]int{ + base.LaneName: 0, + auction.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) + + normalTxs = []sdk.Tx{normalTx} + auctionTxs = []sdk.Tx{} + winningBidTx = nil + insertBundledTxs = false + }, + 1, + map[string]int{ + base.LaneName: 1, + auction.LaneName: 0, + }, + }, + { + "normal transactions and tob transactions in the mempool", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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 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) + + normalTxs = []sdk.Tx{normalTx} + auctionTxs = []sdk.Tx{bidTx} + winningBidTx = bidTx + insertBundledTxs = false + }, + 3, + map[string]int{ + base.LaneName: 1, + auction.LaneName: 1, + }, + }, + { + "multiple tob transactions where the first is invalid", + func() { + // Create an invalid tob transaction (frontrunning) + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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) + + // Create a valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.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) + + normalTxs = []sdk.Tx{} + auctionTxs = []sdk.Tx{bidTx, bidTx2} + winningBidTx = bidTx2 + insertBundledTxs = false + }, + 2, + map[string]int{ + base.LaneName: 0, + auction.LaneName: 1, + }, + }, + { + "multiple tob transactions where the first is valid", + func() { + // Create an valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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("foo", sdk.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) + + normalTxs = []sdk.Tx{} + auctionTxs = []sdk.Tx{bidTx, bidTx2} + winningBidTx = bidTx + insertBundledTxs = false + }, + 3, + map[string]int{ + base.LaneName: 0, + auction.LaneName: 2, + }, + }, + { + "multiple tob transactions where the first is valid and bundle is inserted into mempool", + func() { + frontRunningProtection = false + + // Create an valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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) + + normalTxs = []sdk.Tx{} + auctionTxs = []sdk.Tx{bidTx} + winningBidTx = bidTx + insertBundledTxs = true + }, + 6, + map[string]int{ + base.LaneName: 5, + auction.LaneName: 1, + }, + }, + { + "single tob transaction with other normal transactions in the mempool", + func() { + // Create an valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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) + + account := suite.accounts[5] + 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) + + normalTxs = []sdk.Tx{normalTx} + auctionTxs = []sdk.Tx{bidTx} + winningBidTx = bidTx + insertBundledTxs = true + }, + 7, + map[string]int{ + base.LaneName: 6, + auction.LaneName: 1, + }, + }, + } + + for _, tc := range cases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset + tc.malleate() + + // Insert all of the normal transactions into the default lane + for _, tx := range normalTxs { + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) + } + + // Insert all of the auction transactions into the TOB lane + for _, tx := range auctionTxs { + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) + } + + // 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, + } + suite.builderKeeper.SetParams(suite.ctx, params) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) + + suite.proposalHandler = abci.NewProposalHandler( + []blockbuster.Lane{suite.baseLane}, + suite.tobLane, + suite.logger, + suite.encodingConfig.TxConfig.TxEncoder(), + suite.encodingConfig.TxConfig.TxDecoder(), + ) + handler := suite.proposalHandler.PrepareProposalHandler() + req := suite.createPrepareProposalRequest(maxTxBytes) + res := handler(suite.ctx, req) + + // -------------------- Check Invariants -------------------- // + // The first slot in the proposal must be the auction info + auctionInfo := abci.AuctionInfo{} + err := auctionInfo.Unmarshal(res.Txs[abci.AuctionInfoIndex]) + suite.Require().NoError(err) + + // Total bytes must be less than or equal to maxTxBytes + totalBytes := int64(0) + for _, tx := range res.Txs[abci.NumInjectedTxs:] { + totalBytes += int64(len(tx)) + } + suite.Require().LessOrEqual(totalBytes, maxTxBytes) + + // 2. the number of transactions in the response must be equal to the number of expected transactions + // NOTE: We add 1 to the expected number of transactions because the first transaction in the response + // is the auction transaction + suite.Require().Equal(tc.expectedNumberProposalTxs+1, len(res.Txs)) + + // 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[1]) + 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+abci.NumInjectedTxs]) + } + } else if len(res.Txs) > 1 { + tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1]) + 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()) + }) + } +} + +func (suite *ABCITestSuite) TestProcessProposal() { + var ( + // auction configuration + maxBundleSize uint32 = 10 + reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) + frontRunningProtection = true + + // mempool configuration + proposal [][]byte + ) + + params := buildertypes.Params{ + MaxBundleSize: maxBundleSize, + ReserveFee: reserveFee, + FrontRunningProtection: frontRunningProtection, + } + suite.builderKeeper.SetParams(suite.ctx, params) + + cases := []struct { + name string + createTxs func() + response comettypes.ResponseProcessProposal_ProposalStatus + }{ + { + "no transactions in mempool with no vote extension info", + func() { + proposal = nil + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "no transactions in mempool with empty vote extension info", + func() { + proposal = [][]byte{} + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single normal tx, no vote extension info", + func() { + account := suite.accounts[0] + nonce := suite.nonces[account.Address.String()] + timeout := uint64(100) + numberMsgs := uint64(3) + normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) + suite.Require().NoError(err) + + proposal = [][]byte{normalTxBz} + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single auction tx, single auction tx, no vote extension info", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.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.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) + suite.Require().NoError(err) + + proposal = [][]byte{bidTx, normalTx} + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single auction tx with ref txs (no unwrapping)", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.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.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTx}, 2) + + proposal = [][]byte{ + auctionInfo, + bidTx, + normalTx, + } + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single auction tx with ref txs (with unwrapping)", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + }, + comettypes.ResponseProcessProposal_ACCEPT, + }, + { + "single auction tx with ref txs but misplaced in proposal", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[1], bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = [][]byte{ + auctionInfo, + bidTxBz, + bidInfo.Transactions[1], + bidInfo.Transactions[0], + } + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single auction tx, but auction tx is not valid", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder, suite.accounts[1]} // front-running + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "multiple auction txs but wrong auction tx is at top of block", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder, bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create another valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 3) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "multiple auction txs and correct auction tx is selected", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder, bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create another valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz2, + }, + bidInfo.Transactions..., + ) + }, + comettypes.ResponseProcessProposal_ACCEPT, + }, + { + "multiple auction txs included in block", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder, bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create another valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2) + bidInfo2 := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz2, + }, + bidInfo.Transactions..., + ) + + proposal = append(proposal, bidTxBz) + proposal = append(proposal, bidInfo2.Transactions...) + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "single auction tx, but rest of the mempool is invalid", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + + proposal = append(proposal, []byte("invalid tx")) + }, + comettypes.ResponseProcessProposal_REJECT, + }, + { + "multiple auction txs with ref txs + normal transactions", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + + normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[1], nonce, 3, timeout) + suite.Require().NoError(err) + proposal = append(proposal, normalTxBz) + + normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[2], nonce, 3, timeout) + suite.Require().NoError(err) + proposal = append(proposal, normalTxBz) + }, + comettypes.ResponseProcessProposal_ACCEPT, + }, + { + "front-running protection disabled", + func() { + // Create a valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.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]} + bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, uint64(len(signers)+1)) + + bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz) + + proposal = append( + [][]byte{ + auctionInfo, + bidTxBz, + }, + bidInfo.Transactions..., + ) + + normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[5], nonce, 3, timeout) + suite.Require().NoError(err) + proposal = append(proposal, normalTxBz) + + normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[6], nonce, 3, timeout) + suite.Require().NoError(err) + proposal = append(proposal, normalTxBz) + + // disable frontrunning protection + params := buildertypes.Params{ + MaxBundleSize: maxBundleSize, + ReserveFee: reserveFee, + FrontRunningProtection: false, + } + suite.builderKeeper.SetParams(suite.ctx, params) + }, + comettypes.ResponseProcessProposal_ACCEPT, + }, + } + + for _, tc := range cases { + suite.Run(tc.name, func() { + // suite.SetupTest() // reset + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) + + // reset the proposal handler with the new mempool + suite.proposalHandler = abci.NewProposalHandler( + []blockbuster.Lane{suite.baseLane}, + suite.tobLane, log.NewNopLogger(), + suite.encodingConfig.TxConfig.TxEncoder(), + suite.encodingConfig.TxConfig.TxDecoder(), + ) + + tc.createTxs() + + handler := suite.proposalHandler.ProcessProposalHandler() + res := handler(suite.ctx, comettypes.RequestProcessProposal{ + Txs: proposal, + }) + + // Check if the response is valid + suite.Require().Equal(tc.response, res.Status) + }) + } +} diff --git a/abci/v2/types.go b/abci/types.go similarity index 99% rename from abci/v2/types.go rename to abci/types.go index 169e71a..2b8961e 100644 --- a/abci/v2/types.go +++ b/abci/types.go @@ -4,7 +4,7 @@ alpha/RC tag is released. These types are simply used to prototype and develop against. */ //nolint -package v2 +package abci import ( sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/abci/v2/abci_test.go b/abci/v2/abci_test.go deleted file mode 100644 index 9fa5a10..0000000 --- a/abci/v2/abci_test.go +++ /dev/null @@ -1,336 +0,0 @@ -package v2_test - -import ( - "math/rand" - "testing" - "time" - - comettypes "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/log" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - "github.com/cosmos/cosmos-sdk/testutil" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" - "github.com/skip-mev/pob/abci" - v2 "github.com/skip-mev/pob/abci/v2" - "github.com/skip-mev/pob/mempool" - 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" -) - -type ABCITestSuite struct { - suite.Suite - ctx sdk.Context - - // mempool setup - mempool *mempool.AuctionMempool - logger log.Logger - encodingConfig testutils.EncodingConfig - proposalHandler *v2.ProposalHandler - voteExtensionHandler *v2.VoteExtensionHandler - config mempool.AuctionFactory - txs map[string]struct{} - - // auction bid setup - auctionBidAmount sdk.Coin - minBidIncrement sdk.Coin - - // builder setup - builderKeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper - builderDecorator ante.BuilderDecorator - key *storetypes.KVStoreKey - authorityAccount sdk.AccAddress - - // account set up - accounts []testutils.Account - balances sdk.Coins - random *rand.Rand - nonces map[string]uint64 -} - -func TestABCISuite(t *testing.T) { - suite.Run(t, new(ABCITestSuite)) -} - -func (suite *ABCITestSuite) SetupTest() { - // General config - suite.encodingConfig = testutils.CreateTestEncodingConfig() - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx.WithBlockHeight(1) - - // Mempool set up - suite.config = mempool.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()) - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) - suite.txs = make(map[string]struct{}) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(1000000000)) - suite.minBidIncrement = sdk.NewCoin("foo", sdk.NewInt(1000)) - - // 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) - suite.authorityAccount = sdk.AccAddress([]byte("authority")) - - // Builder keeper / decorator set up - suite.builderKeeper = keeper.NewKeeper( - suite.encodingConfig.Codec, - suite.key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - suite.authorityAccount.String(), - ) - err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) - suite.Require().NoError(err) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) - - // Accounts set up - suite.accounts = testutils.RandomAccounts(suite.random, 10) - suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000))) - suite.nonces = make(map[string]uint64) - for _, acc := range suite.accounts { - suite.nonces[acc.Address.String()] = 0 - } - - // Proposal handler set up - suite.logger = log.NewNopLogger() - suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) - suite.voteExtensionHandler = v2.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler) -} - -func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { - signer := tx.GetMsgs()[0].GetSigners()[0] - suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances) - - next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { - return ctx, nil - } - - ctx, err := suite.builderDecorator.AnteHandle(ctx, tx, false, next) - if err != nil { - return ctx, err - } - - return ctx, nil -} - -func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int { - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) - - // Insert a bunch of normal transactions into the global mempool - for i := 0; i < numNormalTxs; i++ { - // randomly select an account to create the tx - randomIndex := suite.random.Intn(len(suite.accounts)) - acc := suite.accounts[randomIndex] - - // create a few random msgs - randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - - nonce := suite.nonces[acc.Address.String()] - randomTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) - suite.Require().NoError(err) - - suite.nonces[acc.Address.String()]++ - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), randomTx)) - } - - suite.Require().Equal(numNormalTxs, suite.mempool.CountTx()) - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) - - // Insert a bunch of auction transactions into the global mempool and auction mempool - for i := 0; i < numAuctionTxs; i++ { - // randomly select a bidder to create the tx - randomIndex := suite.random.Intn(len(suite.accounts)) - acc := suite.accounts[randomIndex] - - // create a new auction bid msg with numBundledTxs bundled transactions - nonce := suite.nonces[acc.Address.String()] - bidMsg, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, acc, suite.auctionBidAmount, nonce, numBundledTxs) - suite.nonces[acc.Address.String()] += uint64(numBundledTxs) - suite.Require().NoError(err) - - // create the auction tx - nonce = suite.nonces[acc.Address.String()] - auctionTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg}) - suite.Require().NoError(err) - - // insert the auction tx into the global mempool - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx)) - suite.nonces[acc.Address.String()]++ - - if insertRefTxs { - for _, refRawTx := range bidMsg.GetTransactions() { - refTx, err := suite.encodingConfig.TxConfig.TxDecoder()(refRawTx) - suite.Require().NoError(err) - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx)) - } - } - - // decrement the bid amount for the next auction tx - suite.auctionBidAmount = suite.auctionBidAmount.Sub(suite.minBidIncrement) - } - - numSeenGlobalTxs := 0 - for iterator := suite.mempool.Select(suite.ctx, nil); iterator != nil; iterator = iterator.Next() { - numSeenGlobalTxs++ - } - - numSeenAuctionTxs := 0 - for iterator := suite.mempool.AuctionBidSelect(suite.ctx); iterator != nil; iterator = iterator.Next() { - numSeenAuctionTxs++ - } - - var totalNumTxs int - suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx()) - if insertRefTxs { - totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs) - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } else { - totalNumTxs = numNormalTxs - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } - - suite.Require().Equal(numAuctionTxs, numSeenAuctionTxs) - - return totalNumTxs -} - -func (suite *ABCITestSuite) exportMempool() [][]byte { - txs := make([][]byte, 0) - seenTxs := make(map[string]bool) - - iterator := suite.mempool.Select(suite.ctx, nil) - for ; iterator != nil; iterator = iterator.Next() { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx()) - suite.Require().NoError(err) - - if !seenTxs[string(txBz)] { - txs = append(txs, txBz) - } - } - - return txs -} - -func (suite *ABCITestSuite) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal { - voteExtensions := make([]comettypes.ExtendedVoteInfo, 0) - - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) - for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { - tx := auctionIterator.Tx() - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{ - VoteExtension: txBz, - }) - } - - return comettypes.RequestPrepareProposal{ - MaxTxBytes: maxBytes, - LocalLastCommit: comettypes.ExtendedCommitInfo{ - Votes: voteExtensions, - }, - } -} - -func (suite *ABCITestSuite) createExtendedCommitInfoFromTxBzs(txs [][]byte) []byte { - voteExtensions := make([]comettypes.ExtendedVoteInfo, 0) - - for _, txBz := range txs { - voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{ - VoteExtension: txBz, - }) - } - - commitInfo := comettypes.ExtendedCommitInfo{ - Votes: voteExtensions, - } - - commitInfoBz, err := commitInfo.Marshal() - suite.Require().NoError(err) - - return commitInfoBz -} - -func (suite *ABCITestSuite) createAuctionInfoFromTxBzs(txs [][]byte, numTxs uint64) []byte { - auctionInfo := abci.AuctionInfo{ - ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs), - NumTxs: numTxs, - MaxTxBytes: int64(len(txs[0])), - } - - auctionInfoBz, err := auctionInfo.Marshal() - suite.Require().NoError(err) - - return auctionInfoBz -} - -func (suite *ABCITestSuite) getAllAuctionTxs() ([]sdk.Tx, [][]byte) { - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) - txs := make([]sdk.Tx, 0) - txBzs := make([][]byte, 0) - - for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { - txs = append(txs, auctionIterator.Tx()) - - bz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionIterator.Tx()) - suite.Require().NoError(err) - - txBzs = append(txBzs, bz) - } - - return txs, txBzs -} - -func (suite *ABCITestSuite) createExtendedCommitInfoFromTxs(txs []sdk.Tx) comettypes.ExtendedCommitInfo { - voteExtensions := make([][]byte, 0) - for _, tx := range txs { - bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - voteExtensions = append(voteExtensions, bz) - } - - return suite.createExtendedCommitInfo(voteExtensions) -} - -func (suite *ABCITestSuite) createExtendedVoteInfo(voteExtensions [][]byte) []comettypes.ExtendedVoteInfo { - commitInfo := make([]comettypes.ExtendedVoteInfo, 0) - for _, voteExtension := range voteExtensions { - info := comettypes.ExtendedVoteInfo{ - VoteExtension: voteExtension, - } - - commitInfo = append(commitInfo, info) - } - - return commitInfo -} - -func (suite *ABCITestSuite) createExtendedCommitInfo(voteExtensions [][]byte) comettypes.ExtendedCommitInfo { - commitInfo := comettypes.ExtendedCommitInfo{ - Votes: suite.createExtendedVoteInfo(voteExtensions), - } - - return commitInfo -} diff --git a/abci/v2/proposals.go b/abci/v2/proposals.go deleted file mode 100644 index 012a628..0000000 --- a/abci/v2/proposals.go +++ /dev/null @@ -1,237 +0,0 @@ -package v2 - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - - abci "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/log" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" - pobabci "github.com/skip-mev/pob/abci" - mempool "github.com/skip-mev/pob/mempool" -) - -const ( - // NumInjectedTxs is the minimum number of transactions that were injected into - // the proposal but are not actual transactions. In this case, the auction - // info is injected into the proposal but should be ignored by the application.ß - NumInjectedTxs = 1 - - // AuctionInfoIndex is the index of the auction info in the proposal. - AuctionInfoIndex = 0 -) - -type ( - // ProposalMempool contains the methods required by the ProposalHandler - // to interact with the local mempool. - ProposalMempool interface { - sdkmempool.Mempool - - // The AuctionFactory interface is utilized to retrieve, validate, and wrap bid - // information into the block proposal. - mempool.AuctionFactory - - // AuctionBidSelect returns an iterator that iterates over the top bid - // transactions in the mempool. - AuctionBidSelect(ctx context.Context) sdkmempool.Iterator - } - - // ProposalHandler contains the functionality and handlers required to\ - // process, validate and build blocks. - ProposalHandler struct { - mempool ProposalMempool - logger log.Logger - anteHandler sdk.AnteHandler - txEncoder sdk.TxEncoder - txDecoder sdk.TxDecoder - } -) - -// NewProposalHandler returns a ProposalHandler that contains the functionality and handlers -// required to process, validate and build blocks. -func NewProposalHandler( - mp ProposalMempool, - logger log.Logger, - anteHandler sdk.AnteHandler, - txEncoder sdk.TxEncoder, - txDecoder sdk.TxDecoder, -) *ProposalHandler { - return &ProposalHandler{ - mempool: mp, - logger: logger, - anteHandler: anteHandler, - txEncoder: txEncoder, - txDecoder: txDecoder, - } -} - -// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs -// top-of-block auctioning and general block proposal construction. -func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { - return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal { - // Proposal includes all of the transactions that will be included in the - // block along with the vote extensions from the previous block included at - // the beginning of the proposal. Vote extensions must be included in the - // first slot of the proposal because they are inaccessible in ProcessProposal. - proposal := make([][]byte, 0) - - // Build the top of block portion of the proposal given the vote extensions - // from the previous block. - topOfBlock := h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes) - - // If information is unable to be marshaled, we return an empty proposal. This will - // cause another proposal to be generated after it is rejected in ProcessProposal. - lastCommitInfo, err := req.LocalLastCommit.Marshal() - if err != nil { - return abci.ResponsePrepareProposal{Txs: proposal} - } - - auctionInfo := pobabci.AuctionInfo{ - ExtendedCommitInfo: lastCommitInfo, - MaxTxBytes: req.MaxTxBytes, - NumTxs: uint64(len(topOfBlock.Txs)), - } - - // Add the auction info and top of block transactions into the proposal. - auctionInfoBz, err := auctionInfo.Marshal() - if err != nil { - return abci.ResponsePrepareProposal{Txs: proposal} - } - - proposal = append(proposal, auctionInfoBz) - proposal = append(proposal, topOfBlock.Txs...) - - // Select remaining transactions for the block proposal until we've reached - // size capacity. - totalTxBytes := topOfBlock.Size - txsToRemove := make(map[sdk.Tx]struct{}, 0) - for iterator := h.mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { - memTx := iterator.Tx() - - // If the transaction has already been seen in the top of block, skip it. - txBz, err := h.txEncoder(memTx) - if err != nil { - txsToRemove[memTx] = struct{}{} - continue - } - - hashBz := sha256.Sum256(txBz) - hash := hex.EncodeToString(hashBz[:]) - if _, ok := topOfBlock.Cache[hash]; ok { - continue - } - - // Verify that the transaction is valid. - txBz, err = h.PrepareProposalVerifyTx(ctx, memTx) - if err != nil { - txsToRemove[memTx] = struct{}{} - continue - } - - txSize := int64(len(txBz)) - if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes { - proposal = append(proposal, txBz) - } else { - // We've reached capacity per req.MaxTxBytes so we cannot select any - // more transactions. - break - } - } - - // Remove all invalid transactions from the mempool. - for tx := range txsToRemove { - h.RemoveTx(tx) - } - - return abci.ResponsePrepareProposal{Txs: proposal} - } -} - -// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs -// block proposal verification. -func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { - return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal { - proposal := req.Txs - - // Verify that the same top of block transactions can be built from the vote - // extensions included in the proposal. - auctionInfo, err := h.VerifyTOB(ctx, proposal) - if err != nil { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - // Track the transactions that need to be removed from the mempool. - txsToRemove := make(map[sdk.Tx]struct{}, 0) - invalidProposal := false - - // Verify that the remaining transactions in the proposal are valid. - for _, txBz := range proposal[auctionInfo.NumTxs+NumInjectedTxs:] { - tx, err := h.ProcessProposalVerifyTx(ctx, txBz) - if tx == nil || err != nil { - invalidProposal = true - if tx != nil { - txsToRemove[tx] = struct{}{} - } - - continue - } - - // The only auction transactions that should be included in the block proposal - // must be at the top of the block. - if bidInfo, err := h.mempool.GetAuctionBidInfo(tx); err != nil || bidInfo != nil { - invalidProposal = true - } - } - // Remove all invalid transactions from the mempool. - for tx := range txsToRemove { - h.RemoveTx(tx) - } - - if invalidProposal { - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} - } - - return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT} - } -} - -// PrepareProposalVerifyTx encodes a transaction and verifies it. -func (h *ProposalHandler) PrepareProposalVerifyTx(ctx sdk.Context, tx sdk.Tx) ([]byte, error) { - txBz, err := h.txEncoder(tx) - if err != nil { - return nil, err - } - - return txBz, h.verifyTx(ctx, tx) -} - -// ProcessProposalVerifyTx decodes a transaction and verifies it. -func (h *ProposalHandler) ProcessProposalVerifyTx(ctx sdk.Context, txBz []byte) (sdk.Tx, error) { - tx, err := h.txDecoder(txBz) - if err != nil { - return nil, err - } - - return tx, h.verifyTx(ctx, tx) -} - -// RemoveTx removes a transaction from the application-side mempool. -func (h *ProposalHandler) RemoveTx(tx sdk.Tx) { - if err := h.mempool.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) - } -} - -// VerifyTx verifies a transaction against the application's state. -func (h *ProposalHandler) verifyTx(ctx sdk.Context, tx sdk.Tx) error { - if h.anteHandler != nil { - _, err := h.anteHandler(ctx, tx, false) - return err - } - - return nil -} diff --git a/abci/v2/proposals_test.go b/abci/v2/proposals_test.go deleted file mode 100644 index 541f51c..0000000 --- a/abci/v2/proposals_test.go +++ /dev/null @@ -1,766 +0,0 @@ -package v2_test - -import ( - comettypes "github.com/cometbft/cometbft/abci/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/pob/abci" - v2 "github.com/skip-mev/pob/abci/v2" - testutils "github.com/skip-mev/pob/testutils" - "github.com/skip-mev/pob/x/builder/ante" - buildertypes "github.com/skip-mev/pob/x/builder/types" -) - -func (suite *ABCITestSuite) TestPrepareProposal() { - var ( - // the modified transactions cannot exceed this size - maxTxBytes int64 = 1000000000000000000 - - // mempool configuration - numNormalTxs = 100 - numAuctionTxs = 100 - numBundledTxs = 3 - insertRefTxs = false - expectedTopAuctionTx sdk.Tx - - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - expectedNumberProposalTxs int - expectedNumberTxsInMempool int - expectedNumberTxsInAuctionMempool int - }{ - { - "single bundle in the mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 5, - 3, - 1, - }, - { - "single bundle in the mempool, no ref txs in mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 5, - 0, - 1, - }, - { - "single bundle in the mempool, not valid", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = nil - }, - 1, - 0, - 0, - }, - { - "single bundle in the mempool, not valid with ref txs in mempool", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = nil - }, - 4, - 3, - 0, - }, - { - "multiple bundles in the mempool, no normal txs + no ref txs in mempool", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000000)) - numNormalTxs = 0 - numAuctionTxs = 10 - numBundledTxs = 3 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 5, - 0, - 10, - }, - { - "multiple bundles in the mempool, normal txs + ref txs in mempool", - func() { - numNormalTxs = 0 - numAuctionTxs = 10 - numBundledTxs = 3 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 32, - 30, - 10, - }, - { - "normal txs only", - func() { - numNormalTxs = 1 - numAuctionTxs = 0 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 2, - 1, - 0, - }, - { - "many normal txs only", - func() { - numNormalTxs = 100 - numAuctionTxs = 0 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 101, - 100, - 0, - }, - { - "single normal tx, single auction tx", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 3, - 1, - 1, - }, - { - "single normal tx, single auction tx with ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 6, - 4, - 1, - }, - { - "single normal tx, single failing auction tx with ref txs", - func() { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = true - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000)) // this will fail the ante handler - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000000000)) - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = nil - }, - 5, - 4, - 0, - }, - { - "many normal tx, single auction tx with no ref txs", - func() { - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000000)) - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = nil - }, - 102, - 100, - 1, - }, - { - "many normal tx, single auction tx with ref txs", - func() { - numNormalTxs = 100 - numAuctionTxs = 100 - numBundledTxs = 3 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - }, - 402, - 400, - 100, - }, - { - "many normal tx, many auction tx with ref txs but top bid is invalid", - func() { - numNormalTxs = 100 - numAuctionTxs = 100 - numBundledTxs = 1 - insertRefTxs = true - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx) - - // create a new bid that is greater than the current top bid - bid := sdk.NewCoin("foo", sdk.NewInt(200000000000000000)) - bidTx, err := testutils.CreateAuctionTxWithSigners( - suite.encodingConfig.TxConfig, - suite.accounts[0], - bid, - 0, - 0, - []testutils.Account{suite.accounts[0], suite.accounts[1]}, - ) - suite.Require().NoError(err) - - // add the new bid to the mempool - err = suite.mempool.Insert(suite.ctx, bidTx) - suite.Require().NoError(err) - - suite.Require().Equal(suite.mempool.CountAuctionTx(), 101) - }, - 202, - 200, - 100, - }, - } - - for _, tc := range cases { - suite.Run(tc.name, func() { - tc.malleate() - - // Create a new auction. - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: suite.minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) - - // Reset the proposal handler with the new mempool. - suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) - - // Create a prepare proposal request based on the current state of the mempool. - handler := suite.proposalHandler.PrepareProposalHandler() - req := suite.createPrepareProposalRequest(maxTxBytes) - res := handler(suite.ctx, req) - - // -------------------- Check Invariants -------------------- // - // The first slot in the proposal must be the auction info - auctionInfo := abci.AuctionInfo{} - err := auctionInfo.Unmarshal(res.Txs[v2.AuctionInfoIndex]) - suite.Require().NoError(err) - - // Total bytes must be less than or equal to maxTxBytes - totalBytes := int64(0) - for _, tx := range res.Txs[v2.NumInjectedTxs:] { - totalBytes += int64(len(tx)) - } - suite.Require().LessOrEqual(totalBytes, maxTxBytes) - - // The number of transactions in the response must be equal to the number of expected transactions - suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs)) - - // 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 expectedTopAuctionTx != nil { - auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1]) - suite.Require().NoError(err) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTx) - suite.Require().NoError(err) - - for index, tx := range bidInfo.Transactions { - suite.Require().Equal(tx, res.Txs[v2.NumInjectedTxs+index+1]) - } - } - - // 5. All of the transactions must be unique - uniqueTxs := make(map[string]bool) - for _, tx := range res.Txs[v2.NumInjectedTxs:] { - suite.Require().False(uniqueTxs[string(tx)]) - uniqueTxs[string(tx)] = true - } - - // 6. The number of transactions in the mempool must be correct - suite.Require().Equal(tc.expectedNumberTxsInMempool, suite.mempool.CountTx()) - suite.Require().Equal(tc.expectedNumberTxsInAuctionMempool, suite.mempool.CountAuctionTx()) - }) - } -} - -func (suite *ABCITestSuite) TestProcessProposal() { - var ( - // mempool set up - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 3 - insertRefTxs = false - - // auction set up - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) - ) - - cases := []struct { - name string - createTxs func() [][]byte - response comettypes.ResponseProcessProposal_ProposalStatus - }{ - { - "single normal tx, no vote extension info", - func() [][]byte { - numNormalTxs = 1 - numAuctionTxs = 0 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - txs := suite.exportMempool() - - return txs - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx, no vote extension info", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - return suite.exportMempool() - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx, single auction tx, no vote extension info", - func() [][]byte { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 0 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - return suite.exportMempool() - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx with ref txs (no unwrapping)", - func() [][]byte { - numNormalTxs = 1 - numAuctionTxs = 1 - numBundledTxs = 4 - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx) - suite.Require().NotNil(topAuctionTx) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5) - - proposal := append([][]byte{ - auctionInfo, - txBz, - }, suite.exportMempool()...) - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx with ref txs (with unwrapping)", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 4 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx) - suite.Require().NotNil(topAuctionTx) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx) - suite.Require().NoError(err) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5) - - proposal := append([][]byte{ - auctionInfo, - txBz, - }, bidInfo.Transactions...) - - return proposal - }, - comettypes.ResponseProcessProposal_ACCEPT, - }, - { - "single auction tx but no inclusion of ref txs", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 4 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx) - suite.Require().NotNil(topAuctionTx) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5) - - return [][]byte{ - auctionInfo, - txBz, - } - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx, but auction tx is not valid", - func() [][]byte { - tx, err := testutils.CreateAuctionTxWithSigners( - suite.encodingConfig.TxConfig, - suite.accounts[0], - sdk.NewCoin("foo", sdk.NewInt(100)), - 1, - 0, // invalid timeout - []testutils.Account{}, - ) - suite.Require().NoError(err) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1) - - return [][]byte{ - auctionInfoBz, - txBz, - } - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx with ref txs, but auction tx is not valid", - func() [][]byte { - tx, err := testutils.CreateAuctionTxWithSigners( - suite.encodingConfig.TxConfig, - suite.accounts[0], - sdk.NewCoin("foo", sdk.NewInt(100)), - 1, - 1, - []testutils.Account{suite.accounts[1], suite.accounts[1], suite.accounts[0]}, - ) - suite.Require().NoError(err) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 4) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - return append([][]byte{ - auctionInfoBz, - txBz, - }, bidInfo.Transactions...) - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "multiple auction txs but wrong auction tx is at top of block", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 2 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - _, auctionTxBzs := suite.getAllAuctionTxs() - - auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1) - - proposal := [][]byte{ - auctionInfo, - auctionTxBzs[1], - } - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "multiple auction txs included in block", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 2 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - _, auctionTxBzs := suite.getAllAuctionTxs() - - auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1) - - proposal := [][]byte{ - auctionInfo, - auctionTxBzs[0], - auctionTxBzs[1], - } - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx, but rest of the mempool is invalid", - func() [][]byte { - numNormalTxs = 0 - numAuctionTxs = 1 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx) - suite.Require().NotNil(topAuctionTx) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1) - - proposal := [][]byte{ - auctionInfo, - txBz, - []byte("invalid tx"), - } - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "single auction tx with filled mempool, but rest of the mempool is invalid", - func() [][]byte { - numNormalTxs = 100 - numAuctionTxs = 1 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx) - suite.Require().NotNil(topAuctionTx) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1) - - proposal := append([][]byte{ - auctionInfo, - txBz, - }, suite.exportMempool()...) - - proposal = append(proposal, []byte("invalid tx")) - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - { - "multiple auction txs with filled mempool", - func() [][]byte { - numNormalTxs = 100 - numAuctionTxs = 10 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - _, auctionTxBzs := suite.getAllAuctionTxs() - - auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1) - - proposal := append([][]byte{ - auctionInfo, - auctionTxBzs[0], - }, suite.exportMempool()...) - - return proposal - }, - comettypes.ResponseProcessProposal_ACCEPT, - }, - { - "multiple auction txs with ref txs + filled mempool", - func() [][]byte { - numNormalTxs = 100 - numAuctionTxs = 10 - numBundledTxs = 10 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - auctionTxs, auctionTxBzs := suite.getAllAuctionTxs() - - auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 11) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTxs[0]) - suite.Require().NoError(err) - - proposal := append([][]byte{ - auctionInfo, - auctionTxBzs[0], - }, bidInfo.Transactions...) - - proposal = append(proposal, suite.exportMempool()...) - - return proposal - }, - comettypes.ResponseProcessProposal_ACCEPT, - }, - { - "auction tx with front-running", - func() [][]byte { - numNormalTxs = 100 - numAuctionTxs = 0 - numBundledTxs = 0 - insertRefTxs = false - - suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) - - topAuctionTx, err := testutils.CreateAuctionTxWithSigners( - suite.encodingConfig.TxConfig, - suite.accounts[0], - sdk.NewCoin("foo", sdk.NewInt(1000000)), - 0, - 1, - []testutils.Account{suite.accounts[0], suite.accounts[1]}, // front-running - ) - suite.Require().NoError(err) - - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx) - suite.Require().NoError(err) - - bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx) - suite.Require().NoError(err) - - auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 3) - - proposal := append([][]byte{ - auctionInfo, - txBz, - }, bidInfo.Transactions...) - - proposal = append(proposal, suite.exportMempool()...) - - return proposal - }, - comettypes.ResponseProcessProposal_REJECT, - }, - } - - for _, tc := range cases { - suite.Run(tc.name, func() { - // create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: true, - MinBidIncrement: suite.minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) - - // reset the proposal handler with the new mempool - suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) - - handler := suite.proposalHandler.ProcessProposalHandler() - res := handler(suite.ctx, comettypes.RequestProcessProposal{ - Txs: tc.createTxs(), - }) - - // Check if the response is valid - suite.Require().Equal(tc.response, res.Status) - }) - } -} diff --git a/abci/v2/vote_extensions.go b/abci/vote_extensions.go similarity index 67% rename from abci/v2/vote_extensions.go rename to abci/vote_extensions.go index 832ac24..4107a49 100644 --- a/abci/v2/vote_extensions.go +++ b/abci/vote_extensions.go @@ -1,30 +1,34 @@ -package v2 +package abci import ( - "context" "crypto/sha256" "encoding/hex" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/blockbuster/lanes/auction" ) type ( - // VoteExtensionMempool contains the methods required by the VoteExtensionHandler - // to interact with the local mempool. - VoteExtensionMempool interface { - Remove(tx sdk.Tx) error - AuctionBidSelect(ctx context.Context) sdkmempool.Iterator - GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error) - WrapBundleTransaction(tx []byte) (sdk.Tx, error) + // TOBLaneVE contains the methods required by the VoteExtensionHandler + // to interact with the local mempool i.e. the top of block lane. + TOBLaneVE interface { + sdkmempool.Mempool + + // 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.). + auction.Factory + + // VerifyTx is utilized to verify a bid transaction according to the preferences + // of the top of block lane. + VerifyTx(ctx sdk.Context, tx sdk.Tx) error } // VoteExtensionHandler contains the functionality and handlers required to // process, validate and build vote extensions. VoteExtensionHandler struct { - mempool VoteExtensionMempool + tobLane TOBLaneVE // txDecoder is used to decode the top bidding auction transaction txDecoder sdk.TxDecoder @@ -32,9 +36,6 @@ type ( // txEncoder is used to encode the top bidding auction transaction txEncoder sdk.TxEncoder - // anteHandler is used to validate the vote extension - anteHandler sdk.AnteHandler - // cache is used to store the results of the vote extension verification // for a given block height. cache map[string]error @@ -46,14 +47,11 @@ type ( // NewVoteExtensionHandler returns an VoteExtensionHandler that contains the functionality and handlers // required to inject, process, and validate vote extensions. -func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder, - txEncoder sdk.TxEncoder, ah sdk.AnteHandler, -) *VoteExtensionHandler { +func NewVoteExtensionHandler(lane TOBLaneVE, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder) *VoteExtensionHandler { return &VoteExtensionHandler{ - mempool: mp, + tobLane: lane, txDecoder: txDecoder, txEncoder: txEncoder, - anteHandler: ah, cache: make(map[string]error), currentHeight: 0, } @@ -65,15 +63,17 @@ func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder, func (h *VoteExtensionHandler) ExtendVoteHandler() ExtendVoteHandler { return func(ctx sdk.Context, req *RequestExtendVote) (*ResponseExtendVote, error) { // Iterate through auction bids until we find a valid one - auctionIterator := h.mempool.AuctionBidSelect(ctx) + auctionIterator := h.tobLane.Select(ctx, nil) for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { bidTx := auctionIterator.Tx() // Verify the bid tx can be encoded and included in vote extension if bidBz, err := h.txEncoder(bidTx); err == nil { - // Validate the auction transaction - if err := h.verifyAuctionTx(ctx, bidTx); err == nil { + // Validate the auction transaction against a cache state + cacheCtx, _ := ctx.CacheContext() + + if err := h.tobLane.VerifyTx(cacheCtx, bidTx); err == nil { return &ResponseExtendVote{VoteExtension: bidBz}, nil } } @@ -116,7 +116,7 @@ func (h *VoteExtensionHandler) VerifyVoteExtensionHandler() VerifyVoteExtensionH } // Verify the auction transaction and cache the result - if err = h.verifyAuctionTx(ctx, bidTx); err != nil { + if err = h.tobLane.VerifyTx(ctx, bidTx); err != nil { h.cache[hash] = err return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_REJECT}, err } @@ -136,40 +136,3 @@ func (h *VoteExtensionHandler) resetCache(blockHeight int64) { h.currentHeight = blockHeight } } - -// verifyAuctionTx verifies a transaction against the application's state. -func (h *VoteExtensionHandler) verifyAuctionTx(ctx sdk.Context, bidTx sdk.Tx) error { - // Verify the vote extension is a auction transaction - bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx) - if err != nil { - return err - } - - if bidInfo == nil { - return fmt.Errorf("vote extension is not a valid auction transaction") - } - - if h.anteHandler == nil { - return nil - } - - // Cache context is used to avoid state changes - cache, _ := ctx.CacheContext() - if _, err := h.anteHandler(cache, bidTx, false); err != nil { - return err - } - - // Verify all bundled transactions - for _, tx := range bidInfo.Transactions { - wrappedTx, err := h.mempool.WrapBundleTransaction(tx) - if err != nil { - return err - } - - if _, err := h.anteHandler(cache, wrappedTx, false); err != nil { - return err - } - } - - return nil -} diff --git a/abci/v2/vote_extensions_test.go b/abci/vote_extensions_test.go similarity index 72% rename from abci/v2/vote_extensions_test.go rename to abci/vote_extensions_test.go index 87f9f04..9aa47aa 100644 --- a/abci/v2/vote_extensions_test.go +++ b/abci/vote_extensions_test.go @@ -1,9 +1,8 @@ -package v2_test +package abci_test import ( sdk "github.com/cosmos/cosmos-sdk/types" - v2 "github.com/skip-mev/pob/abci/v2" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/abci" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/types" ) @@ -13,12 +12,8 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { MaxBundleSize: 5, ReserveFee: sdk.NewCoin("foo", sdk.NewInt(10)), FrontRunningProtection: true, - MinBidIncrement: suite.minBidIncrement, } - err := suite.builderKeeper.SetParams(suite.ctx, params) - suite.Require().NoError(err) - testCases := []struct { name string getExpectedVE func() []byte @@ -26,21 +21,20 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { { "empty mempool", func() []byte { - suite.createFilledMempool(0, 0, 0, false) return []byte{} }, }, { "filled mempool with no auction transactions", func() []byte { - suite.createFilledMempool(100, 0, 0, false) + suite.fillBaseLane(10) return []byte{} }, }, { "mempool with invalid auction transaction (too many bundled transactions)", func() []byte { - suite.createFilledMempool(0, 1, int(params.MaxBundleSize)+1, true) + suite.fillTOBLane(3, int(params.MaxBundleSize)+1) return []byte{} }, }, @@ -55,9 +49,7 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) suite.Require().NoError(err) - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) - err = suite.mempool.Insert(suite.ctx, bidTx) - suite.Require().NoError(err) + suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx)) // this should return nothing since the top bid is not valid return []byte{} @@ -66,14 +58,12 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { { "mempool contains only invalid auction bids (bid is too low)", func() []byte { - params.ReserveFee = suite.auctionBidAmount + params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10000000000000000)) err := suite.builderKeeper.SetParams(suite.ctx, params) suite.Require().NoError(err) // this way all of the bids will be too small - suite.auctionBidAmount = params.ReserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1))) - - suite.createFilledMempool(100, 100, 2, true) + suite.fillTOBLane(4, 1) return []byte{} }, @@ -88,10 +78,7 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) suite.Require().NoError(err) - - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) - err = suite.mempool.Insert(suite.ctx, bidTx) - suite.Require().NoError(err) + suite.Require().NoError(suite.tobLane.Insert(suite.ctx, bidTx)) // this should return nothing since the top bid is not valid return []byte{} @@ -100,26 +87,26 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { { "top bid is invalid but next best is valid", func() []byte { - params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(100)) - err := suite.builderKeeper.SetParams(suite.ctx, params) - suite.Require().NoError(err) + params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10)) bidder := suite.accounts[0] - bid := suite.auctionBidAmount.Add(suite.minBidIncrement) + bid := params.ReserveFee.Add(params.ReserveFee) signers := []testutils.Account{bidder} timeout := 0 bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) suite.Require().NoError(err) + suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx)) - suite.createFilledMempool(100, 100, 2, true) - - topBidTx := suite.mempool.GetTopAuctionTx(suite.ctx) - - err = suite.mempool.Insert(suite.ctx, bidTx) + bidder = suite.accounts[1] + bid = params.ReserveFee + signers = []testutils.Account{bidder} + timeout = 100 + bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) suite.Require().NoError(err) + suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx2)) - bz, err := suite.encodingConfig.TxConfig.TxEncoder()(topBidTx) + bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx2) suite.Require().NoError(err) return bz @@ -129,10 +116,14 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { for _, tc := range testCases { suite.Run(tc.name, func() { + suite.SetupTest() // reset expectedVE := tc.getExpectedVE() + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + // Reset the handler with the new mempool - suite.voteExtensionHandler = v2.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler) + suite.voteExtensionHandler = abci.NewVoteExtensionHandler(suite.tobLane, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder()) handler := suite.voteExtensionHandler.ExtendVoteHandler() resp, err := handler(suite.ctx, nil) @@ -148,7 +139,6 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { MaxBundleSize: 5, ReserveFee: sdk.NewCoin("foo", sdk.NewInt(100)), FrontRunningProtection: true, - MinBidIncrement: sdk.NewCoin("foo", sdk.NewInt(10)), // can't be tested atm } err := suite.builderKeeper.SetParams(suite.ctx, params) @@ -156,13 +146,13 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { testCases := []struct { name string - req func() *v2.RequestVerifyVoteExtension + req func() *abci.RequestVerifyVoteExtension expectedErr bool }{ { "invalid vote extension bytes", - func() *v2.RequestVerifyVoteExtension { - return &v2.RequestVerifyVoteExtension{ + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ VoteExtension: []byte("invalid vote extension"), } }, @@ -170,8 +160,8 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "empty vote extension bytes", - func() *v2.RequestVerifyVoteExtension { - return &v2.RequestVerifyVoteExtension{ + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ VoteExtension: []byte{}, } }, @@ -179,8 +169,8 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "nil vote extension bytes", - func() *v2.RequestVerifyVoteExtension { - return &v2.RequestVerifyVoteExtension{ + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ VoteExtension: nil, } }, @@ -188,14 +178,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "invalid extension with bid tx with bad timeout", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := sdk.NewCoin("foo", sdk.NewInt(10)) signers := []testutils.Account{bidder} timeout := 0 bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -203,14 +193,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "invalid vote extension with bid tx with bad bid", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := sdk.NewCoin("foo", sdk.NewInt(0)) signers := []testutils.Account{bidder} timeout := 10 bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -218,14 +208,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "valid vote extension", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := params.ReserveFee signers := []testutils.Account{bidder} timeout := 10 bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -233,7 +223,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "invalid vote extension with front running bid tx", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := params.ReserveFee timeout := 10 @@ -242,7 +232,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { signers := []testutils.Account{bidder, bundlee} bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -250,7 +240,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "invalid vote extension with too many bundle txs", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { // disable front running protection params.FrontRunningProtection = false err := suite.builderKeeper.SetParams(suite.ctx, params) @@ -262,7 +252,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { timeout := 10 bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -270,7 +260,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "invalid vote extension with a failing bundle tx", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := params.ReserveFee @@ -286,7 +276,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) suite.Require().NoError(err) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, @@ -294,7 +284,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { }, { "valid vote extension + no comparison to local mempool", - func() *v2.RequestVerifyVoteExtension { + func() *abci.RequestVerifyVoteExtension { bidder := suite.accounts[0] bid := params.ReserveFee signers := []testutils.Account{bidder} @@ -303,17 +293,17 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) // Add a bid to the mempool that is greater than the one in the vote extension - bid = bid.Add(params.MinBidIncrement) + bid = bid.Add(params.ReserveFee) bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 10, 1, signers) suite.Require().NoError(err) err = suite.mempool.Insert(suite.ctx, bidTx) suite.Require().NoError(err) - tx := suite.mempool.GetTopAuctionTx(suite.ctx) + tx := suite.tobLane.GetTopAuctionTx(suite.ctx) suite.Require().NotNil(tx) - return &v2.RequestVerifyVoteExtension{ + return &abci.RequestVerifyVoteExtension{ VoteExtension: bz, } }, diff --git a/blockbuster/abci/abci.go b/blockbuster/abci/abci.go index 232796a..532ecf0 100644 --- a/blockbuster/abci/abci.go +++ b/blockbuster/abci/abci.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/blockbuster/lanes/terminator" + "github.com/skip-mev/pob/blockbuster/utils" ) type ( @@ -33,10 +34,22 @@ func NewProposalHandler(logger log.Logger, mempool blockbuster.Mempool) *Proposa // the default lane will not have a boundary on the number of bytes that can be included in the proposal and // will include all valid transactions in the proposal (up to MaxTxBytes). func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { - return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal { + return func(ctx sdk.Context, req abci.RequestPrepareProposal) (resp abci.ResponsePrepareProposal) { + // In the case where there is a panic, we recover here and return an empty proposal. + defer func() { + if err := recover(); err != nil { + h.logger.Error("failed to prepare proposal", "err", err) + resp = abci.ResponsePrepareProposal{Txs: make([][]byte, 0)} + } + }() + proposal := h.prepareLanesHandler(ctx, blockbuster.NewProposal(req.MaxTxBytes)) - return abci.ResponsePrepareProposal{Txs: proposal.Txs} + resp = abci.ResponsePrepareProposal{ + Txs: proposal.Txs, + } + + return } } @@ -45,7 +58,16 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { // If a lane's portion of the proposal is invalid, we reject the proposal. After a lane's portion // of the proposal is verified, we pass the remaining transactions to the next lane in the chain. func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { - return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal { + return func(ctx sdk.Context, req abci.RequestProcessProposal) (resp abci.ResponseProcessProposal) { + // In the case where any of the lanes panic, we recover here and return a reject status. + defer func() { + if err := recover(); err != nil { + h.logger.Error("failed to process proposal", "err", err) + resp = abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} + } + }() + + // Verify the proposal using the verification logic from each lane. if _, err := h.processLanesHandler(ctx, req.Txs); err != nil { h.logger.Error("failed to validate the proposal", "err", err) return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} @@ -58,6 +80,9 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { // ChainPrepareLanes chains together the proposal preparation logic from each lane // into a single function. The first lane in the chain is the first lane to be prepared and // the last lane in the chain is the last lane to be prepared. +// +// In the case where any of the lanes fail to prepare the partial proposal, the lane that failed +// will be skipped and the next lane in the chain will be called to prepare the proposal. func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandler { if len(chain) == 0 { return nil @@ -68,8 +93,62 @@ func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandle chain = append(chain, terminator.Terminator{}) } - return func(ctx sdk.Context, proposal *blockbuster.Proposal) *blockbuster.Proposal { - return chain[0].PrepareLane(ctx, proposal, ChainPrepareLanes(chain[1:]...)) + return func(ctx sdk.Context, partialProposal *blockbuster.Proposal) (finalProposal *blockbuster.Proposal) { + lane := chain[0] + lane.Logger().Info("preparing lane", "lane", lane.Name()) + + // Cache the context in the case where any of the lanes fail to prepare the proposal. + cacheCtx, write := ctx.CacheContext() + + defer func() { + if err := recover(); err != nil { + lane.Logger().Error("failed to prepare lane", "lane", lane.Name(), "err", err) + + lanesRemaining := len(chain) + switch { + case lanesRemaining <= 2: + // If there are only two lanes remaining, then the first lane in the chain + // is the lane that failed to prepare the partial proposal and the second lane in the + // chain is the terminator lane. We return the proposal as is. + finalProposal = partialProposal + default: + // If there are more than two lanes remaining, then the first lane in the chain + // is the lane that failed to prepare the proposal but the second lane in the + // chain is not the terminator lane so there could potentially be more transactions + // added to the proposal + maxTxBytesForLane := utils.GetMaxTxBytesForLane( + partialProposal, + chain[1].GetMaxBlockSpace(), + ) + + finalProposal = chain[1].PrepareLane( + ctx, + partialProposal, + maxTxBytesForLane, + ChainPrepareLanes(chain[2:]...), + ) + } + } else { + // Write the cache to the context since we know that the lane successfully prepared + // the partial proposal. + write() + + lane.Logger().Info("prepared lane", "lane", lane.Name()) + } + }() + + // Get the maximum number of bytes that can be included in the proposal for this lane. + maxTxBytesForLane := utils.GetMaxTxBytesForLane( + partialProposal, + lane.GetMaxBlockSpace(), + ) + + return lane.PrepareLane( + cacheCtx, + partialProposal, + maxTxBytesForLane, + ChainPrepareLanes(chain[1:]...), + ) } } @@ -87,6 +166,16 @@ func ChainProcessLanes(chain ...blockbuster.Lane) blockbuster.ProcessLanesHandle } return func(ctx sdk.Context, proposalTxs [][]byte) (sdk.Context, error) { + // Short circuit if there are no transactions to process. + if len(proposalTxs) == 0 { + return ctx, nil + } + + chain[0].Logger().Info("processing lane", "lane", chain[0].Name()) + if err := chain[0].ProcessLaneBasic(proposalTxs); err != nil { + return ctx, err + } + return chain[0].ProcessLane(ctx, proposalTxs, ChainProcessLanes(chain[1:]...)) } } diff --git a/abci/check_tx.go b/blockbuster/abci/check_tx.go similarity index 88% rename from abci/check_tx.go rename to blockbuster/abci/check_tx.go index 57f1d47..1ab4e2e 100644 --- a/abci/check_tx.go +++ b/blockbuster/abci/check_tx.go @@ -9,7 +9,7 @@ import ( tmproto "github.com/cometbft/cometbft/proto/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/x/builder/types" ) type ( @@ -26,9 +26,9 @@ type ( // bid transactions. txDecoder sdk.TxDecoder - // mempool is utilized to retrieve the bid info of a transaction and to - // insert a transaction into the application-side mempool. - mempool CheckTxMempool + // TOBLane is utilized to retrieve the bid info of a transaction and to + // insert a bid transaction into the application-side mempool. + tobLane TOBLane // anteHandler is utilized to verify the bid transaction against the latest // committed state. @@ -42,11 +42,11 @@ type ( // transaction. CheckTx func(cometabci.RequestCheckTx) cometabci.ResponseCheckTx - // CheckTxMempool is the interface that defines all of the dependencies that - // are required to interact with the application-side mempool. - CheckTxMempool interface { + // TOBLane is the interface that defines all of the dependencies that + // are required to interact with the top of block lane. + TOBLane interface { // GetAuctionBidInfo is utilized to retrieve the bid info of a transaction. - GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error) + GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) // Insert is utilized to insert a transaction into the application-side mempool. Insert(ctx context.Context, tx sdk.Tx) error @@ -78,11 +78,17 @@ type ( ) // NewCheckTxHandler is a constructor for CheckTxHandler. -func NewCheckTxHandler(baseApp BaseApp, txDecoder sdk.TxDecoder, mempool CheckTxMempool, anteHandler sdk.AnteHandler, chainID string) *CheckTxHandler { +func NewCheckTxHandler( + baseApp BaseApp, + txDecoder sdk.TxDecoder, + tobLane TOBLane, + anteHandler sdk.AnteHandler, + chainID string, +) *CheckTxHandler { return &CheckTxHandler{ baseApp: baseApp, txDecoder: txDecoder, - mempool: mempool, + tobLane: tobLane, anteHandler: anteHandler, chainID: chainID, } @@ -108,7 +114,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx { } // Attempt to get the bid info of the transaction. - bidInfo, err := handler.mempool.GetAuctionBidInfo(tx) + bidInfo, err := handler.tobLane.GetAuctionBidInfo(tx) if err != nil { return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to get auction bid info: %w", err), 0, 0, nil, false) } @@ -130,7 +136,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx { } // If the bid transaction is valid, we know we can insert it into the mempool for consideration in the next block. - if err := handler.mempool.Insert(ctx, tx); err != nil { + if err := handler.tobLane.Insert(ctx, tx); err != nil { return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("invalid bid tx; failed to insert bid transaction into mempool: %w", err), gasInfo.GasWanted, gasInfo.GasUsed, nil, false) } @@ -143,7 +149,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx { } // ValidateBidTx is utilized to verify the bid transaction against the latest committed state. -func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *mempool.AuctionBidInfo) (sdk.GasInfo, error) { +func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *types.BidInfo) (sdk.GasInfo, error) { // Verify the bid transaction. ctx, err := handler.anteHandler(ctx, bidTx, false) if err != nil { @@ -158,13 +164,13 @@ func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidI // Verify all of the bundled transactions. for _, tx := range bidInfo.Transactions { - bundledTx, err := handler.mempool.WrapBundleTransaction(tx) + bundledTx, err := handler.tobLane.WrapBundleTransaction(tx) if err != nil { return gasInfo, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) } // bid txs cannot be included in bundled txs - bidInfo, _ := handler.mempool.GetAuctionBidInfo(bundledTx) + bidInfo, _ := handler.tobLane.GetAuctionBidInfo(bundledTx) if bidInfo != nil { return gasInfo, fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx") } diff --git a/blockbuster/lane.go b/blockbuster/lane.go index 8394b2e..f1d2216 100644 --- a/blockbuster/lane.go +++ b/blockbuster/lane.go @@ -3,6 +3,7 @@ package blockbuster import ( "crypto/sha256" "encoding/hex" + "fmt" "github.com/cometbft/cometbft/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" @@ -15,8 +16,8 @@ type ( // Txs is the list of transactions in the proposal. Txs [][]byte - // SelectedTxs is a cache of the selected transactions in the proposal. - SelectedTxs map[string]struct{} + // Cache is a cache of the selected transactions in the proposal. + Cache map[string]struct{} // TotalTxBytes is the total number of bytes currently included in the proposal. TotalTxBytes int64 @@ -65,14 +66,29 @@ type ( // Contains returns true if the mempool contains the given transaction. Contains(tx sdk.Tx) (bool, error) - // PrepareLane which builds a portion of the block. Inputs include the max - // number of bytes that can be included in the block and the selected transactions - // thus from from previous lane(s) as mapping from their HEX-encoded hash to - // the raw transaction. - PrepareLane(ctx sdk.Context, proposal *Proposal, next PrepareLanesHandler) *Proposal + // PrepareLane builds a portion of the block. It inputs the maxTxBytes that can be + // included in the proposal for the given lane, the partial proposal, and a function + // to call the next lane in the chain. The next lane in the chain will be called with + // the updated proposal and context. + PrepareLane(ctx sdk.Context, proposal *Proposal, maxTxBytes int64, next PrepareLanesHandler) *Proposal - // ProcessLane verifies this lane's portion of a proposed block. + // ProcessLaneBasic validates that transactions belonging to this lane are not misplaced + // in the block proposal. + ProcessLaneBasic(txs [][]byte) error + + // ProcessLane verifies this lane's portion of a proposed block. It inputs the transactions + // that may belong to this lane and a function to call the next lane in the chain. The next + // lane in the chain will be called with the updated context and filtered down transactions. ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next ProcessLanesHandler) (sdk.Context, error) + + // SetAnteHandler sets the lane's antehandler. + SetAnteHandler(antehander sdk.AnteHandler) + + // Logger returns the lane's logger. + Logger() log.Logger + + // GetMaxBlockSpace returns the max block space for the lane as a relative percentage. + GetMaxBlockSpace() sdk.Dec } ) @@ -87,11 +103,33 @@ func NewBaseLaneConfig(logger log.Logger, txEncoder sdk.TxEncoder, txDecoder sdk } } +// ValidateBasic validates the lane configuration. +func (c *BaseLaneConfig) ValidateBasic() error { + if c.Logger == nil { + return fmt.Errorf("logger cannot be nil") + } + + if c.TxEncoder == nil { + return fmt.Errorf("tx encoder cannot be nil") + } + + if c.TxDecoder == nil { + return fmt.Errorf("tx decoder cannot be nil") + } + + if c.MaxBlockSpace.IsNil() || c.MaxBlockSpace.IsNegative() || c.MaxBlockSpace.GT(sdk.OneDec()) { + return fmt.Errorf("max block space must be set to a value between 0 and 1") + } + + return nil +} + +// NewProposal returns a new empty proposal. func NewProposal(maxTxBytes int64) *Proposal { return &Proposal{ - Txs: make([][]byte, 0), - SelectedTxs: make(map[string]struct{}), - MaxTxBytes: maxTxBytes, + Txs: make([][]byte, 0), + Cache: make(map[string]struct{}), + MaxTxBytes: maxTxBytes, } } @@ -104,7 +142,7 @@ func (p *Proposal) UpdateProposal(txs [][]byte, totalSize int64) *Proposal { txHash := sha256.Sum256(tx) txHashStr := hex.EncodeToString(txHash[:]) - p.SelectedTxs[txHashStr] = struct{}{} + p.Cache[txHashStr] = struct{}{} } return p diff --git a/blockbuster/lanes/auction/abci.go b/blockbuster/lanes/auction/abci.go index 6e4f3de..80eb200 100644 --- a/blockbuster/lanes/auction/abci.go +++ b/blockbuster/lanes/auction/abci.go @@ -2,17 +2,22 @@ package auction import ( "bytes" - "errors" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/utils" ) // PrepareLane 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.Proposal, next blockbuster.PrepareLanesHandler) *blockbuster.Proposal { +func (l *TOBLane) PrepareLane( + ctx sdk.Context, + proposal *blockbuster.Proposal, + maxTxBytes int64, + next blockbuster.PrepareLanesHandler, +) *blockbuster.Proposal { // Define all of the info we need to select transactions for the partial proposal. var ( totalSize int64 @@ -20,10 +25,6 @@ func (l *TOBLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposal, n txsToRemove = make(map[sdk.Tx]struct{}, 0) ) - // Calculate the max tx bytes for the lane and track the total size of the - // transactions we have selected so far. - maxTxBytes := blockbuster.GetMaxTxBytesForLane(proposal, l.cfg.MaxBlockSpace) - // Attempt to select the highest bid transaction that is valid and whose // bundled transactions are valid. bidTxIterator := l.Select(ctx, nil) @@ -32,19 +33,14 @@ selectBidTxLoop: cacheCtx, write := ctx.CacheContext() tmpBidTx := bidTxIterator.Tx() - // if the transaction is already in the (partial) block proposal, we skip it. - txHash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, tmpBidTx) + bidTxBz, txHash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tmpBidTx) if err != nil { txsToRemove[tmpBidTx] = struct{}{} continue } - if _, ok := proposal.SelectedTxs[txHash]; ok { - continue selectBidTxLoop - } - bidTxBz, err := l.cfg.TxEncoder(tmpBidTx) - if err != nil { - txsToRemove[tmpBidTx] = struct{}{} + // if the transaction is already in the (partial) block proposal, we skip it. + if _, ok := proposal.Cache[txHash]; ok { continue selectBidTxLoop } @@ -75,19 +71,14 @@ selectBidTxLoop: continue selectBidTxLoop } - sdkTxBz, err := l.cfg.TxEncoder(sdkTx) + sdkTxBz, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, sdkTx) if err != nil { txsToRemove[tmpBidTx] = struct{}{} continue selectBidTxLoop } // if the transaction is already in the (partial) block proposal, we skip it. - hash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, sdkTx) - if err != nil { - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - if _, ok := proposal.SelectedTxs[hash]; ok { + if _, ok := proposal.Cache[hash]; ok { continue selectBidTxLoop } @@ -112,7 +103,7 @@ selectBidTxLoop: } txsToRemove[tmpBidTx] = struct{}{} - l.cfg.Logger.Info( + l.Cfg.Logger.Info( "failed to select auction bid tx; tx size is too large", "tx_size", bidTxSize, "max_size", proposal.MaxTxBytes, @@ -120,8 +111,8 @@ selectBidTxLoop: } // Remove all transactions that were invalid during the creation of the partial proposal. - if err := blockbuster.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { - l.cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err) + if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { + l.Cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err) return proposal } @@ -132,68 +123,99 @@ selectBidTxLoop: } // ProcessLane will ensure that block proposals that include transactions from -// the top-of-block auction lane are valid. It will return an error if the -// block proposal is invalid. The block proposal is invalid if it does not -// respect the ordering of transactions in the bid transaction or if the bid/bundled -// transactions are invalid. +// the top-of-block auction lane are valid. func (l *TOBLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next blockbuster.ProcessLanesHandler) (sdk.Context, error) { - // Track the index of the first transaction that does not belong to this lane. - endIndex := 0 + tx, err := l.Cfg.TxDecoder(proposalTxs[0]) + if err != nil { + return ctx, fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err) + } - for index, txBz := range proposalTxs { - tx, err := l.cfg.TxDecoder(txBz) + if !l.Match(tx) { + return next(ctx, proposalTxs) + } + + bidInfo, err := l.GetAuctionBidInfo(tx) + if err != nil { + return ctx, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + } + + if err := l.VerifyTx(ctx, tx); err != nil { + return ctx, fmt.Errorf("invalid bid tx: %w", err) + } + + return next(ctx, proposalTxs[len(bidInfo.Transactions)+1:]) +} + +// ProcessLaneBasic 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(txs [][]byte) error { + tx, err := l.Cfg.TxDecoder(txs[0]) + if err != nil { + return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err) + } + + // If there is a bid transaction, it must be the first transaction in the block proposal. + if !l.Match(tx) { + for _, txBz := range txs[1:] { + tx, err := l.Cfg.TxDecoder(txBz) + if err != nil { + return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err) + } + + if l.Match(tx) { + return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) + } + } + + return nil + } + + bidInfo, err := l.GetAuctionBidInfo(tx) + 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, bundleTxBz := range txs[1 : len(bidInfo.Transactions)+1] { + tx, err := l.WrapBundleTransaction(bundleTxBz) if err != nil { - return ctx, err + return fmt.Errorf("failed to decode bundled tx in lane %s: %w", l.Name(), err) } if l.Match(tx) { - // If the transaction is an auction bid, then we need to ensure that it is - // the first transaction in the block proposal and that the order of - // transactions in the block proposal follows the order of transactions in - // the bid. - if index != 0 { - return ctx, fmt.Errorf("block proposal did not place auction bid transaction at the top of the lane: %d", index) - } + return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) + } - bidInfo, err := l.GetAuctionBidInfo(tx) - if err != nil { - return ctx, fmt.Errorf("failed to get auction bid info for tx at index %w", err) - } + txBz, err := l.Cfg.TxEncoder(tx) + if err != nil { + return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) + } - if bidInfo != nil { - if len(proposalTxs) < len(bidInfo.Transactions)+1 { - return ctx, errors.New("block proposal does not contain enough transactions to match the bundled transactions in the auction bid") - } - - for i, refTxRaw := range bidInfo.Transactions { - // Wrap and then encode the bundled transaction to ensure that the underlying - // reference transaction can be processed as an sdk.Tx. - wrappedTx, err := l.WrapBundleTransaction(refTxRaw) - if err != nil { - return ctx, err - } - - refTxBz, err := l.cfg.TxEncoder(wrappedTx) - if err != nil { - return ctx, err - } - - if !bytes.Equal(refTxBz, proposalTxs[i+1]) { - return ctx, errors.New("block proposal does not match the bundled transactions in the auction bid") - } - } - - // Verify the bid transaction. - if err = l.VerifyTx(ctx, tx); err != nil { - return ctx, err - } - - endIndex = len(bidInfo.Transactions) + 1 - } + if !bytes.Equal(txBz, bidInfo.Transactions[i]) { + return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) } } - return next(ctx, proposalTxs[endIndex:]) + // Ensure that there are no more bid transactions in the block proposal. + for _, txBz := range txs[len(bidInfo.Transactions)+1:] { + tx, err := l.Cfg.TxDecoder(txBz) + if err != nil { + return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err) + } + + if l.Match(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 @@ -235,8 +257,8 @@ func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error { // 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) + if l.Cfg.AnteHandler != nil { + newCtx, err := l.Cfg.AnteHandler(ctx, tx, false) return newCtx, err } diff --git a/blockbuster/lanes/auction/auction_test.go b/blockbuster/lanes/auction/auction_test.go new file mode 100644 index 0000000..afa7277 --- /dev/null +++ b/blockbuster/lanes/auction/auction_test.go @@ -0,0 +1,47 @@ +package auction_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster/lanes/auction" + testutils "github.com/skip-mev/pob/testutils" + "github.com/stretchr/testify/suite" +) + +type IntegrationTestSuite struct { + suite.Suite + + encCfg testutils.EncodingConfig + config auction.Factory + mempool auction.Mempool + ctx sdk.Context + random *rand.Rand + accounts []testutils.Account + nonces map[string]uint64 +} + +func TestMempoolTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} + +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.NewNopLogger()) + + // Init accounts + suite.random = rand.New(rand.NewSource(time.Now().Unix())) + suite.accounts = testutils.RandomAccounts(suite.random, 10) + + suite.nonces = make(map[string]uint64) + for _, acc := range suite.accounts { + suite.nonces[acc.Address.String()] = 0 + } +} diff --git a/blockbuster/lanes/auction/factory.go b/blockbuster/lanes/auction/factory.go index 77cc13f..f49f566 100644 --- a/blockbuster/lanes/auction/factory.go +++ b/blockbuster/lanes/auction/factory.go @@ -5,18 +5,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/skip-mev/pob/x/builder/types" ) type ( - // BidInfo defines the information about a bid to the auction house. - BidInfo struct { - Bidder sdk.AccAddress - Bid sdk.Coin - Transactions [][]byte - Timeout uint64 - Signers []map[string]struct{} - } - // Factory defines the interface for processing auction transactions. It is // a wrapper around all of the functionality that each application chain must implement // in order for auction processing to work. @@ -28,7 +20,7 @@ type ( WrapBundleTransaction(tx []byte) (sdk.Tx, error) // GetAuctionBidInfo defines a function that returns the bid info from an auction transaction. - GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, error) + GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) } // DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions. @@ -65,7 +57,7 @@ func (config *DefaultAuctionFactory) WrapBundleTransaction(tx []byte) (sdk.Tx, e // GetAuctionBidInfo defines a default function that returns the auction bid info from // an auction transaction. In the default case, the auction bid info is stored in the // MsgAuctionBid message. -func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, error) { +func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) { msg, err := GetMsgAuctionBidFromTx(tx) if err != nil { return nil, err @@ -90,7 +82,7 @@ func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, err return nil, err } - return &BidInfo{ + return &types.BidInfo{ Bid: msg.Bid, Bidder: bidder, Transactions: msg.Transactions, diff --git a/mempool/auction_bid_test.go b/blockbuster/lanes/auction/factory_test.go similarity index 99% rename from mempool/auction_bid_test.go rename to blockbuster/lanes/auction/factory_test.go index 314663b..dfc5bd0 100644 --- a/mempool/auction_bid_test.go +++ b/blockbuster/lanes/auction/factory_test.go @@ -1,4 +1,4 @@ -package mempool_test +package auction_test import ( "crypto/rand" diff --git a/blockbuster/lanes/auction/lane.go b/blockbuster/lanes/auction/lane.go index de3a65b..40e8c5b 100644 --- a/blockbuster/lanes/auction/lane.go +++ b/blockbuster/lanes/auction/lane.go @@ -1,17 +1,20 @@ package auction import ( - "github.com/cometbft/cometbft/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/base" ) const ( // LaneName defines the name of the top-of-block auction lane. - LaneName = "tob" + LaneName = "top-of-block" ) -var _ blockbuster.Lane = (*TOBLane)(nil) +var ( + _ blockbuster.Lane = (*TOBLane)(nil) + _ Factory = (*TOBLane)(nil) +) // TOBLane defines a top-of-block auction lane. The top of block auction lane // hosts transactions that want to bid for inclusion at the top of the next block. @@ -24,7 +27,7 @@ type TOBLane struct { Mempool // LaneConfig defines the base lane configuration. - cfg blockbuster.BaseLaneConfig + *base.DefaultLane // Factory defines the API/functionality which is responsible for determining // if a transaction is a bid transaction and how to extract relevant @@ -34,18 +37,18 @@ type TOBLane struct { // NewTOBLane returns a new TOB lane. func NewTOBLane( - logger log.Logger, - txDecoder sdk.TxDecoder, - txEncoder sdk.TxEncoder, + cfg blockbuster.BaseLaneConfig, maxTx int, - anteHandler sdk.AnteHandler, af Factory, - maxBlockSpace sdk.Dec, ) *TOBLane { + if err := cfg.ValidateBasic(); err != nil { + panic(err) + } + return &TOBLane{ - Mempool: NewMempool(txEncoder, maxTx, af), - cfg: blockbuster.NewBaseLaneConfig(logger, txEncoder, txDecoder, anteHandler, maxBlockSpace), - Factory: af, + Mempool: NewMempool(cfg.TxEncoder, maxTx, af), + DefaultLane: base.NewDefaultLane(cfg), + Factory: af, } } diff --git a/blockbuster/lanes/auction/mempool.go b/blockbuster/lanes/auction/mempool.go index 948e939..2a9529f 100644 --- a/blockbuster/lanes/auction/mempool.go +++ b/blockbuster/lanes/auction/mempool.go @@ -8,7 +8,7 @@ import ( 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/mempool" + "github.com/skip-mev/pob/blockbuster/utils" ) var _ Mempool = (*TOBMempool)(nil) @@ -48,8 +48,8 @@ type ( // TxPriority returns a TxPriority over auction bid transactions only. It // is to be used in the auction index only. -func TxPriority(config Factory) mempool.TxPriority[string] { - return mempool.TxPriority[string]{ +func TxPriority(config Factory) blockbuster.TxPriority[string] { + return blockbuster.TxPriority[string]{ GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { bidInfo, err := config.GetAuctionBidInfo(tx) if err != nil { @@ -92,8 +92,8 @@ func TxPriority(config Factory) mempool.TxPriority[string] { // NewMempool returns a new auction mempool. func NewMempool(txEncoder sdk.TxEncoder, maxTx int, config Factory) *TOBMempool { return &TOBMempool{ - index: mempool.NewPriorityMempool( - mempool.PriorityNonceMempoolConfig[string]{ + index: blockbuster.NewPriorityMempool( + blockbuster.PriorityNonceMempoolConfig[string]{ TxPriority: TxPriority(config), MaxTx: maxTx, }, @@ -120,7 +120,7 @@ func (am *TOBMempool) Insert(ctx context.Context, tx sdk.Tx) error { return fmt.Errorf("failed to insert tx into auction index: %w", err) } - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { return err } @@ -167,7 +167,7 @@ func (am *TOBMempool) CountTx() int { // Contains returns true if the transaction is contained in the mempool. func (am *TOBMempool) Contains(tx sdk.Tx) (bool, error) { - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { return false, fmt.Errorf("failed to get tx hash string: %w", err) } @@ -181,7 +181,7 @@ func (am *TOBMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) { panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) } - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { panic(fmt.Errorf("failed to get tx hash string: %w", err)) } diff --git a/mempool/utils_test.go b/blockbuster/lanes/auction/utils_test.go similarity index 79% rename from mempool/utils_test.go rename to blockbuster/lanes/auction/utils_test.go index c2903aa..28b7eba 100644 --- a/mempool/utils_test.go +++ b/blockbuster/lanes/auction/utils_test.go @@ -1,11 +1,11 @@ -package mempool_test +package auction_test import ( "testing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/skip-mev/pob/blockbuster/lanes/auction" pobcodec "github.com/skip-mev/pob/codec" - "github.com/skip-mev/pob/mempool" buildertypes "github.com/skip-mev/pob/x/builder/types" "github.com/stretchr/testify/require" ) @@ -16,7 +16,7 @@ func TestGetMsgAuctionBidFromTx_Valid(t *testing.T) { txBuilder := encCfg.TxConfig.NewTxBuilder() txBuilder.SetMsgs(&buildertypes.MsgAuctionBid{}) - msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx()) + msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx()) require.NoError(t, err) require.NotNil(t, msg) } @@ -31,7 +31,7 @@ func TestGetMsgAuctionBidFromTx_MultiMsgBid(t *testing.T) { &banktypes.MsgSend{}, ) - msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx()) + msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx()) require.Error(t, err) require.Nil(t, msg) } @@ -42,7 +42,7 @@ func TestGetMsgAuctionBidFromTx_NoBid(t *testing.T) { txBuilder := encCfg.TxConfig.NewTxBuilder() txBuilder.SetMsgs(&banktypes.MsgSend{}) - msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx()) + msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx()) require.NoError(t, err) require.Nil(t, msg) } diff --git a/blockbuster/lanes/base/abci.go b/blockbuster/lanes/base/abci.go index 173d749..515fc76 100644 --- a/blockbuster/lanes/base/abci.go +++ b/blockbuster/lanes/base/abci.go @@ -5,37 +5,36 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/utils" ) // PrepareLane will prepare a partial proposal for the base lane. -func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposal, next blockbuster.PrepareLanesHandler) *blockbuster.Proposal { +func (l *DefaultLane) PrepareLane( + ctx sdk.Context, + proposal *blockbuster.Proposal, + maxTxBytes int64, + next blockbuster.PrepareLanesHandler, +) *blockbuster.Proposal { // Define all of the info we need to select transactions for the partial proposal. - txs := make([][]byte, 0) - txsToRemove := make(map[sdk.Tx]struct{}, 0) - totalSize := int64(0) - - // Calculate the max tx bytes for the lane and track the total size of the - // transactions we have selected so far. - maxTxBytes := blockbuster.GetMaxTxBytesForLane(proposal, l.cfg.MaxBlockSpace) + var ( + totalSize int64 + txs [][]byte + txsToRemove = make(map[sdk.Tx]struct{}, 0) + ) // Select all transactions in the mempool that are valid and not already in the // partial proposal. for iterator := l.Mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { tx := iterator.Tx() - txBytes, err := l.cfg.TxEncoder(tx) + txBytes, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tx) if err != nil { txsToRemove[tx] = struct{}{} continue } // if the transaction is already in the (partial) block proposal, we skip it. - hash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, tx) - if err != nil { - txsToRemove[tx] = struct{}{} - continue - } - if _, ok := proposal.SelectedTxs[hash]; ok { + if _, ok := proposal.Cache[hash]; ok { continue } @@ -56,8 +55,8 @@ func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposa } // Remove all transactions that were invalid during the creation of the partial proposal. - if err := blockbuster.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { - l.cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err) + if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { + l.Cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err) return proposal } @@ -69,7 +68,7 @@ func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposa // ProcessLane verifies the default lane's portion of a block proposal. func (l *DefaultLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next blockbuster.ProcessLanesHandler) (sdk.Context, error) { for index, tx := range proposalTxs { - tx, err := l.cfg.TxDecoder(tx) + tx, err := l.Cfg.TxDecoder(tx) if err != nil { return ctx, fmt.Errorf("failed to decode tx: %w", err) } @@ -87,10 +86,37 @@ func (l *DefaultLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next bl return ctx, nil } +// ProcessLaneBasic does basic validation on the block proposal to ensure that +// transactions that belong to this lane are not misplaced in the block proposal. +func (l *DefaultLane) ProcessLaneBasic(txs [][]byte) error { + seenOtherLaneTx := false + lastSeenIndex := 0 + + for _, txBz := range txs { + tx, err := l.Cfg.TxDecoder(txBz) + if err != nil { + return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err) + } + + if l.Match(tx) { + if seenOtherLaneTx { + return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name()) + } + + lastSeenIndex++ + continue + } + + seenOtherLaneTx = true + } + + return nil +} + // VerifyTx does basic verification of the transaction using the ante handler. func (l *DefaultLane) VerifyTx(ctx sdk.Context, tx sdk.Tx) error { - if l.cfg.AnteHandler != nil { - _, err := l.cfg.AnteHandler(ctx, tx, false) + if l.Cfg.AnteHandler != nil { + _, err := l.Cfg.AnteHandler(ctx, tx, false) return err } diff --git a/blockbuster/lanes/base/lane.go b/blockbuster/lanes/base/lane.go index 18ae85d..10c2463 100644 --- a/blockbuster/lanes/base/lane.go +++ b/blockbuster/lanes/base/lane.go @@ -20,13 +20,18 @@ type DefaultLane struct { Mempool // LaneConfig defines the base lane configuration. - cfg blockbuster.BaseLaneConfig + Cfg blockbuster.BaseLaneConfig } -func NewDefaultLane(logger log.Logger, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, anteHandler sdk.AnteHandler, maxBlockSpace sdk.Dec) *DefaultLane { +// NewDefaultLane returns a new default lane. +func NewDefaultLane(cfg blockbuster.BaseLaneConfig) *DefaultLane { + if err := cfg.ValidateBasic(); err != nil { + panic(err) + } + return &DefaultLane{ - Mempool: NewDefaultMempool(txEncoder), - cfg: blockbuster.NewBaseLaneConfig(logger, txEncoder, txDecoder, anteHandler, maxBlockSpace), + Mempool: NewDefaultMempool(cfg.TxEncoder), + Cfg: cfg, } } @@ -41,3 +46,18 @@ func (l *DefaultLane) Match(sdk.Tx) bool { func (l *DefaultLane) Name() string { return LaneName } + +// Logger returns the lane's logger. +func (l *DefaultLane) Logger() log.Logger { + return l.Cfg.Logger +} + +// SetAnteHandler sets the lane's antehandler. +func (l *DefaultLane) SetAnteHandler(anteHandler sdk.AnteHandler) { + l.Cfg.AnteHandler = anteHandler +} + +// GetMaxBlockSpace returns the maximum block space for the lane as a relative percentage. +func (l *DefaultLane) GetMaxBlockSpace() sdk.Dec { + return l.Cfg.MaxBlockSpace +} diff --git a/blockbuster/lanes/base/mempool.go b/blockbuster/lanes/base/mempool.go index 7147978..d491f4e 100644 --- a/blockbuster/lanes/base/mempool.go +++ b/blockbuster/lanes/base/mempool.go @@ -8,7 +8,7 @@ import ( 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/mempool" + "github.com/skip-mev/pob/blockbuster/utils" ) var _ sdkmempool.Mempool = (*DefaultMempool)(nil) @@ -42,8 +42,8 @@ type ( func NewDefaultMempool(txEncoder sdk.TxEncoder) *DefaultMempool { return &DefaultMempool{ - index: mempool.NewPriorityMempool( - mempool.DefaultPriorityNonceMempoolConfig(), + index: blockbuster.NewPriorityMempool( + blockbuster.DefaultPriorityNonceMempoolConfig(), ), txEncoder: txEncoder, txIndex: make(map[string]struct{}), @@ -56,7 +56,7 @@ func (am *DefaultMempool) Insert(ctx context.Context, tx sdk.Tx) error { return fmt.Errorf("failed to insert tx into auction index: %w", err) } - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { return err } @@ -82,7 +82,7 @@ func (am *DefaultMempool) CountTx() int { // Contains returns true if the transaction is contained in the mempool. func (am *DefaultMempool) Contains(tx sdk.Tx) (bool, error) { - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { return false, fmt.Errorf("failed to get tx hash string: %w", err) } @@ -97,7 +97,7 @@ func (am *DefaultMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) { panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) } - txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx) + _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) if err != nil { panic(fmt.Errorf("failed to get tx hash string: %w", err)) } diff --git a/blockbuster/lanes/terminator/lane.go b/blockbuster/lanes/terminator/lane.go index 00d8460..972fc9c 100644 --- a/blockbuster/lanes/terminator/lane.go +++ b/blockbuster/lanes/terminator/lane.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/cometbft/cometbft/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/pob/blockbuster" @@ -34,7 +35,7 @@ type Terminator struct{} var _ blockbuster.Lane = (*Terminator)(nil) // PrepareLane is a no-op -func (t Terminator) PrepareLane(_ sdk.Context, proposal *blockbuster.Proposal, _ blockbuster.PrepareLanesHandler) *blockbuster.Proposal { +func (t Terminator) PrepareLane(_ sdk.Context, proposal *blockbuster.Proposal, _ int64, _ blockbuster.PrepareLanesHandler) *blockbuster.Proposal { return proposal } @@ -82,3 +83,21 @@ func (t Terminator) Remove(sdk.Tx) error { func (t Terminator) Select(context.Context, [][]byte) sdkmempool.Iterator { return nil } + +// ValidateLaneBasic is a no-op +func (t Terminator) ProcessLaneBasic([][]byte) error { + return nil +} + +// SetLaneConfig is a no-op +func (t Terminator) SetAnteHandler(sdk.AnteHandler) {} + +// Logger is a no-op +func (t Terminator) Logger() log.Logger { + return log.NewNopLogger() +} + +// GetMaxBlockSpace is a no-op +func (t Terminator) GetMaxBlockSpace() sdk.Dec { + return sdk.ZeroDec() +} diff --git a/blockbuster/mempool.go b/blockbuster/mempool.go index 8038153..f9316e8 100644 --- a/blockbuster/mempool.go +++ b/blockbuster/mempool.go @@ -19,6 +19,9 @@ type ( // Contains returns true if the transaction is contained in the mempool. Contains(tx sdk.Tx) (bool, error) + + // GetTxDistribution returns the number of transactions in each lane. + GetTxDistribution() map[string]int } // Mempool defines the Blockbuster mempool implement. It contains a registry @@ -34,24 +37,27 @@ func NewMempool(lanes ...Lane) *BBMempool { } } -// TODO: Consider using a tx cache in Mempool and returning the length of that -// cache instead of relying on lane count tracking. +// CountTx returns the total number of transactions in the mempool. func (m *BBMempool) CountTx() int { var total int for _, lane := range m.registry { - // TODO: If a global lane exists, we assume that lane has all transactions - // and we return the total. - // - // if lane.Name() == LaneNameGlobal { - // return lane.CountTx() - // } - total += lane.CountTx() } return total } +// GetTxDistribution returns the number of transactions in each lane. +func (m *BBMempool) GetTxDistribution() map[string]int { + counts := make(map[string]int, len(m.registry)) + + for _, lane := range m.registry { + counts[lane.Name()] = lane.CountTx() + } + + return counts +} + // Insert inserts a transaction into every lane that it matches. Insertion will // be attempted on all lanes, even if an error is encountered. func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) error { @@ -74,8 +80,8 @@ func (m *BBMempool) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator { return nil } -// Remove removes a transaction from every lane that it matches. Removal will be -// attempted on all lanes, even if an error is encountered. +// Remove removes a transaction from the mempool. It removes the transaction +// from the first lane that it matches. func (m *BBMempool) Remove(tx sdk.Tx) error { for _, lane := range m.registry { if lane.Match(tx) { diff --git a/mempool/priority_nonce.go b/blockbuster/priority_nonce.go similarity index 99% rename from mempool/priority_nonce.go rename to blockbuster/priority_nonce.go index 6784f03..5b55c14 100644 --- a/mempool/priority_nonce.go +++ b/blockbuster/priority_nonce.go @@ -1,4 +1,4 @@ -package mempool +package blockbuster import ( "context" diff --git a/blockbuster/utils.go b/blockbuster/utils/utils.go similarity index 75% rename from blockbuster/utils.go rename to blockbuster/utils/utils.go index 350c39d..e3d63ea 100644 --- a/blockbuster/utils.go +++ b/blockbuster/utils/utils.go @@ -1,4 +1,4 @@ -package blockbuster +package utils import ( "crypto/sha256" @@ -7,19 +7,21 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + "github.com/skip-mev/pob/blockbuster" ) -// GetTxHashStr returns the hex-encoded hash of the transaction. -func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) (string, error) { +// GetTxHashStr returns the hex-encoded hash of the transaction alongside the +// transaction bytes. +func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) ([]byte, string, error) { txBz, err := txEncoder(tx) if err != nil { - return "", fmt.Errorf("failed to encode transaction: %w", err) + return nil, "", fmt.Errorf("failed to encode transaction: %w", err) } txHash := sha256.Sum256(txBz) txHashStr := hex.EncodeToString(txHash[:]) - return txHashStr, nil + return txBz, txHashStr, nil } // RemoveTxsFromLane removes the transactions from the given lane's mempool. @@ -35,7 +37,7 @@ func RemoveTxsFromLane(txs map[sdk.Tx]struct{}, mempool sdkmempool.Mempool) erro // GetMaxTxBytesForLane returns the maximum number of bytes that can be included in the proposal // for the given lane. -func GetMaxTxBytesForLane(proposal *Proposal, ratio sdk.Dec) int64 { +func GetMaxTxBytesForLane(proposal *blockbuster.Proposal, ratio sdk.Dec) int64 { // In the case where the ratio is zero, we return the max tx bytes remaining. Note, the only // lane that should have a ratio of zero is the default lane. This means the default lane // will have no limit on the number of transactions it can include in a block and is only diff --git a/mempool/auction_bid.go b/mempool/auction_bid.go deleted file mode 100644 index 86d36d4..0000000 --- a/mempool/auction_bid.go +++ /dev/null @@ -1,128 +0,0 @@ -package mempool - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/signing" -) - -type ( - // AuctionBidInfo defines the information about a bid to the auction house. - AuctionBidInfo struct { - Bidder sdk.AccAddress - Bid sdk.Coin - Transactions [][]byte - Timeout uint64 - Signers []map[string]struct{} - } - - // AuctionFactory defines the interface for processing auction transactions. It is - // a wrapper around all of the functionality that each application chain must implement - // in order for auction processing to work. - AuctionFactory interface { - // WrapBundleTransaction defines a function that wraps a bundle transaction into a sdk.Tx. Since - // this is a potentially expensive operation, we allow each application chain to define how - // they want to wrap the transaction such that it is only called when necessary (i.e. when the - // transaction is being considered in the proposal handlers). - WrapBundleTransaction(tx []byte) (sdk.Tx, error) - - // GetAuctionBidInfo defines a function that returns the bid info from an auction transaction. - GetAuctionBidInfo(tx sdk.Tx) (*AuctionBidInfo, error) - } - - // DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions. - DefaultAuctionFactory struct { - txDecoder sdk.TxDecoder - } - - // TxWithTimeoutHeight is used to extract timeouts from sdk.Tx transactions. In the case where, - // timeouts are explicitly set on the sdk.Tx, we can use this interface to extract the timeout. - TxWithTimeoutHeight interface { - sdk.Tx - - GetTimeoutHeight() uint64 - } -) - -var _ AuctionFactory = (*DefaultAuctionFactory)(nil) - -// NewDefaultAuctionFactory returns a default auction factory interface implementation. -func NewDefaultAuctionFactory(txDecoder sdk.TxDecoder) AuctionFactory { - return &DefaultAuctionFactory{ - txDecoder: txDecoder, - } -} - -// WrapBundleTransaction defines a default function that wraps a transaction -// that is included in the bundle into a sdk.Tx. In the default case, the transaction -// that is included in the bundle will be the raw bytes of an sdk.Tx so we can just -// decode it. -func (config *DefaultAuctionFactory) WrapBundleTransaction(tx []byte) (sdk.Tx, error) { - return config.txDecoder(tx) -} - -// GetAuctionBidInfo defines a default function that returns the auction bid info from -// an auction transaction. In the default case, the auction bid info is stored in the -// MsgAuctionBid message. -func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*AuctionBidInfo, error) { - msg, err := GetMsgAuctionBidFromTx(tx) - if err != nil { - return nil, err - } - - if msg == nil { - return nil, nil - } - - bidder, err := sdk.AccAddressFromBech32(msg.Bidder) - if err != nil { - return nil, fmt.Errorf("invalid bidder address (%s): %w", msg.Bidder, err) - } - - timeoutTx, ok := tx.(TxWithTimeoutHeight) - if !ok { - return nil, fmt.Errorf("cannot extract timeout; transaction does not implement TxWithTimeoutHeight") - } - - signers, err := config.getBundleSigners(msg.Transactions) - if err != nil { - return nil, err - } - - return &AuctionBidInfo{ - Bid: msg.Bid, - Bidder: bidder, - Transactions: msg.Transactions, - Timeout: timeoutTx.GetTimeoutHeight(), - Signers: signers, - }, 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. -func (config *DefaultAuctionFactory) getBundleSigners(bundle [][]byte) ([]map[string]struct{}, error) { - signers := make([]map[string]struct{}, 0) - - for _, tx := range bundle { - sdkTx, err := config.txDecoder(tx) - if err != nil { - return nil, err - } - - sigTx, ok := sdkTx.(signing.SigVerifiableTx) - if !ok { - return nil, fmt.Errorf("transaction is not valid") - } - - txSigners := make(map[string]struct{}) - for _, signer := range sigTx.GetSigners() { - txSigners[signer.String()] = struct{}{} - } - - signers = append(signers, txSigners) - } - - return signers, nil -} diff --git a/mempool/mempool.go b/mempool/mempool.go deleted file mode 100644 index 156f527..0000000 --- a/mempool/mempool.go +++ /dev/null @@ -1,240 +0,0 @@ -package mempool - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" -) - -var _ Mempool = (*AuctionMempool)(nil) - -type ( - // Mempool defines the interface for a POB mempool. - Mempool interface { - // Inherit the methods of the SDK's Mempool interface. - sdkmempool.Mempool - - // GetTopAuctionTx returns the top auction bid transaction in the mempool. - GetTopAuctionTx(ctx context.Context) sdk.Tx - - // CountAuctionTx returns the number of auction bid transactions in the mempool. - CountAuctionTx() int - - // AuctionBidSelect returns an iterator over the auction bid transactions in the mempool. - AuctionBidSelect(ctx context.Context) sdkmempool.Iterator - - // Contains returns true if the mempool contains the given transaction. - Contains(tx sdk.Tx) (bool, error) - - // AuctionFactory implements the functionality required to process auction transactions. - AuctionFactory - } - - // AuctionMempool 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. - AuctionMempool struct { - // globalIndex defines the index of all transactions in the mempool. It uses - // the SDK's builtin PriorityNonceMempool. Once a bid is selected for top-of-block, - // all subsequent transactions in the mempool will be selected from this index. - globalIndex sdkmempool.Mempool - - // auctionIndex defines an index of auction bids. - auctionIndex sdkmempool.Mempool - - // txDecoder defines the sdk.Tx decoder that allows us to decode transactions - // and construct sdk.Txs from the bundled transactions. - txDecoder sdk.TxDecoder - - // 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{} - - // AuctionFactory implements the functionality required to process auction transactions. - AuctionFactory - } -) - -// AuctionTxPriority returns a TxPriority over auction bid transactions only. It -// is to be used in the auction index only. -func AuctionTxPriority(config AuctionFactory) TxPriority[string] { - return TxPriority[string]{ - GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { - bidInfo, err := config.GetAuctionBidInfo(tx) - if err != nil { - panic(err) - } - - return bidInfo.Bid.String() - }, - Compare: func(a, b string) int { - aCoins, _ := sdk.ParseCoinsNormalized(a) - bCoins, _ := sdk.ParseCoinsNormalized(b) - - switch { - case aCoins == nil && bCoins == nil: - return 0 - - case aCoins == nil: - return -1 - - case bCoins == nil: - return 1 - - default: - switch { - case aCoins.IsAllGT(bCoins): - return 1 - - case aCoins.IsAllLT(bCoins): - return -1 - - default: - return 0 - } - } - }, - MinValue: "", - } -} - -func NewAuctionMempool(txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, maxTx int, config AuctionFactory) *AuctionMempool { - return &AuctionMempool{ - globalIndex: NewPriorityMempool( - PriorityNonceMempoolConfig[int64]{ - TxPriority: NewDefaultTxPriority(), - MaxTx: maxTx, - }, - ), - auctionIndex: NewPriorityMempool( - PriorityNonceMempoolConfig[string]{ - TxPriority: AuctionTxPriority(config), - MaxTx: maxTx, - }, - ), - txDecoder: txDecoder, - txEncoder: txEncoder, - txIndex: make(map[string]struct{}), - AuctionFactory: config, - } -} - -// Insert inserts a transaction into the mempool based on the transaction type (normal or auction). -func (am *AuctionMempool) Insert(ctx context.Context, tx sdk.Tx) error { - bidInfo, err := am.GetAuctionBidInfo(tx) - if err != nil { - return err - } - - // Insert the transactions into the appropriate index. - if bidInfo == nil { - if err := am.globalIndex.Insert(ctx, tx); err != nil { - return fmt.Errorf("failed to insert tx into global index: %w", err) - } - } else { - if err := am.auctionIndex.Insert(ctx, tx); err != nil { - return fmt.Errorf("failed to insert tx into auction index: %w", err) - } - } - - txHashStr, err := am.getTxHashStr(tx) - if err != nil { - return err - } - - am.txIndex[txHashStr] = struct{}{} - - return nil -} - -// Remove removes a transaction from the mempool based on the transaction type (normal or auction). -func (am *AuctionMempool) Remove(tx sdk.Tx) error { - bidInfo, err := am.GetAuctionBidInfo(tx) - if err != nil { - return err - } - - // Remove the transactions from the appropriate index. - if bidInfo == nil { - am.removeTx(am.globalIndex, tx) - } else { - am.removeTx(am.auctionIndex, tx) - } - - return nil -} - -// GetTopAuctionTx returns the highest bidding transaction in the auction mempool. -func (am *AuctionMempool) GetTopAuctionTx(ctx context.Context) sdk.Tx { - iterator := am.auctionIndex.Select(ctx, nil) - if iterator == nil { - return nil - } - - return iterator.Tx() -} - -// AuctionBidSelect returns an iterator over auction bids transactions only. -func (am *AuctionMempool) AuctionBidSelect(ctx context.Context) sdkmempool.Iterator { - return am.auctionIndex.Select(ctx, nil) -} - -func (am *AuctionMempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { - return am.globalIndex.Select(ctx, txs) -} - -func (am *AuctionMempool) CountAuctionTx() int { - return am.auctionIndex.CountTx() -} - -func (am *AuctionMempool) CountTx() int { - return am.globalIndex.CountTx() -} - -// Contains returns true if the transaction is contained in the mempool. -func (am *AuctionMempool) Contains(tx sdk.Tx) (bool, error) { - txHashStr, err := am.getTxHashStr(tx) - if err != nil { - return false, fmt.Errorf("failed to get tx hash string: %w", err) - } - - _, ok := am.txIndex[txHashStr] - return ok, nil -} - -func (am *AuctionMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) { - err := mp.Remove(tx) - if err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err)) - } - - txHashStr, err := am.getTxHashStr(tx) - if err != nil { - panic(fmt.Errorf("failed to get tx hash string: %w", err)) - } - - delete(am.txIndex, txHashStr) -} - -// getTxHashStr returns the transaction hash string for a given transaction. -func (am *AuctionMempool) getTxHashStr(tx sdk.Tx) (string, error) { - txBz, err := am.txEncoder(tx) - if err != nil { - return "", fmt.Errorf("failed to encode transaction: %w", err) - } - - txHash := sha256.Sum256(txBz) - txHashStr := hex.EncodeToString(txHash[:]) - - return txHashStr, nil -} diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go deleted file mode 100644 index 0f375ee..0000000 --- a/mempool/mempool_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package mempool_test - -import ( - "context" - "math/rand" - "testing" - "time" - - "github.com/cometbft/cometbft/libs/log" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/pob/mempool" - testutils "github.com/skip-mev/pob/testutils" - buildertypes "github.com/skip-mev/pob/x/builder/types" - "github.com/stretchr/testify/suite" -) - -type IntegrationTestSuite struct { - suite.Suite - - encCfg testutils.EncodingConfig - config mempool.AuctionFactory - mempool *mempool.AuctionMempool - ctx sdk.Context - random *rand.Rand - accounts []testutils.Account - nonces map[string]uint64 -} - -func TestMempoolTestSuite(t *testing.T) { - suite.Run(t, new(IntegrationTestSuite)) -} - -func (suite *IntegrationTestSuite) SetupTest() { - // Mempool setup - suite.encCfg = testutils.CreateTestEncodingConfig() - suite.config = mempool.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder()) - suite.mempool = mempool.NewAuctionMempool(suite.encCfg.TxConfig.TxDecoder(), suite.encCfg.TxConfig.TxEncoder(), 0, suite.config) - suite.ctx = sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger()) - - // Init accounts - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - suite.accounts = testutils.RandomAccounts(suite.random, 10) - - suite.nonces = make(map[string]uint64) - for _, acc := range suite.accounts { - suite.nonces[acc.Address.String()] = 0 - } -} - -// CreateFilledMempool creates a pre-filled mempool with numNormalTxs normal transactions, numAuctionTxs auction transactions, and numBundledTxs bundled -// transactions per auction transaction. If insertRefTxs is true, it will also insert a the referenced transactions into the mempool. This returns -// the total number of transactions inserted into the mempool. -func (suite *IntegrationTestSuite) CreateFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int { - // Insert a bunch of normal transactions into the global mempool - for i := 0; i < numNormalTxs; i++ { - // create a few random msgs - - // randomly select an account to create the tx - randomIndex := suite.random.Intn(len(suite.accounts)) - acc := suite.accounts[randomIndex] - nonce := suite.nonces[acc.Address.String()] - randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - randomTx, err := testutils.CreateTx(suite.encCfg.TxConfig, acc, nonce, 100, randomMsgs) - suite.Require().NoError(err) - - suite.nonces[acc.Address.String()]++ - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), randomTx)) - contains, err := suite.mempool.Contains(randomTx) - suite.Require().NoError(err) - suite.Require().True(contains) - } - - suite.Require().Equal(numNormalTxs, suite.mempool.CountTx()) - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) - - // Insert a bunch of auction transactions into the global mempool and auction mempool - for i := 0; i < numAuctionTxs; i++ { - // randomly select a bidder to create the tx - acc := testutils.RandomAccounts(suite.random, 1)[0] - - // create a new auction bid msg with numBundledTxs bundled transactions - priority := suite.random.Int63n(100) + 1 - bid := sdk.NewInt64Coin("foo", priority) - nonce := suite.nonces[acc.Address.String()] - bidMsg, err := testutils.CreateMsgAuctionBid(suite.encCfg.TxConfig, acc, bid, nonce, numBundledTxs) - suite.nonces[acc.Address.String()] += uint64(numBundledTxs) - suite.Require().NoError(err) - - // create the auction tx - nonce = suite.nonces[acc.Address.String()] - auctionTx, err := testutils.CreateTx(suite.encCfg.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg}) - suite.Require().NoError(err) - - // insert the auction tx into the global mempool - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx)) - contains, err := suite.mempool.Contains(auctionTx) - suite.Require().NoError(err) - suite.Require().True(contains) - suite.nonces[acc.Address.String()]++ - - if insertRefTxs { - for _, refRawTx := range bidMsg.GetTransactions() { - refTx, err := suite.encCfg.TxConfig.TxDecoder()(refRawTx) - suite.Require().NoError(err) - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx)) - contains, err = suite.mempool.Contains(refTx) - suite.Require().NoError(err) - suite.Require().True(contains) - } - } - } - - var totalNumTxs int - suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx()) - if insertRefTxs { - totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs) - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - } else { - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - } - - return totalNumTxs -} - -func (suite *IntegrationTestSuite) TestAuctionMempoolRemove() { - numberTotalTxs := 100 - numberAuctionTxs := 10 - numberBundledTxs := 5 - insertRefTxs := true - numMempoolTxs := suite.CreateFilledMempool(numberTotalTxs, numberAuctionTxs, numberBundledTxs, insertRefTxs) - - // Select the top bid tx from the auction mempool and do sanity checks - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) - suite.Require().NotNil(auctionIterator) - tx := auctionIterator.Tx() - suite.Require().Len(tx.GetMsgs(), 1) - suite.Require().NoError(suite.mempool.Remove(tx)) - - // Ensure that the auction tx was removed from the auction mempool only - suite.Require().Equal(numberAuctionTxs-1, suite.mempool.CountAuctionTx()) - suite.Require().Equal(numMempoolTxs, suite.mempool.CountTx()) - contains, err := suite.mempool.Contains(tx) - suite.Require().NoError(err) - suite.Require().False(contains) - - // Attempt to remove again and ensure that the tx is not found - suite.Require().NoError(suite.mempool.Remove(tx)) - suite.Require().Equal(numberAuctionTxs-1, suite.mempool.CountAuctionTx()) - suite.Require().Equal(numMempoolTxs, suite.mempool.CountTx()) - - // Bundled txs should be in the global mempool - auctionMsg, err := mempool.GetMsgAuctionBidFromTx(tx) - suite.Require().NoError(err) - for _, refTx := range auctionMsg.GetTransactions() { - tx, err := suite.encCfg.TxConfig.TxDecoder()(refTx) - suite.Require().NoError(err) - contains, err = suite.mempool.Contains(tx) - suite.Require().NoError(err) - suite.Require().True(contains) - } - - // Attempt to remove a global tx - iterator := suite.mempool.Select(context.Background(), nil) - tx = iterator.Tx() - size := suite.mempool.CountTx() - suite.mempool.Remove(tx) - suite.Require().Equal(size-1, suite.mempool.CountTx()) - - // Remove the rest of the global transactions - iterator = suite.mempool.Select(context.Background(), nil) - suite.Require().NotNil(iterator) - for iterator != nil { - tx = iterator.Tx() - suite.Require().NoError(suite.mempool.Remove(tx)) - iterator = suite.mempool.Select(context.Background(), nil) - } - suite.Require().Equal(0, suite.mempool.CountTx()) - - // Remove the rest of the auction transactions - auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx) - for auctionIterator != nil { - tx = auctionIterator.Tx() - suite.Require().NoError(suite.mempool.Remove(tx)) - auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx) - } - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) - - // Ensure that the mempool is empty - iterator = suite.mempool.Select(context.Background(), nil) - suite.Require().Nil(iterator) - auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx) - suite.Require().Nil(auctionIterator) - suite.Require().Equal(0, suite.mempool.CountTx()) - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) -} - -func (suite *IntegrationTestSuite) TestAuctionMempoolSelect() { - numberTotalTxs := 100 - numberAuctionTxs := 10 - numberBundledTxs := 5 - insertRefTxs := true - totalTxs := suite.CreateFilledMempool(numberTotalTxs, numberAuctionTxs, numberBundledTxs, insertRefTxs) - - // iterate through the entire auction mempool and ensure the bids are in order - var highestBid sdk.Coin - var prevBid sdk.Coin - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) - numberTxsSeen := 0 - for auctionIterator != nil { - tx := auctionIterator.Tx() - suite.Require().Len(tx.GetMsgs(), 1) - - msgAuctionBid := tx.GetMsgs()[0].(*buildertypes.MsgAuctionBid) - if highestBid.IsNil() { - highestBid = msgAuctionBid.Bid - prevBid = msgAuctionBid.Bid - } else { - suite.Require().True(msgAuctionBid.Bid.IsLTE(highestBid)) - suite.Require().True(msgAuctionBid.Bid.IsLTE(prevBid)) - prevBid = msgAuctionBid.Bid - } - - suite.Require().Len(msgAuctionBid.GetTransactions(), numberBundledTxs) - - auctionIterator = auctionIterator.Next() - numberTxsSeen++ - } - - suite.Require().Equal(numberAuctionTxs, numberTxsSeen) - - iterator := suite.mempool.Select(context.Background(), nil) - numberTxsSeen = 0 - for iterator != nil { - iterator = iterator.Next() - numberTxsSeen++ - } - suite.Require().Equal(totalTxs, numberTxsSeen) -} diff --git a/mempool/utils.go b/mempool/utils.go deleted file mode 100644 index 6bcd4ad..0000000 --- a/mempool/utils.go +++ /dev/null @@ -1,35 +0,0 @@ -package mempool - -import ( - "errors" - - sdk "github.com/cosmos/cosmos-sdk/types" - buildertypes "github.com/skip-mev/pob/x/builder/types" -) - -// GetMsgAuctionBidFromTx attempts to retrieve a MsgAuctionBid from an sdk.Tx if -// one exists. If a MsgAuctionBid does exist and other messages are also present, -// an error is returned. If no MsgAuctionBid is present, is returned. -func GetMsgAuctionBidFromTx(tx sdk.Tx) (*buildertypes.MsgAuctionBid, error) { - auctionBidMsgs := make([]*buildertypes.MsgAuctionBid, 0) - for _, msg := range tx.GetMsgs() { - t, ok := msg.(*buildertypes.MsgAuctionBid) - if ok { - auctionBidMsgs = append(auctionBidMsgs, t) - } - } - - switch { - case len(auctionBidMsgs) == 0: - // a normal transaction without a MsgAuctionBid message - return nil, nil - - case len(auctionBidMsgs) == 1 && len(tx.GetMsgs()) == 1: - // a single MsgAuctionBid message transaction - return auctionBidMsgs[0], nil - - default: - // a transaction with at at least one MsgAuctionBid message - return nil, errors.New("invalid MsgAuctionBid transaction") - } -} diff --git a/tests/app/ante.go b/tests/app/ante.go index c7a6709..ed5d160 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -3,14 +3,15 @@ package app import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/ante" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/blockbuster" builderante "github.com/skip-mev/pob/x/builder/ante" builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) type POBHandlerOptions struct { BaseOptions ante.HandlerOptions - Mempool mempool.Mempool + Mempool blockbuster.Mempool + TOBLane builderante.TOBLane TxDecoder sdk.TxDecoder TxEncoder sdk.TxEncoder BuilderKeeper builderkeeper.Keeper @@ -48,7 +49,7 @@ func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler { ante.NewSigGasConsumeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SigGasConsumer), ante.NewSigVerificationDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SignModeHandler), ante.NewIncrementSequenceDecorator(options.BaseOptions.AccountKeeper), - builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxEncoder, options.Mempool), + builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxEncoder, options.TOBLane, options.Mempool), } return sdk.ChainAnteDecorators(anteDecorators...) diff --git a/tests/app/app.go b/tests/app/app.go index e09e705..290bc44 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -67,8 +67,10 @@ import ( "github.com/cosmos/cosmos-sdk/x/upgrade" upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" - "github.com/skip-mev/pob/abci" - "github.com/skip-mev/pob/mempool" + "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" buildermodule "github.com/skip-mev/pob/x/builder" builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) @@ -261,8 +263,38 @@ func New( app.App = appBuilder.Build(logger, db, traceStore, baseAppOptions...) + // ---------------------------------------------------------------------------- // + // ------------------------- Begin Custom Code -------------------------------- // + // ---------------------------------------------------------------------------- // + // Set POB's mempool into the app. - mempool := mempool.NewAuctionMempool(app.txConfig.TxDecoder(), app.txConfig.TxEncoder(), 0, mempool.NewDefaultAuctionFactory(app.txConfig.TxDecoder())) + config := blockbuster.BaseLaneConfig{ + Logger: app.Logger(), + TxEncoder: app.txConfig.TxEncoder(), + TxDecoder: app.txConfig.TxDecoder(), + MaxBlockSpace: sdk.ZeroDec(), + } + + // Create the lanes. + // + // 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. + tobLane := auction.NewTOBLane( + config, + 0, + auction.NewDefaultAuctionFactory(app.txConfig.TxDecoder()), + ) + + // Default lane accepts all other transactions. + defaultLane := base.NewDefaultLane(config) + lanes := []blockbuster.Lane{ + tobLane, + defaultLane, + } + + mempool := blockbuster.NewMempool(lanes...) app.App.SetMempool(mempool) // Create a global ante handler that will be called on each transaction when @@ -277,19 +309,22 @@ func New( options := POBHandlerOptions{ BaseOptions: handlerOptions, BuilderKeeper: app.BuilderKeeper, - Mempool: mempool, TxDecoder: app.txConfig.TxDecoder(), TxEncoder: app.txConfig.TxEncoder(), + TOBLane: tobLane, + Mempool: mempool, } anteHandler := NewPOBAnteHandler(options) + // Set the lane config on the lanes. + for _, lane := range lanes { + lane.SetAnteHandler(anteHandler) + } + // Set the proposal handlers on the BaseApp along with the custom antehandler. proposalHandlers := abci.NewProposalHandler( + app.Logger(), mempool, - app.App.Logger(), - anteHandler, - options.TxEncoder, - options.TxDecoder, ) app.App.SetPrepareProposal(proposalHandlers.PrepareProposalHandler()) app.App.SetProcessProposal(proposalHandlers.ProcessProposalHandler()) @@ -299,12 +334,16 @@ func New( checkTxHandler := abci.NewCheckTxHandler( app.App, app.txConfig.TxDecoder(), - mempool, + tobLane, anteHandler, ChainID, ) app.SetCheckTx(checkTxHandler.CheckTx()) + // ---------------------------------------------------------------------------- // + // ------------------------- End Custom Code ---------------------------------- // + // ---------------------------------------------------------------------------- // + // load state streaming if enabled if _, _, err := streaming.LoadStreamingServices(app.App.BaseApp, appOpts, app.appCodec, logger, app.kvStoreKeys()); err != nil { logger.Error("failed to load state streaming", "err", err) diff --git a/x/builder/ante/ante.go b/x/builder/ante/ante.go index 00a2d49..c9c0def 100644 --- a/x/builder/ante/ante.go +++ b/x/builder/ante/ante.go @@ -7,32 +7,39 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/pob/mempool" "github.com/skip-mev/pob/x/builder/keeper" + "github.com/skip-mev/pob/x/builder/types" ) var _ sdk.AnteDecorator = BuilderDecorator{} type ( + // TOBLane is an interface that defines the methods required to interact with the top of block + // lane. + TOBLane interface { + GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) + GetTopAuctionTx(ctx context.Context) sdk.Tx + } + // Mempool is an interface that defines the methods required to interact with the application-side mempool. Mempool interface { Contains(tx sdk.Tx) (bool, error) - GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error) - GetTopAuctionTx(ctx context.Context) sdk.Tx } // BuilderDecorator is an AnteDecorator that validates the auction bid and bundled transactions. BuilderDecorator struct { builderKeeper keeper.Keeper txEncoder sdk.TxEncoder + lane TOBLane mempool Mempool } ) -func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator { +func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, lane TOBLane, mempool Mempool) BuilderDecorator { return BuilderDecorator{ builderKeeper: ak, txEncoder: txEncoder, + lane: lane, mempool: mempool, } } @@ -52,7 +59,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, } } - bidInfo, err := bd.mempool.GetAuctionBidInfo(tx) + bidInfo, err := bd.lane.GetAuctionBidInfo(tx) if err != nil { return ctx, err } @@ -70,7 +77,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // poor liveness guarantees. topBid := sdk.Coin{} if ctx.IsCheckTx() || ctx.IsReCheckTx() { - if topBidTx := bd.mempool.GetTopAuctionTx(ctx); topBidTx != nil { + if topBidTx := bd.lane.GetTopAuctionTx(ctx); topBidTx != nil { topBidBz, err := bd.txEncoder(topBidTx) if err != nil { return ctx, err @@ -83,7 +90,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // Compare the bytes to see if the current transaction is the highest bidding transaction. if !bytes.Equal(topBidBz, currentTxBz) { - topBidInfo, err := bd.mempool.GetAuctionBidInfo(topBidTx) + topBidInfo, err := bd.lane.GetAuctionBidInfo(topBidTx) if err != nil { return ctx, err } diff --git a/x/builder/ante/ante_test.go b/x/builder/ante/ante_test.go index 4253aa7..7765f6c 100644 --- a/x/builder/ante/ante_test.go +++ b/x/builder/ante/ante_test.go @@ -9,7 +9,9 @@ import ( "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/auction" + "github.com/skip-mev/pob/blockbuster/lanes/base" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/ante" "github.com/skip-mev/pob/x/builder/keeper" @@ -21,7 +23,6 @@ type AnteTestSuite struct { suite.Suite ctx sdk.Context - // mempool setup encodingConfig testutils.EncodingConfig random *rand.Rand @@ -34,6 +35,15 @@ type AnteTestSuite struct { builderDecorator ante.BuilderDecorator key *storetypes.KVStoreKey authorityAccount sdk.AccAddress + + // mempool and lane set up + mempool blockbuster.Mempool + tobLane *auction.TOBLane + baseLane *base.DefaultLane + lanes []blockbuster.Lane + + // Account set up + balance sdk.Coins } func TestAnteTestSuite(t *testing.T) { @@ -67,17 +77,40 @@ func (suite *AnteTestSuite) SetupTest() { ) err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) suite.Require().NoError(err) + + // Lanes configuration + // + // TOB lane set up + config := blockbuster.BaseLaneConfig{ + Logger: suite.ctx.Logger(), + TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: suite.anteHandler, + MaxBlockSpace: sdk.ZeroDec(), + } + suite.tobLane = auction.NewTOBLane( + config, + 0, // No bound on the number of transactions in the lane + auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), + ) + + // Base lane set up + suite.baseLane = base.NewDefaultLane(config) + + // Mempool set up + suite.lanes = []blockbuster.Lane{suite.tobLane, suite.baseLane} + suite.mempool = blockbuster.NewMempool(suite.lanes...) } -func (suite *AnteTestSuite) executeAnteHandler(tx sdk.Tx, balance sdk.Coins) (sdk.Context, error) { +func (suite *AnteTestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { signer := tx.GetMsgs()[0].GetSigners()[0] - suite.bankKeeper.EXPECT().GetAllBalances(suite.ctx, signer).AnyTimes().Return(balance) + suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balance) - next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + next := func(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil } - return suite.builderDecorator.AnteHandle(suite.ctx, tx, false, next) + return suite.builderDecorator.AnteHandle(ctx, tx, false, next) } func (suite *AnteTestSuite) TestAnteHandler() { @@ -232,15 +265,19 @@ func (suite *AnteTestSuite) TestAnteHandler() { suite.Require().NoError(err) // Insert the top bid into the mempool - config := mempool.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()) - mempool := mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, config) if insertTopBid { topAuctionTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, topBidder, topBid, 0, timeout, []testutils.Account{}) suite.Require().NoError(err) - suite.Require().Equal(0, mempool.CountTx()) - suite.Require().Equal(0, mempool.CountAuctionTx()) - suite.Require().NoError(mempool.Insert(suite.ctx, topAuctionTx)) - suite.Require().Equal(1, mempool.CountAuctionTx()) + + distribution := suite.mempool.GetTxDistribution() + suite.Require().Equal(0, distribution[auction.LaneName]) + suite.Require().Equal(0, distribution[base.LaneName]) + + suite.Require().NoError(suite.mempool.Insert(suite.ctx, topAuctionTx)) + + distribution = suite.mempool.GetTxDistribution() + suite.Require().Equal(1, distribution[auction.LaneName]) + suite.Require().Equal(0, distribution[base.LaneName]) } // Create the actual auction tx and insert into the mempool @@ -248,8 +285,9 @@ func (suite *AnteTestSuite) TestAnteHandler() { suite.Require().NoError(err) // Execute the ante handler - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), mempool) - _, err = suite.executeAnteHandler(auctionTx, balance) + suite.balance = balance + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) + _, err = suite.anteHandler(suite.ctx, auctionTx, false) if tc.pass { suite.Require().NoError(err) } else { diff --git a/x/builder/keeper/auction.go b/x/builder/keeper/auction.go index d0774c3..bb0dff9 100644 --- a/x/builder/keeper/auction.go +++ b/x/builder/keeper/auction.go @@ -4,11 +4,11 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/pob/mempool" + "github.com/skip-mev/pob/x/builder/types" ) // ValidateBidInfo validates that the bid can be included in the auction. -func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *mempool.AuctionBidInfo) error { +func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *types.BidInfo) error { // Validate the bundle size. maxBundleSize, err := k.GetMaxBundleSize(ctx) if err != nil { diff --git a/x/builder/keeper/auction_test.go b/x/builder/keeper/auction_test.go index 64fe69d..8617709 100644 --- a/x/builder/keeper/auction_test.go +++ b/x/builder/keeper/auction_test.go @@ -5,10 +5,9 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/pob/mempool" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/keeper" - buildertypes "github.com/skip-mev/pob/x/builder/types" + "github.com/skip-mev/pob/x/builder/types" ) func (suite *KeeperTestSuite) TestValidateBidInfo() { @@ -162,7 +161,7 @@ func (suite *KeeperTestSuite) TestValidateBidInfo() { suite.stakingKeeper, suite.authorityAccount.String(), ) - params := buildertypes.Params{ + params := types.Params{ MaxBundleSize: maxBundleSize, ReserveFee: reserveFee, EscrowAccountAddress: escrowAddress.String(), @@ -191,7 +190,7 @@ func (suite *KeeperTestSuite) TestValidateBidInfo() { signers[index] = txSigners } - bidInfo := &mempool.AuctionBidInfo{ + bidInfo := &types.BidInfo{ Bidder: bidder.Address, Bid: bid, Transactions: bundle, diff --git a/x/builder/keeper/keeper_test.go b/x/builder/keeper/keeper_test.go index c4d1094..a8d07e9 100644 --- a/x/builder/keeper/keeper_test.go +++ b/x/builder/keeper/keeper_test.go @@ -7,7 +7,6 @@ import ( "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" - "github.com/skip-mev/pob/mempool" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/keeper" "github.com/skip-mev/pob/x/builder/types" @@ -28,8 +27,6 @@ type KeeperTestSuite struct { msgServer types.MsgServer key *storetypes.KVStoreKey authorityAccount sdk.AccAddress - - mempool *mempool.AuctionMempool } func TestKeeperTestSuite(t *testing.T) { @@ -64,7 +61,5 @@ func (suite *KeeperTestSuite) SetupTest() { err := suite.builderKeeper.SetParams(suite.ctx, types.DefaultParams()) suite.Require().NoError(err) - config := mempool.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder()) - suite.mempool = mempool.NewAuctionMempool(suite.encCfg.TxConfig.TxDecoder(), suite.encCfg.TxConfig.TxEncoder(), 0, config) suite.msgServer = keeper.NewMsgServerImpl(suite.builderKeeper) } diff --git a/x/builder/types/bid_info.go b/x/builder/types/bid_info.go new file mode 100644 index 0000000..54d2627 --- /dev/null +++ b/x/builder/types/bid_info.go @@ -0,0 +1,12 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +// BidInfo defines the information about a bid to the auction house. +type BidInfo struct { + Bidder sdk.AccAddress + Bid sdk.Coin + Transactions [][]byte + Timeout uint64 + Signers []map[string]struct{} +}