From 10688927ce1712eb6fcb56dc92f1ca300294b391 Mon Sep 17 00:00:00 2001 From: David Terpay <35130517+davidterpay@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:28:13 -0400 Subject: [PATCH] feat(BB): Initialization of lane constructor code (#244) --- blockbuster/abci/abci.go | 42 +- blockbuster/abci/abci_test.go | 1745 +++++++---------- blockbuster/lane.go | 123 -- blockbuster/lane_abci.go | 67 + blockbuster/lane_constructor.go | 198 ++ blockbuster/lane_handlers.go | 155 ++ blockbuster/lane_interface.go | 71 + blockbuster/lane_mempool.go | 159 ++ blockbuster/lanes/auction/abci.go | 429 ++-- blockbuster/lanes/auction/auction_test.go | 2 - .../{abci => lanes/auction}/check_tx.go | 21 +- blockbuster/lanes/auction/factory.go | 11 + blockbuster/lanes/auction/lane.go | 80 +- blockbuster/lanes/auction/mempool.go | 110 +- blockbuster/lanes/base/abci.go | 142 -- blockbuster/lanes/base/abci_test.go | 547 ++++++ blockbuster/lanes/base/base_test.go | 32 + blockbuster/lanes/base/lane.go | 97 +- blockbuster/lanes/base/mempool.go | 105 - blockbuster/lanes/base/mempool_test.go | 240 +++ blockbuster/lanes/free/factory.go | 47 - blockbuster/lanes/free/lane.go | 55 +- blockbuster/lanes/terminator/lane.go | 52 +- blockbuster/mempool.go | 56 +- blockbuster/mempool_test.go | 35 +- blockbuster/priority_nonce.go | 16 +- blockbuster/proposals.go | 23 +- blockbuster/types.go | 164 ++ blockbuster/utils/ante.go | 2 +- blockbuster/utils/mocks/lane.go | 256 +++ blockbuster/utils/mocks/lane_mempool.go | 117 ++ blockbuster/utils/utils.go | 4 +- go.mod | 23 +- go.sum | 34 - tests/app/app.go | 34 +- tests/integration/chain_setup.go | 1 - tests/integration/pob_integration_test.go | 4 +- testutils/utils.go | 59 +- x/builder/ante/ante_test.go | 7 +- 39 files changed, 3282 insertions(+), 2083 deletions(-) delete mode 100644 blockbuster/lane.go create mode 100644 blockbuster/lane_abci.go create mode 100644 blockbuster/lane_constructor.go create mode 100644 blockbuster/lane_handlers.go create mode 100644 blockbuster/lane_interface.go create mode 100644 blockbuster/lane_mempool.go rename blockbuster/{abci => lanes/auction}/check_tx.go (92%) delete mode 100644 blockbuster/lanes/base/abci.go create mode 100644 blockbuster/lanes/base/abci_test.go create mode 100644 blockbuster/lanes/base/base_test.go delete mode 100644 blockbuster/lanes/base/mempool.go create mode 100644 blockbuster/lanes/base/mempool_test.go delete mode 100644 blockbuster/lanes/free/factory.go create mode 100644 blockbuster/types.go create mode 100644 blockbuster/utils/mocks/lane.go create mode 100644 blockbuster/utils/mocks/lane_mempool.go diff --git a/blockbuster/abci/abci.go b/blockbuster/abci/abci.go index 50f418e..dd8feb6 100644 --- a/blockbuster/abci/abci.go +++ b/blockbuster/abci/abci.go @@ -1,6 +1,8 @@ package abci import ( + "fmt" + "cosmossdk.io/log" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -20,13 +22,14 @@ type ( } ) -// NewProposalHandler returns a new abci++ proposal handler. -func NewProposalHandler(logger log.Logger, txDecoder sdk.TxDecoder, mempool blockbuster.Mempool) *ProposalHandler { +// NewProposalHandler returns a new abci++ proposal handler. This proposal handler will +// iteratively call each of the lanes in the chain to prepare and process the proposal. +func NewProposalHandler(logger log.Logger, txDecoder sdk.TxDecoder, lanes []blockbuster.Lane) *ProposalHandler { return &ProposalHandler{ logger: logger, txDecoder: txDecoder, - prepareLanesHandler: ChainPrepareLanes(mempool.Registry()...), - processLanesHandler: ChainProcessLanes(mempool.Registry()...), + prepareLanesHandler: ChainPrepareLanes(lanes...), + processLanesHandler: ChainProcessLanes(lanes...), } } @@ -55,6 +58,7 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { "prepared proposal", "num_txs", proposal.GetNumTxs(), "total_tx_bytes", proposal.GetTotalTxBytes(), + "height", req.Height, ) return &abci.ResponsePrepareProposal{ @@ -71,9 +75,11 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { return func(ctx sdk.Context, req *abci.RequestProcessProposal) (resp *abci.ResponseProcessProposal, err error) { // 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) + if rec := recover(); rec != nil { + h.logger.Error("failed to process proposal", "recover_err", rec) + resp = &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT} + err = fmt.Errorf("failed to process proposal: %v", rec) } }() @@ -125,9 +131,14 @@ func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandle // Cache the context in the case where any of the lanes fail to prepare the proposal. cacheCtx, write := ctx.CacheContext() + // We utilize a recover to handle any panics or errors that occur during the preparation + // of a lane's transactions. This defer will first check if there was a panic or error + // thrown from the lane's preparation logic. If there was, we log the error, skip the lane, + // and call the next lane in the chain to the prepare the proposal. defer func() { if rec := recover(); rec != nil || err != nil { lane.Logger().Error("failed to prepare lane", "lane", lane.Name(), "err", err, "recover_error", rec) + lane.Logger().Info("skipping lane", "lane", lane.Name()) lanesRemaining := len(chain) switch { @@ -141,22 +152,13 @@ func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandle // 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.GetMaxTxBytes(), - partialProposal.GetTotalTxBytes(), - chain[1].GetMaxBlockSpace(), - ) - - finalProposal, err = chain[1].PrepareLane( - ctx, - partialProposal, - maxTxBytesForLane, - ChainPrepareLanes(chain[2:]...), - ) + finalProposal, err = ChainPrepareLanes(chain[1:]...)(ctx, partialProposal) } } else { // Write the cache to the context since we know that the lane successfully prepared - // the partial proposal. + // the partial proposal. State is written to in a backwards, cascading fashion. This means + // that the final context will only be updated after all other lanes have successfully + // prepared the partial proposal. write() } }() @@ -198,7 +200,7 @@ func ChainProcessLanes(chain ...blockbuster.Lane) blockbuster.ProcessLanesHandle chain[0].Logger().Info("processing lane", "lane", chain[0].Name()) - if err := chain[0].ProcessLaneBasic(ctx, proposalTxs); err != nil { + if err := chain[0].CheckOrder(ctx, proposalTxs); err != nil { chain[0].Logger().Error("failed to process lane", "lane", chain[0].Name(), "err", err) return ctx, err } diff --git a/blockbuster/abci/abci_test.go b/blockbuster/abci/abci_test.go index 4444d4a..0a6fd51 100644 --- a/blockbuster/abci/abci_test.go +++ b/blockbuster/abci/abci_test.go @@ -1,1071 +1,802 @@ package abci_test import ( + "crypto/sha256" + "encoding/hex" + "fmt" "math/rand" "testing" - "time" "cosmossdk.io/log" "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" + cometabci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/blockbuster/abci" "github.com/skip-mev/pob/blockbuster/lanes/auction" "github.com/skip-mev/pob/blockbuster/lanes/base" "github.com/skip-mev/pob/blockbuster/lanes/free" testutils "github.com/skip-mev/pob/testutils" - "github.com/skip-mev/pob/x/builder/ante" - "github.com/skip-mev/pob/x/builder/keeper" - buildertypes "github.com/skip-mev/pob/x/builder/types" "github.com/stretchr/testify/suite" - - abcitypes "github.com/cometbft/cometbft/abci/types" ) -type ABCITestSuite struct { +type ProposalsTestSuite struct { suite.Suite ctx sdk.Context + key *storetypes.KVStoreKey - // Define basic tx configuration encodingConfig testutils.EncodingConfig - - // Define all of the lanes utilized in the test suite - tobConfig blockbuster.BaseLaneConfig - tobLane *auction.TOBLane - - freeConfig blockbuster.BaseLaneConfig - freeLane *free.Lane - - baseConfig blockbuster.BaseLaneConfig - baseLane *base.DefaultLane - - lanes []blockbuster.Lane - mempool blockbuster.Mempool - - // Proposal handler set up - proposalHandler *abci.ProposalHandler - - // account set up - accounts []testutils.Account - random *rand.Rand - nonces map[string]uint64 - - // Keeper set up - builderKeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper - builderDecorator ante.BuilderDecorator + random *rand.Rand + accounts []testutils.Account + gasTokenDenom string } func TestBlockBusterTestSuite(t *testing.T) { - suite.Run(t, new(ABCITestSuite)) + suite.Run(t, new(ProposalsTestSuite)) } -func (suite *ABCITestSuite) SetupTest() { - // General config for transactions and randomness for the test suite - suite.encodingConfig = testutils.CreateTestEncodingConfig() - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - key := storetypes.NewKVStoreKey(buildertypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx.WithBlockHeight(1) +func (s *ProposalsTestSuite) SetupTest() { + // Set up basic TX encoding config. + s.encodingConfig = testutils.CreateTestEncodingConfig() - // Lanes configuration - // Top of block lane set up - suite.tobConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - } - suite.tobLane = auction.NewTOBLane( - suite.tobConfig, - 0, // No bound on the number of transactions in the lane - auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) + // Create a few random accounts + s.random = rand.New(rand.NewSource(1)) + s.accounts = testutils.RandomAccounts(s.random, 5) + s.gasTokenDenom = "stake" - // Free lane set up - suite.freeConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - IgnoreList: []blockbuster.Lane{suite.tobLane}, - } - suite.freeLane = free.NewFreeLane( - suite.freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Base lane set up - suite.baseConfig = blockbuster.BaseLaneConfig{ - Logger: log.NewTestLogger(suite.T()), - TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: suite.anteHandler, - MaxBlockSpace: math.LegacyZeroDec(), // It can be as big as it wants (up to maxTxBytes) - IgnoreList: []blockbuster.Lane{suite.tobLane, suite.freeLane}, - } - suite.baseLane = base.NewDefaultLane( - suite.baseConfig, - ) - - // Mempool set up - suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) - - // Accounts set up - suite.accounts = testutils.RandomAccounts(suite.random, 10) - suite.nonces = make(map[string]uint64) - for _, acc := range suite.accounts { - suite.nonces[acc.Address.String()] = 0 - } - - // Set up the keepers and decorators - // Mock keepers set up - ctrl := gomock.NewController(suite.T()) - suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) - suite.accountKeeper.EXPECT().GetModuleAddress(buildertypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes() - suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) - suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) - suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) - - // Builder keeper / decorator set up - suite.builderKeeper = keeper.NewKeeper( - suite.encodingConfig.Codec, - key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - sdk.AccAddress([]byte("authority")).String(), - ) - - // Set the default params for the builder keeper - err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) - suite.Require().NoError(err) - - // Set up the ante handler - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - // Proposal handler set up - suite.proposalHandler = abci.NewProposalHandler(log.NewTestLogger(suite.T()), suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool) + s.key = storetypes.NewKVStoreKey("test") + testCtx := testutil.DefaultContextWithDB(s.T(), s.key, storetypes.NewTransientStoreKey("transient_test")) + s.ctx = testCtx.Ctx.WithIsCheckTx(true) } -func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { - suite.bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), "stake").AnyTimes().Return( - sdk.NewCoin("stake", math.NewInt(100000000000000)), - ) +func (s *ProposalsTestSuite) TestPrepareProposal() { + s.Run("can prepare a proposal with no transactions", func() { + // Set up the default lane with no transactions + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), nil) - next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { - return ctx, nil - } + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() - return suite.builderDecorator.AnteHandle(ctx, tx, false, next) -} + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(0, len(resp.Txs)) + }) -func (suite *ABCITestSuite) resetLanesWithNewConfig() { - // Top of block lane set up - suite.tobLane = auction.NewTOBLane( - suite.tobConfig, - 0, // No bound on the number of transactions in the lane - auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Free lane set up - suite.freeLane = free.NewFreeLane( - suite.freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), - ) - - // Base lane set up - suite.baseLane = base.NewDefaultLane( - suite.baseConfig, - ) - - suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) -} - -func (suite *ABCITestSuite) TestPrepareProposal() { - var ( - // the modified transactions cannot exceed this size - maxTxBytes int64 = 1000000000000000000 - - // mempool configuration - txs []sdk.Tx - auctionTxs []sdk.Tx - winningBidTx sdk.Tx - insertBundledTxs = false - - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - minBidIncrement = sdk.NewCoin("stake", math.NewInt(100)) - frontRunningProtection = true - ) - - cases := []struct { - name string - malleate func() - expectedNumberProposalTxs int - expectedMempoolDistribution map[string]int - }{ - { - "empty mempool", - func() { - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{} - winningBidTx = nil - insertBundledTxs = false - }, + s.Run("can build a proposal with a single tx from the lane", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], 0, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "maxTxBytes is less than any transaction in the mempool", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[2] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - maxTxBytes = 10 - }, - 0, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "valid tob tx but maxTxBytes is less for the tob lane so only the free tx should be included", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - maxTxBytes = tobSize + freeSize - suite.tobConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.1") - - txs = []sdk.Tx{freeTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - }, 1, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "valid tob tx with sufficient space for only tob tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - maxTxBytes = tobSize*2 + freeSize - 1 - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.freeConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.1") - - txs = []sdk.Tx{freeTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "tob, free, and normal tx but only space for tob and normal tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - // Get the size of the tob tx - bidTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) - suite.Require().NoError(err) - tobSize := int64(len(bidTxBytes)) - - // Get the size of the free tx - freeTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(freeTx) - suite.Require().NoError(err) - freeSize := int64(len(freeTxBytes)) - - // Get the size of the normal tx - normalTxBytes, err := suite.encodingConfig.TxConfig.TxEncoder()(normalTx) - suite.Require().NoError(err) - normalSize := int64(len(normalTxBytes)) - - maxTxBytes = tobSize*2 + freeSize + normalSize + 1 - - // Tob can take up as much space as it wants - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - - // Free can take up less space than the tx - suite.freeConfig.MaxBlockSpace = math.LegacyMustNewDecFromStr("0.01") - - // Default can take up as much space as it wants - suite.baseConfig.MaxBlockSpace = math.LegacyZeroDec() - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 4, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - { - "single valid tob transaction in the mempool", - func() { - // reset the configs - suite.tobConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.freeConfig.MaxBlockSpace = math.LegacyZeroDec() - suite.baseConfig.MaxBlockSpace = math.LegacyZeroDec() - - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - maxTxBytes = 1000000000000000000 - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "single invalid tob transaction in the mempool", - func() { - bidder := suite.accounts[0] - bid := reserveFee.Sub(sdk.NewCoin("stake", math.NewInt(1))) // bid is less than the reserve fee - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = nil - insertBundledTxs = false - }, 0, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "normal transactions in the mempool", - func() { - account := suite.accounts[0] - nonce := suite.nonces[account.Address.String()] - timeout := uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{} - winningBidTx = nil - insertBundledTxs = false - }, + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal with multiple txs from the lane", func() { + // Create a random transaction that will be inserted into the default lane + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, 1, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 0, - free.LaneName: 0, - }, - }, - { - "normal transactions and tob transactions in the mempool", - func() { - // Create a valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - // Create a valid default transaction - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] + 1 - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + // Create a second random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 3, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is invalid", - func() { - // Create an invalid tob transaction (frontrunning) - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder, bidder, suite.accounts[1]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + // Set up the default lane with both transactions passing + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx1: true, tx2: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - winningBidTx = bidTx2 - insertBundledTxs = false - }, - 2, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is valid", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposal := s.getTxBytes(tx2, tx1) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + s.Run("can build a proposal with single tx with other that fails", func() { + // Create a random transaction that will be inserted into the default lane + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - winningBidTx = bidTx - insertBundledTxs = false - }, - 3, - map[string]int{ - base.LaneName: 0, - auction.LaneName: 2, - free.LaneName: 0, - }, - }, - { - "multiple tob transactions where the first is valid and bundle is inserted into mempool", - func() { - frontRunningProtection = false + // Create a second random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), + ) + s.Require().NoError(err) - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + // Set up the default lane with both transactions passing + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx1: true, tx2: false}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = true - }, - 6, - map[string]int{ - base.LaneName: 5, - auction.LaneName: 1, - free.LaneName: 0, - }, - }, - { - "valid tob, free, and normal tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).PrepareProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NotNil(resp) + s.Require().NoError(err) - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) + proposal := s.getTxBytes(tx1) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + s.Run("can build a proposal an empty proposal with multiple lanes", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), nil) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - winningBidTx = bidTx - insertBundledTxs = false - }, - 5, - map[string]int{ - base.LaneName: 1, - auction.LaneName: 1, - free.LaneName: 1, - }, - }, - } + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() - suite.resetLanesWithNewConfig() + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) + s.Require().NoError(err) + s.Require().NotNil(resp) - // Insert all of the normal transactions into the default lane - for _, tx := range txs { - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } + s.Require().Equal(0, len(resp.Txs)) + }) - // Insert all of the auction transactions into the TOB lane - for _, tx := range auctionTxs { - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } + s.Run("can build a proposal with transactions from a single lane given multiple lanes", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) - // Insert all of the bundled transactions into the TOB lane if desired - if insertBundledTxs { - for _, tx := range auctionTxs { - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - for _, txBz := range bidInfo.Transactions { - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz) - suite.Require().NoError(err) - - suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) - } - } - } - - // Create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - MinBidIncrement: minBidIncrement, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - for _, lane := range suite.lanes { - lane.SetAnteHandler(suite.anteHandler) - } - - // Create a new proposal handler - suite.proposalHandler = abci.NewProposalHandler(log.NewTestLogger(suite.T()), suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool) - handler := suite.proposalHandler.PrepareProposalHandler() - res, err := handler(suite.ctx, &abcitypes.RequestPrepareProposal{ - MaxTxBytes: maxTxBytes, - }) - suite.Require().NoError(err) - - // -------------------- Check Invariants -------------------- // - // 1. the number of transactions in the response must be equal to the number of expected transactions - suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs)) - - // 2. total bytes must be less than or equal to maxTxBytes - totalBytes := int64(0) - txIndex := 0 - for txIndex < len(res.Txs) { - totalBytes += int64(len(res.Txs[txIndex])) - - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[txIndex]) - suite.Require().NoError(err) - - suite.Require().Equal(true, suite.mempool.Contains(tx)) - - // In the case where we have a tob tx, we skip the other transactions in the bundle - // in order to not double count - switch { - case suite.tobLane.Match(suite.ctx, tx): - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - txIndex += len(bidInfo.Transactions) + 1 - default: - txIndex++ - } - } - - suite.Require().LessOrEqual(totalBytes, maxTxBytes) - - // 3. if there are auction transactions, the first transaction must be the top bid - // and the rest of the bundle must be in the response - if winningBidTx != nil { - auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0]) - suite.Require().NoError(err) - - bidInfo, err := suite.tobLane.GetAuctionBidInfo(auctionTx) - suite.Require().NoError(err) - - for index, tx := range bidInfo.Transactions { - suite.Require().Equal(tx, res.Txs[index+1]) - } - } else if len(res.Txs) > 0 { - tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0]) - suite.Require().NoError(err) - - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - suite.Require().Nil(bidInfo) - } - - // 4. All of the transactions must be unique - uniqueTxs := make(map[string]bool) - for _, tx := range res.Txs { - suite.Require().False(uniqueTxs[string(tx)]) - uniqueTxs[string(tx)] = true - } - - // 5. The number of transactions in the mempool must be correct - suite.Require().Equal(tc.expectedMempoolDistribution, suite.mempool.GetTxDistribution()) - - // 6. The ordering of transactions must respect the ordering of the lanes - laneIndex := 0 - txIndex = 0 - for txIndex < len(res.Txs) { - sdkTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[txIndex]) - suite.Require().NoError(err) - - if suite.lanes[laneIndex].Match(suite.ctx, sdkTx) { - switch suite.lanes[laneIndex].Name() { - case suite.tobLane.Name(): - bidInfo, err := suite.tobLane.GetAuctionBidInfo(sdkTx) - suite.Require().NoError(err) - - txIndex += len(bidInfo.Transactions) + 1 - default: - txIndex++ - } - } else { - laneIndex++ - } - - suite.Require().Less(laneIndex, len(suite.lanes)) - } + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, }) - } + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx, bundleTxs[0]) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can ignore txs that are already included in a proposal", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx, bundleTxs[0]) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal where first lane has too large of a tx and second lane has a valid tx", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: false, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + // Even though this passes it should not include it in the proposal because it is in the ignore list + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(bundleTxs[0]) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal where first lane cannot fit txs but second lane can", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(tobLane.Insert(sdk.Context{}, tx)) + + // Set up the default lane with the bid tx and the bundled tx + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + // Even though this passes it should not include it in the proposal because it is in the ignore list + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, bundleTxs[0])) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).PrepareProposalHandler() + proposal := s.getTxBytes(tx, bundleTxs[0]) + size := int64(len(proposal[0]) - 1) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: size}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal[1:], resp.Txs) + }) + + s.Run("can build a proposal with single tx from middle lane", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), nil) + + freeTx, err := testutils.CreateFreeTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + "test", + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + freeTx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, freeTx)) + + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + freeTx: true, + }) + s.Require().NoError(freeLane.Insert(sdk.Context{}, freeTx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).PrepareProposalHandler() + + proposal := s.getTxBytes(freeTx) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("transaction from every lane", func() { + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:4], + ) + s.Require().NoError(err) + + // Create a normal tx + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Create a free tx + freeTx, err := testutils.CreateFreeTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + "test", + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + bundleTxs[1]: true, + bundleTxs[2]: true, + bundleTxs[3]: true, + }) + tobLane.Insert(sdk.Context{}, tx) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + normalTx: true, + }) + defaultLane.Insert(sdk.Context{}, normalTx) + + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + freeTx: true, + }) + freeLane.Insert(sdk.Context{}, freeTx) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).PrepareProposalHandler() + proposal := s.getTxBytes(tx, bundleTxs[0], bundleTxs[1], bundleTxs[2], bundleTxs[3], freeTx, normalTx) + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(7, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) } -func (suite *ABCITestSuite) TestProcessProposal() { - var ( - // mempool configuration - txs []sdk.Tx - auctionTxs []sdk.Tx - insertRefTxs = false +func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { + s.Run("can build a proposal if a lane panics first", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) - // auction configuration - maxBundleSize uint32 = 10 - reserveFee = sdk.NewCoin("stake", math.NewInt(1000)) - frontRunningProtection = true + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{panicLane, defaultLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if second lane panics", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{defaultLane, panicLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if multiple consecutive lanes panic", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + panicLane2 := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{panicLane, panicLane2, defaultLane}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) + + s.Run("can build a proposal if the last few lanes panic", func() { + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + panicLane2 := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.25")) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + []blockbuster.Lane{defaultLane, panicLane, panicLane2}, + ).PrepareProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + proposal := s.getTxBytes(tx) + s.Require().Equal(1, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs) + }) +} + +func (s *ProposalsTestSuite) TestProcessProposal() { + s.Run("can process a valid empty proposal", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{}) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).ProcessProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: nil}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, resp) + }) + + s.Run("rejects a proposal with bad txs", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{}) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, freeLane, defaultLane}).ProcessProposalHandler() + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{{0x01, 0x02, 0x03}}}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + + s.Run("rejects a proposal when a lane panics", func() { + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + panicLane := s.setUpPanicLane(math.LegacyMustNewDecFromStr("0.0")) + + txbz, err := testutils.CreateRandomTxBz( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + ) + s.Require().NoError(err) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, panicLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{txbz}}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + + s.Run("can process a invalid proposal (out of order)", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Create a random transaction that will be inserted into the default lane + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx: true}) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(tx, tx2)}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) + + s.Run("can process a invalid proposal where first lane is valid second is not", func() { + bidTx, bundle, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 1, + s.accounts[0:2], + ) + s.Require().NoError(err) + + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + normalTx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), + ) + s.Require().NoError(err) + + // Set up the default lane + defaultLane := s.setUpDefaultLane(math.LegacyMustNewDecFromStr("0.5"), nil) + defaultLane.SetProcessLaneHandler(blockbuster.NoOpProcessLaneHandler()) + + // Set up the TOB lane + tobLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), nil) + tobLane.SetProcessLaneHandler(blockbuster.NoOpProcessLaneHandler()) + + proposalHandler := s.setUpProposalHandlers([]blockbuster.Lane{tobLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(bidTx, bundle[0], bundle[1], normalTx, normalTx2)}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) +} + +func (s *ProposalsTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk.AnteHandler { + txCache := make(map[string]bool) + for tx, pass := range expectedExecution { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + txCache[hashStr] = pass + } + + anteHandler := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + + pass, found := txCache[hashStr] + if !found { + return ctx, fmt.Errorf("tx not found") + } + + if pass { + return ctx, nil + } + + return ctx, fmt.Errorf("tx failed") + } + + return anteHandler +} + +func (s *ProposalsTestSuite) setUpDefaultLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *base.DefaultLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return base.NewDefaultLane(cfg) +} + +func (s *ProposalsTestSuite) setUpTOBLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *auction.TOBLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return auction.NewTOBLane(cfg, auction.NewDefaultAuctionFactory(cfg.TxDecoder)) +} + +func (s *ProposalsTestSuite) setUpFreeLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *free.FreeLane { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + } + + return free.NewFreeLane(cfg, blockbuster.DefaultTxPriority(), free.DefaultMatchHandler()) +} + +func (s *ProposalsTestSuite) setUpPanicLane(maxBlockSpace math.LegacyDec) *blockbuster.LaneConstructor { + cfg := blockbuster.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + MaxBlockSpace: maxBlockSpace, + } + + lane := blockbuster.NewLaneConstructor( + cfg, + "panic", + blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), cfg.TxEncoder, 0), + blockbuster.DefaultMatchHandler(), ) - cases := []struct { - name string - malleate func() - response abcitypes.ResponseProcessProposal_ProposalStatus - }{ - { - "no normal tx, no tob tx", - func() { - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single default tx", - func() { - account := suite.accounts[0] - nonce := suite.nonces[account.Address.String()] - timeout := uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) + lane.SetPrepareLaneHandler(blockbuster.PanicPrepareLaneHandler()) + lane.SetProcessLaneHandler(blockbuster.PanicProcessLaneHandler()) - txs = []sdk.Tx{normalTx} - auctionTxs = []sdk.Tx{} - insertRefTxs = false - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single tob tx without bundled txs in proposal", - func() { - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = false - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single tob tx with bundled txs in proposal", - func() { - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[1], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "single invalid tob tx (front-running)", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "multiple tob txs in the proposal", - func() { - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a valid tob transaction - bidder = suite.accounts[1] - bid = sdk.NewCoin("stake", math.NewInt(1000)) - nonce = suite.nonces[bidder.Address.String()] - timeout = uint64(100) - signers = []testutils.Account{bidder} - bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - txs = []sdk.Tx{} - auctionTxs = []sdk.Tx{bidTx, bidTx2} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_REJECT, - }, - { - "single tob tx with front-running disabled and multiple other txs", - func() { - frontRunningProtection = false - - // Create an valid tob transaction - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(10000000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a few other transactions - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - timeout = uint64(100) - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - timeout = uint64(100) - numberMsgs = uint64(3) - normalTx2, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{normalTx, normalTx2} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "tob, free, and default tx", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{freeTx, normalTx} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - { - "tob, free, and default tx with default and free mixed", - func() { - // Create a tob tx - bidder := suite.accounts[0] - bid := sdk.NewCoin("stake", math.NewInt(1000)) - nonce := suite.nonces[bidder.Address.String()] - timeout := uint64(100) - signers := []testutils.Account{suite.accounts[2], bidder} - bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) - suite.Require().NoError(err) - - // Create a free tx - account := suite.accounts[1] - nonce = suite.nonces[account.Address.String()] - freeTx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, account, nonce, timeout, "val1", bid) - suite.Require().NoError(err) - - // Create a normal tx - account = suite.accounts[3] - nonce = suite.nonces[account.Address.String()] - numberMsgs := uint64(3) - normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) - suite.Require().NoError(err) - - txs = []sdk.Tx{normalTx, freeTx} - auctionTxs = []sdk.Tx{bidTx} - insertRefTxs = true - }, - abcitypes.ResponseProcessProposal_ACCEPT, - }, - } - - for _, tc := range cases { - suite.Run(tc.name, func() { - suite.SetupTest() // reset - tc.malleate() - - // Insert all of the transactions into the proposal - proposalTxs := make([][]byte, 0) - for _, tx := range auctionTxs { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, txBz) - - if insertRefTxs { - bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, bidInfo.Transactions...) - } - } - - for _, tx := range txs { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - suite.Require().NoError(err) - - proposalTxs = append(proposalTxs, txBz) - } - - // create a new auction - params := buildertypes.Params{ - MaxBundleSize: maxBundleSize, - ReserveFee: reserveFee, - FrontRunningProtection: frontRunningProtection, - } - suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool) - - handler := suite.proposalHandler.ProcessProposalHandler() - res, err := handler(suite.ctx, &abcitypes.RequestProcessProposal{ - Txs: proposalTxs, - }) - - // Check if the response is valid - suite.Require().Equal(tc.response, res.Status) - - if res.Status == abcitypes.ResponseProcessProposal_ACCEPT { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } + return lane +} + +func (s *ProposalsTestSuite) setUpProposalHandlers(lanes []blockbuster.Lane) *abci.ProposalHandler { + mempool := blockbuster.NewMempool(log.NewTestLogger(s.T()), true, lanes...) + + return abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + mempool.Registry(), + ) +} + +func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte { + txBytes := make([][]byte, len(txs)) + for i, tx := range txs { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + txBytes[i] = bz + } + return txBytes } diff --git a/blockbuster/lane.go b/blockbuster/lane.go deleted file mode 100644 index c0d9bcc..0000000 --- a/blockbuster/lane.go +++ /dev/null @@ -1,123 +0,0 @@ -package blockbuster - -import ( - "fmt" - - "cosmossdk.io/log" - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" -) - -type ( - // PrepareLanesHandler wraps all of the lanes Prepare function into a single chained - // function. You can think of it like an AnteHandler, but for preparing proposals in the - // context of lanes instead of modules. - PrepareLanesHandler func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) - - // ProcessLanesHandler wraps all of the lanes Process functions into a single chained - // function. You can think of it like an AnteHandler, but for processing proposals in the - // context of lanes instead of modules. - ProcessLanesHandler func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) - - // BaseLaneConfig defines the basic functionality needed for a lane. - BaseLaneConfig struct { - Logger log.Logger - TxEncoder sdk.TxEncoder - TxDecoder sdk.TxDecoder - AnteHandler sdk.AnteHandler - - // MaxBlockSpace defines the relative percentage of block space that can be - // used by this lane. NOTE: If this is set to zero, then there is no limit - // on the number of transactions that can be included in the block for this - // lane (up to maxTxBytes as provided by the request). This is useful for the default lane. - MaxBlockSpace math.LegacyDec - - // IgnoreList defines the list of lanes to ignore when processing transactions. This - // is useful for when you want lanes to exist after the default lane. For example, - // say there are two lanes: default and free. The free lane should be processed after - // the default lane. In this case, the free lane should be added to the ignore list - // of the default lane. Otherwise, the transactions that belong to the free lane - // will be processed by the default lane. - IgnoreList []Lane - } - - // Lane defines an interface used for block construction - Lane interface { - sdkmempool.Mempool - - // Name returns the name of the lane. - Name() string - - // Match determines if a transaction belongs to this lane. - Match(ctx sdk.Context, tx sdk.Tx) bool - - // VerifyTx verifies the transaction belonging to this lane. - VerifyTx(ctx sdk.Context, tx sdk.Tx) error - - // Contains returns true if the mempool/lane contains the given transaction. - Contains(tx sdk.Tx) bool - - // 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 BlockProposal, maxTxBytes int64, next PrepareLanesHandler) (BlockProposal, error) - - // ProcessLaneBasic validates that transactions belonging to this lane are not misplaced - // in the block proposal. - ProcessLaneBasic(ctx sdk.Context, txs []sdk.Tx) 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 []sdk.Tx, 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() math.LegacyDec - } -) - -// NewLaneConfig returns a new LaneConfig. This will be embedded in a lane. -func NewBaseLaneConfig( - logger log.Logger, - txEncoder sdk.TxEncoder, - txDecoder sdk.TxDecoder, - anteHandler sdk.AnteHandler, - maxBlockSpace math.LegacyDec, -) BaseLaneConfig { - return BaseLaneConfig{ - Logger: logger, - TxEncoder: txEncoder, - TxDecoder: txDecoder, - AnteHandler: anteHandler, - MaxBlockSpace: maxBlockSpace, - } -} - -// 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(math.LegacyOneDec()) { - return fmt.Errorf("max block space must be set to a value between 0 and 1") - } - - return nil -} diff --git a/blockbuster/lane_abci.go b/blockbuster/lane_abci.go new file mode 100644 index 0000000..a70b102 --- /dev/null +++ b/blockbuster/lane_abci.go @@ -0,0 +1,67 @@ +package blockbuster + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster/utils" +) + +// PrepareLane will prepare a partial proposal for the lane. It will select transactions from the +// lane respecting the selection logic of the prepareLaneHandler. It will then update the partial +// proposal with the selected transactions. If the proposal is unable to be updated, we return an +// error. The proposal will only be modified if it passes all of the invarient checks. +func (l *LaneConstructor) PrepareLane( + ctx sdk.Context, + proposal BlockProposal, + maxTxBytes int64, + next PrepareLanesHandler, +) (BlockProposal, error) { + txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes) + if err != nil { + return proposal, err + } + + // Remove all transactions that were invalid during the creation of the partial proposal. + if err := utils.RemoveTxsFromLane(txsToRemove, l); err != nil { + l.Logger().Error( + "failed to remove transactions from lane", + "lane", l.Name(), + "err", err, + ) + } + + // Update the proposal with the selected transactions. + if err := proposal.UpdateProposal(l, txs); err != nil { + return proposal, err + } + + return next(ctx, proposal) +} + +// CheckOrder checks that the ordering logic of the lane is respected given the set of transactions +// in the block proposal. If the ordering logic is not respected, we return an error. +func (l *LaneConstructor) CheckOrder(ctx sdk.Context, txs []sdk.Tx) error { + return l.checkOrderHandler(ctx, txs) +} + +// ProcessLane verifies that the transactions included in the block proposal are valid respecting +// the verification logic of the lane (processLaneHandler). If the transactions are valid, we +// return the transactions that do not belong to this lane to the next lane. If the transactions +// are invalid, we return an error. +func (l *LaneConstructor) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next ProcessLanesHandler) (sdk.Context, error) { + remainingTxs, err := l.processLaneHandler(ctx, txs) + if err != nil { + return ctx, err + } + + return next(ctx, remainingTxs) +} + +// AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of +// of the antehandler chain. +func (l *LaneConstructor) AnteVerifyTx(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + if l.cfg.AnteHandler != nil { + return l.cfg.AnteHandler(ctx, tx, simulate) + } + + return ctx, nil +} diff --git a/blockbuster/lane_constructor.go b/blockbuster/lane_constructor.go new file mode 100644 index 0000000..7107814 --- /dev/null +++ b/blockbuster/lane_constructor.go @@ -0,0 +1,198 @@ +package blockbuster + +import ( + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ Lane = (*LaneConstructor)(nil) + +// LaneConstructor is a generic implementation of a lane. It is meant to be used +// as a base for other lanes to be built on top of. It provides a default +// implementation of the MatchHandler, PrepareLaneHandler, ProcessLaneHandler, +// and CheckOrderHandler. To extend this lane, you must either utilize the default +// handlers or construct your own that you pass into the constructor/setters. +type LaneConstructor struct { + // cfg stores functionality required to encode/decode transactions, maintains how + // many transactions are allowed in this lane's mempool, and the amount of block + // space this lane is allowed to consume. + cfg LaneConfig + + // laneName is the name of the lane. + laneName string + + // LaneMempool is the mempool that is responsible for storing transactions + // that are waiting to be processed. + LaneMempool + + // matchHandler is the function that determines whether or not a transaction + // should be processed by this lane. + matchHandler MatchHandler + + // prepareLaneHandler is the function that is called when a new proposal is being + // requested and the lane needs to submit transactions it wants included in the block. + prepareLaneHandler PrepareLaneHandler + + // checkOrderHandler is the function that is called when a new proposal is being + // verified and the lane needs to verify that the transactions included in the proposal + // respect the ordering rules of the lane and does not interleave transactions from other lanes. + checkOrderHandler CheckOrderHandler + + // processLaneHandler is the function that is called when a new proposal is being + // verified and the lane needs to verify that the transactions included in the proposal + // are valid respecting the verification logic of the lane. + processLaneHandler ProcessLaneHandler +} + +// NewLaneConstructor returns a new lane constructor. When creating this lane, the type +// of the lane must be specified. The type of the lane is directly associated with the +// type of the mempool that is used to store transactions that are waiting to be processed. +func NewLaneConstructor( + cfg LaneConfig, + laneName string, + laneMempool LaneMempool, + matchHandlerFn MatchHandler, +) *LaneConstructor { + lane := &LaneConstructor{ + cfg: cfg, + laneName: laneName, + LaneMempool: laneMempool, + matchHandler: matchHandlerFn, + } + + if err := lane.ValidateBasic(); err != nil { + panic(err) + } + + return lane +} + +// ValidateBasic ensures that the lane was constructed properly. In the case that +// the lane was not constructed with proper handlers, default handlers are set. +func (l *LaneConstructor) ValidateBasic() error { + if err := l.cfg.ValidateBasic(); err != nil { + return err + } + + if l.laneName == "" { + return fmt.Errorf("lane name cannot be empty") + } + + if l.LaneMempool == nil { + return fmt.Errorf("lane mempool cannot be nil") + } + + if l.matchHandler == nil { + return fmt.Errorf("match handler cannot be nil") + } + + if l.prepareLaneHandler == nil { + l.prepareLaneHandler = l.DefaultPrepareLaneHandler() + } + + if l.processLaneHandler == nil { + l.processLaneHandler = l.DefaultProcessLaneHandler() + } + + if l.checkOrderHandler == nil { + l.checkOrderHandler = l.DefaultCheckOrderHandler() + } + + return nil +} + +// SetPrepareLaneHandler sets the prepare lane handler for the lane. This handler +// is called when a new proposal is being requested and the lane needs to submit +// transactions it wants included in the block. +func (l *LaneConstructor) SetPrepareLaneHandler(prepareLaneHandler PrepareLaneHandler) { + if prepareLaneHandler == nil { + panic("prepare lane handler cannot be nil") + } + + l.prepareLaneHandler = prepareLaneHandler +} + +// SetProcessLaneHandler sets the process lane handler for the lane. This handler +// is called when a new proposal is being verified and the lane needs to verify +// that the transactions included in the proposal are valid respecting the verification +// logic of the lane. +func (l *LaneConstructor) SetProcessLaneHandler(processLaneHandler ProcessLaneHandler) { + if processLaneHandler == nil { + panic("process lane handler cannot be nil") + } + + l.processLaneHandler = processLaneHandler +} + +// SetCheckOrderHandler sets the check order handler for the lane. This handler +// is called when a new proposal is being verified and the lane needs to verify +// that the transactions included in the proposal respect the ordering rules of +// the lane and does not include transactions from other lanes. +func (l *LaneConstructor) SetCheckOrderHandler(checkOrderHandler CheckOrderHandler) { + if checkOrderHandler == nil { + panic("check order handler cannot be nil") + } + + l.checkOrderHandler = checkOrderHandler +} + +// Match returns true if the transaction should be processed by this lane. This +// function first determines if the transaction matches the lane and then checks +// if the transaction is on the ignore list. If the transaction is on the ignore +// list, it returns false. +func (l *LaneConstructor) Match(ctx sdk.Context, tx sdk.Tx) bool { + return l.matchHandler(ctx, tx) && !l.CheckIgnoreList(ctx, tx) +} + +// CheckIgnoreList returns true if the transaction is on the ignore list. The ignore +// list is utilized to prevent transactions that should be considered in other lanes +// from being considered from this lane. +func (l *LaneConstructor) CheckIgnoreList(ctx sdk.Context, tx sdk.Tx) bool { + for _, lane := range l.cfg.IgnoreList { + if lane.Match(ctx, tx) { + return true + } + } + + return false +} + +// Name returns the name of the lane. +func (l *LaneConstructor) Name() string { + return l.laneName +} + +// SetIgnoreList sets the ignore list for the lane. The ignore list is a list +// of lanes that the lane should ignore when processing transactions. +func (l *LaneConstructor) SetIgnoreList(lanes []Lane) { + l.cfg.IgnoreList = lanes +} + +// SetAnteHandler sets the ante handler for the lane. +func (l *LaneConstructor) SetAnteHandler(anteHandler sdk.AnteHandler) { + l.cfg.AnteHandler = anteHandler +} + +// Logger returns the logger for the lane. +func (l *LaneConstructor) Logger() log.Logger { + return l.cfg.Logger +} + +// TxDecoder returns the tx decoder for the lane. +func (l *LaneConstructor) TxDecoder() sdk.TxDecoder { + return l.cfg.TxDecoder +} + +// TxEncoder returns the tx encoder for the lane. +func (l *LaneConstructor) TxEncoder() sdk.TxEncoder { + return l.cfg.TxEncoder +} + +// GetMaxBlockSpace returns the maximum amount of block space that the lane is +// allowed to consume as a percentage of the total block space. +func (l *LaneConstructor) GetMaxBlockSpace() math.LegacyDec { + return l.cfg.MaxBlockSpace +} diff --git a/blockbuster/lane_handlers.go b/blockbuster/lane_handlers.go new file mode 100644 index 0000000..0a87a5d --- /dev/null +++ b/blockbuster/lane_handlers.go @@ -0,0 +1,155 @@ +package blockbuster + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster/utils" +) + +// DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It +// selects all transactions in the mempool that are valid and not already in the partial +// proposal. It will continue to reap transactions until the maximum block space for this +// lane has been reached. Additionally, any transactions that are invalid will be returned. +func (l *LaneConstructor) DefaultPrepareLaneHandler() PrepareLaneHandler { + return func(ctx sdk.Context, proposal BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + var ( + totalSize int64 + txs [][]byte + txsToRemove []sdk.Tx + ) + + // Select all transactions in the mempool that are valid and not already in the + // partial proposal. + for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { + tx := iterator.Tx() + + txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx) + if err != nil { + l.Logger().Info("failed to get hash of tx", "err", err) + + txsToRemove = append(txsToRemove, tx) + continue + } + + // Double check that the transaction belongs to this lane. + if !l.Match(ctx, tx) { + l.Logger().Info( + "failed to select tx for lane; tx does not belong to lane", + "tx_hash", hash, + "lane", l.Name(), + ) + + txsToRemove = append(txsToRemove, tx) + continue + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(txBytes) { + l.Logger().Info( + "failed to select tx for lane; tx is already in proposal", + "tx_hash", hash, + "lane", l.Name(), + ) + + continue + } + + // If the transaction is too large, we break and do not attempt to include more txs. + txSize := int64(len(txBytes)) + if updatedSize := totalSize + txSize; updatedSize > maxTxBytes { + l.Logger().Info( + "tx bytes above the maximum allowed", + "lane", l.Name(), + "tx_size", txSize, + "total_size", totalSize, + "max_tx_bytes", maxTxBytes, + "tx_hash", hash, + ) + + break + } + + // Verify the transaction. + if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil { + l.Logger().Info( + "failed to verify tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tx) + continue + } + + totalSize += txSize + txs = append(txs, txBytes) + } + + return txs, txsToRemove, nil + } +} + +// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It +// verifies all transactions in the lane that matches to the lane. If any transaction +// fails to verify, the entire proposal is rejected. If the handler comes across a transaction +// that does not match the lane's matcher, it will return the remaining transactions in the +// proposal. +func (l *LaneConstructor) DefaultProcessLaneHandler() ProcessLaneHandler { + return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { + var err error + + // Process all transactions that match the lane's matcher. + for index, tx := range txs { + if l.Match(ctx, tx) { + if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil { + return nil, fmt.Errorf("failed to verify tx: %w", err) + } + } else { + return txs[index:], nil + } + } + + // This means we have processed all transactions in the proposal. + return nil, nil + } +} + +// DefaultCheckOrderHandler returns a default implementation of the CheckOrderHandler. It +// ensures the following invariants: +// +// 1. All transactions that belong to this lane respect the ordering logic defined by the +// lane. +// 2. Transactions that belong to other lanes cannot be interleaved with transactions that +// belong to this lane. +func (l *LaneConstructor) DefaultCheckOrderHandler() CheckOrderHandler { + return func(ctx sdk.Context, txs []sdk.Tx) error { + seenOtherLaneTx := false + + for index, tx := range txs { + if l.Match(ctx, tx) { + if seenOtherLaneTx { + return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name()) + } + + // If the transactions do not respect the priority defined by the mempool, we consider the proposal + // to be invalid + if index > 0 && l.Compare(ctx, txs[index-1], tx) == -1 { + return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1) + } + } else { + seenOtherLaneTx = true + } + } + + return nil + } +} + +// DefaultMatchHandler returns a default implementation of the MatchHandler. It matches all +// transactions. +func DefaultMatchHandler() MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + return true + } +} diff --git a/blockbuster/lane_interface.go b/blockbuster/lane_interface.go new file mode 100644 index 0000000..59ee35e --- /dev/null +++ b/blockbuster/lane_interface.go @@ -0,0 +1,71 @@ +package blockbuster + +import ( + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" +) + +// LaneMempool defines the interface a lane's mempool should implement. The basic API +// is the same as the sdk.Mempool, but it also includes a Compare function that is used +// to determine the relative priority of two transactions belonging in the same lane. +// +//go:generate mockery --name LaneMempool --output ./utils/mocks --outpkg mocks --case underscore +type LaneMempool interface { + sdkmempool.Mempool + + // Compare determines the relative priority of two transactions belonging in the same lane. Compare + // will return -1 if this transaction has a lower priority than the other transaction, 0 if they have + // the same priority, and 1 if this transaction has a higher priority than the other transaction. + Compare(ctx sdk.Context, this, other sdk.Tx) int + + // Contains returns true if the transaction is contained in the mempool. + Contains(tx sdk.Tx) bool +} + +// Lane defines an interface used for matching transactions to lanes, storing transactions, +// and constructing partial blocks. +// +//go:generate mockery --name Lane --output ./utils/mocks --outpkg mocks --case underscore +type Lane interface { + LaneMempool + + // 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 BlockProposal, + maxTxBytes int64, + next PrepareLanesHandler, + ) (BlockProposal, error) + + // CheckOrder validates that transactions belonging to this lane are not misplaced + // in the block proposal and respect the ordering rules of the lane. + CheckOrder(ctx sdk.Context, txs []sdk.Tx) 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 []sdk.Tx, next ProcessLanesHandler) (sdk.Context, error) + + // GetMaxBlockSpace returns the max block space for the lane as a relative percentage. + GetMaxBlockSpace() math.LegacyDec + + // Logger returns the lane's logger. + Logger() log.Logger + + // Name returns the name of the lane. + Name() string + + // SetAnteHandler sets the lane's antehandler. + SetAnteHandler(antehander sdk.AnteHandler) + + // SetIgnoreList sets the lanes that should be ignored by this lane. + SetIgnoreList(ignoreList []Lane) + + // Match determines if a transaction belongs to this lane. + Match(ctx sdk.Context, tx sdk.Tx) bool +} diff --git a/blockbuster/lane_mempool.go b/blockbuster/lane_mempool.go new file mode 100644 index 0000000..e102ee8 --- /dev/null +++ b/blockbuster/lane_mempool.go @@ -0,0 +1,159 @@ +package blockbuster + +import ( + "context" + "errors" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + "github.com/skip-mev/pob/blockbuster/utils" +) + +type ( + // ConstructorMempool defines a mempool that orders transactions based on the + // txPriority. The mempool is a wrapper on top of the SDK's Priority Nonce mempool. + // It include's additional helper functions that allow users to determine if a + // transaction is already in the mempool and to compare the priority of two + // transactions. + ConstructorMempool[C comparable] struct { + // index defines an index of transactions. + index sdkmempool.Mempool + + // txPriority defines the transaction priority function. It is used to + // retrieve the priority of a given transaction and to compare the priority + // of two transactions. The index utilizes this struct to order transactions + // in the mempool. + txPriority TxPriority[C] + + // txEncoder defines the sdk.Tx encoder that allows us to encode transactions + // to bytes. + txEncoder sdk.TxEncoder + + // txCache is a map of all transactions in the mempool. It is used + // to quickly check if a transaction is already in the mempool. + txCache map[string]struct{} + } +) + +// DefaultTxPriority returns a default implementation of the TxPriority. It prioritizes +// transactions by their fee. +func DefaultTxPriority() TxPriority[string] { + return TxPriority[string]{ + GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return "" + } + + return feeTx.GetFee().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: "", + } +} + +// NewConstructorMempool returns a new ConstructorMempool. +func NewConstructorMempool[C comparable](txPriority TxPriority[C], txEncoder sdk.TxEncoder, maxTx int) *ConstructorMempool[C] { + return &ConstructorMempool[C]{ + index: NewPriorityMempool( + PriorityNonceMempoolConfig[C]{ + TxPriority: txPriority, + MaxTx: maxTx, + }, + ), + txPriority: txPriority, + txEncoder: txEncoder, + txCache: make(map[string]struct{}), + } +} + +// Insert inserts a transaction into the mempool. +func (cm *ConstructorMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error { + if err := cm.index.Insert(ctx, tx); err != nil { + return fmt.Errorf("failed to insert tx into auction index: %w", err) + } + + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + cm.Remove(tx) + return err + } + + cm.txCache[txHashStr] = struct{}{} + + return nil +} + +// Remove removes a transaction from the mempool. +func (cm *ConstructorMempool[C]) Remove(tx sdk.Tx) error { + if err := cm.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { + return fmt.Errorf("failed to remove transaction from the mempool: %w", err) + } + + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + return fmt.Errorf("failed to get tx hash string: %w", err) + } + + delete(cm.txCache, txHashStr) + + return nil +} + +// Select returns an iterator of all transactions in the mempool. NOTE: If you +// remove a transaction from the mempool while iterating over the transactions, +// the iterator will not be aware of the removal and will continue to iterate +// over the removed transaction. Be sure to reset the iterator if you remove a transaction. +func (cm *ConstructorMempool[C]) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { + return cm.index.Select(ctx, txs) +} + +// CountTx returns the number of transactions in the mempool. +func (cm *ConstructorMempool[C]) CountTx() int { + return cm.index.CountTx() +} + +// Contains returns true if the transaction is contained in the mempool. +func (cm *ConstructorMempool[C]) Contains(tx sdk.Tx) bool { + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + return false + } + + _, ok := cm.txCache[txHashStr] + return ok +} + +// Compare determines the relative priority of two transactions belonging in the same lane. +func (cm *ConstructorMempool[C]) Compare(ctx sdk.Context, this sdk.Tx, other sdk.Tx) int { + firstPriority := cm.txPriority.GetTxPriority(ctx, this) + secondPriority := cm.txPriority.GetTxPriority(ctx, other) + return cm.txPriority.Compare(firstPriority, secondPriority) +} diff --git a/blockbuster/lanes/auction/abci.go b/blockbuster/lanes/auction/abci.go index 9a1f62b..0773c82 100644 --- a/blockbuster/lanes/auction/abci.go +++ b/blockbuster/lanes/auction/abci.go @@ -7,257 +7,247 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/blockbuster/utils" + "github.com/skip-mev/pob/x/builder/types" ) -// PrepareLane will attempt to select the highest bid transaction that is valid +// PrepareLaneHandler will attempt to select the highest bid transaction that is valid // and whose bundled transactions are valid and include them in the proposal. It -// will return an empty partial proposal if no valid bids are found. -func (l *TOBLane) PrepareLane( - ctx sdk.Context, - proposal blockbuster.BlockProposal, - maxTxBytes int64, - next blockbuster.PrepareLanesHandler, -) (blockbuster.BlockProposal, error) { - // Define all of the info we need to select transactions for the partial proposal. - var ( - txs [][]byte - txsToRemove = make(map[sdk.Tx]struct{}, 0) - ) +// will return no transactions if no valid bids are found. If any of the bids are invalid, +// it will return them and will only remove the bids and not the bundled transactions. +func (l *TOBLane) PrepareLaneHandler() blockbuster.PrepareLaneHandler { + return func(ctx sdk.Context, proposal blockbuster.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + // Define all of the info we need to select transactions for the partial proposal. + var ( + txs [][]byte + txsToRemove []sdk.Tx + ) - // Attempt to select the highest bid transaction that is valid and whose - // bundled transactions are valid. - bidTxIterator := l.Select(ctx, nil) -selectBidTxLoop: - for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { - cacheCtx, write := ctx.CacheContext() - tmpBidTx := bidTxIterator.Tx() + // Attempt to select the highest bid transaction that is valid and whose + // bundled transactions are valid. + bidTxIterator := l.Select(ctx, nil) + selectBidTxLoop: + for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { + cacheCtx, write := ctx.CacheContext() + tmpBidTx := bidTxIterator.Tx() - bidTxBz, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tmpBidTx) - if err != nil { - l.Logger().Info("failed to get hash of auction bid tx", "err", err) + bidTxBz, hash, err := utils.GetTxHashStr(l.TxEncoder(), tmpBidTx) + if err != nil { + l.Logger().Info("failed to get hash of auction bid tx", "err", err) - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(bidTxBz) { + l.Logger().Info( + "failed to select auction bid tx for lane; tx is already in proposal", + "tx_hash", hash, + ) + + continue selectBidTxLoop + } + + bidTxSize := int64(len(bidTxBz)) + if bidTxSize <= maxTxBytes { + // Build the partial proposal by selecting the bid transaction and all of + // its bundled transactions. + bidInfo, err := l.GetAuctionBidInfo(tmpBidTx) + if err != nil { + l.Logger().Info( + "failed to get auction bid info", + "tx_hash", hash, + "err", err, + ) + + // Some transactions in the bundle may be malformed or invalid, so we + // remove the bid transaction and try the next top bid. + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // Verify the bid transaction and all of its bundled transactions. + if err := l.VerifyTx(cacheCtx, tmpBidTx, bidInfo); err != nil { + l.Logger().Info( + "failed to verify auction bid tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal + bundledTxBz := make([][]byte, len(bidInfo.Transactions)) + for index, rawRefTx := range bidInfo.Transactions { + sdkTx, err := l.WrapBundleTransaction(rawRefTx) + if err != nil { + l.Logger().Info( + "failed to wrap bundled tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + sdkTxBz, _, err := utils.GetTxHashStr(l.TxEncoder(), sdkTx) + if err != nil { + l.Logger().Info( + "failed to get hash of bundled tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tmpBidTx) + continue selectBidTxLoop + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(sdkTxBz) { + l.Logger().Info( + "failed to select auction bid tx for lane; tx is already in proposal", + "tx_hash", hash, + ) + + continue selectBidTxLoop + } + + bundleTxBz := make([]byte, len(sdkTxBz)) + copy(bundleTxBz, sdkTxBz) + bundledTxBz[index] = sdkTxBz + } + + // At this point, both the bid transaction itself and all the bundled + // transactions are valid. So we select the bid transaction along with + // all the bundled transactions. We also mark these transactions as seen and + // update the total size selected thus far. + txs = append(txs, bidTxBz) + txs = append(txs, bundledTxBz...) + + // Write the cache context to the original context when we know we have a + // valid top of block bundle. + write() + + break selectBidTxLoop + } - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(bidTxBz) { l.Logger().Info( - "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, + "failed to select auction bid tx for lane; tx size is too large", + "tx_size", bidTxSize, + "max_size", maxTxBytes, ) - - continue selectBidTxLoop } - bidTxSize := int64(len(bidTxBz)) - if bidTxSize <= maxTxBytes { - // Verify the bid transaction and all of its bundled transactions. - if err := l.VerifyTx(cacheCtx, tmpBidTx); err != nil { - l.Logger().Info( - "failed to verify auction bid tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // Build the partial proposal by selecting the bid transaction and all of - // its bundled transactions. - bidInfo, err := l.GetAuctionBidInfo(tmpBidTx) - if bidInfo == nil || err != nil { - l.Logger().Info( - "failed to get auction bid info", - "tx_hash", hash, - "err", err, - ) - - // Some transactions in the bundle may be malformed or invalid, so we - // remove the bid transaction and try the next top bid. - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal - bundledTxBz := make([][]byte, len(bidInfo.Transactions)) - for index, rawRefTx := range bidInfo.Transactions { - sdkTx, err := l.WrapBundleTransaction(rawRefTx) - if err != nil { - l.Logger().Info( - "failed to wrap bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - sdkTxBz, _, err := utils.GetTxHashStr(l.Cfg.TxEncoder, sdkTx) - if err != nil { - l.Logger().Info( - "failed to get hash of bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tmpBidTx] = struct{}{} - continue selectBidTxLoop - } - - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(sdkTxBz) { - l.Logger().Info( - "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, - ) - - continue selectBidTxLoop - } - - bundleTxBz := make([]byte, len(sdkTxBz)) - copy(bundleTxBz, sdkTxBz) - bundledTxBz[index] = sdkTxBz - } - - // At this point, both the bid transaction itself and all the bundled - // transactions are valid. So we select the bid transaction along with - // all the bundled transactions. We also mark these transactions as seen and - // update the total size selected thus far. - txs = append(txs, bidTxBz) - txs = append(txs, bundledTxBz...) - - // Write the cache context to the original context when we know we have a - // valid top of block bundle. - write() - - break selectBidTxLoop - } - - l.Logger().Info( - "failed to select auction bid tx for lane; tx size is too large", - "tx_size", bidTxSize, - "max_size", maxTxBytes, - ) + return txs, txsToRemove, nil } - - // Remove all transactions that were invalid during the creation of the partial proposal. - if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { - l.Logger().Error( - "failed to remove transactions from lane", - "lane", l.Name(), - "err", err, - ) - - return proposal, err - } - - // Update the proposal with the selected transactions. This will only return an error - // if the invarient checks are not passed. In the case when this errors, the original proposal - // will be returned (without the selected transactions from this lane). - if err := proposal.UpdateProposal(l, txs); err != nil { - return proposal, err - } - - return next(ctx, proposal) } -// ProcessLane will ensure that block proposals that include transactions from +// ProcessLaneHandler will ensure that block proposals that include transactions from // the top-of-block auction lane are valid. -func (l *TOBLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next blockbuster.ProcessLanesHandler) (sdk.Context, error) { - if len(txs) == 0 { - return next(ctx, txs) - } +func (l *TOBLane) ProcessLaneHandler() blockbuster.ProcessLaneHandler { + return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { + if len(txs) == 0 { + return txs, nil + } - bidTx := txs[0] - if !l.Match(ctx, bidTx) { - return next(ctx, txs) - } + bidTx := txs[0] + if !l.Match(ctx, bidTx) { + return txs, nil + } - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return ctx, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) - } + bidInfo, err := l.GetAuctionBidInfo(bidTx) + if err != nil { + return nil, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + } - if err := l.VerifyTx(ctx, bidTx); err != nil { - return ctx, fmt.Errorf("invalid bid tx: %w", err) - } + if err := l.VerifyTx(ctx, bidTx, bidInfo); err != nil { + return nil, fmt.Errorf("invalid bid tx: %w", err) + } - return next(ctx, txs[len(bidInfo.Transactions)+1:]) + return txs[len(bidInfo.Transactions)+1:], nil + } } -// ProcessLaneBasic ensures that if a bid transaction is present in a proposal, +// CheckOrderHandler ensures that if a bid transaction is present in a proposal, // - it is the first transaction in the partial proposal // - all of the bundled transactions are included after the bid transaction in the order // they were included in the bid transaction. // - there are no other bid transactions in the proposal -func (l *TOBLane) ProcessLaneBasic(ctx sdk.Context, txs []sdk.Tx) error { - if len(txs) == 0 { - return nil - } +// - transactions from other lanes are not interleaved with transactions from the bid +// transaction. +func (l *TOBLane) CheckOrderHandler() blockbuster.CheckOrderHandler { + return func(ctx sdk.Context, txs []sdk.Tx) error { + if len(txs) == 0 { + return nil + } - bidTx := txs[0] + bidTx := txs[0] - // If there is a bid transaction, it must be the first transaction in the block proposal. - if !l.Match(ctx, bidTx) { - for _, tx := range txs[1:] { + // If there is a bid transaction, it must be the first transaction in the block proposal. + if !l.Match(ctx, bidTx) { + for _, tx := range txs[1:] { + if l.Match(ctx, tx) { + return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) + } + } + + return nil + } + + bidInfo, err := l.GetAuctionBidInfo(bidTx) + if err != nil { + return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + } + + if len(txs) < len(bidInfo.Transactions)+1 { + return fmt.Errorf( + "invalid number of transactions in lane %s; expected at least %d, got %d", + l.Name(), + len(bidInfo.Transactions)+1, + len(txs), + ) + } + + // Ensure that the order of transactions in the bundle is preserved. + for i, bundleTx := range txs[1 : len(bidInfo.Transactions)+1] { + if l.Match(ctx, bundleTx) { + return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) + } + + txBz, err := l.TxEncoder()(bundleTx) + if err != nil { + return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) + } + + if !bytes.Equal(txBz, bidInfo.Transactions[i]) { + return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) + } + } + + // Ensure that there are no more bid transactions in the block proposal. + for _, tx := range txs[len(bidInfo.Transactions)+1:] { if l.Match(ctx, tx) { - return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) + return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) } } return nil } - - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) - } - - if len(txs) < len(bidInfo.Transactions)+1 { - return fmt.Errorf("invalid number of transactions in lane %s; expected at least %d, got %d", l.Name(), len(bidInfo.Transactions)+1, len(txs)) - } - - // Ensure that the order of transactions in the bundle is preserved. - for i, bundleTx := range txs[1 : len(bidInfo.Transactions)+1] { - if l.Match(ctx, bundleTx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) - } - - txBz, err := l.Cfg.TxEncoder(bundleTx) - if err != nil { - return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) - } - - if !bytes.Equal(txBz, bidInfo.Transactions[i]) { - return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) - } - } - - // Ensure that there are no more bid transactions in the block proposal. - for _, tx := range txs[len(bidInfo.Transactions)+1:] { - if l.Match(ctx, tx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) - } - } - - return nil } // VerifyTx will verify that the bid transaction and all of its bundled // transactions are valid. It will return an error if any of the transactions // are invalid. -func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error { - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return fmt.Errorf("failed to get auction bid info: %w", err) +func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *types.BidInfo) (err error) { + if bidInfo == nil { + return fmt.Errorf("bid info is nil") } // verify the top-level bid transaction - ctx, err = l.verifyTx(ctx, bidTx) - if err != nil { + if ctx, err = l.AnteVerifyTx(ctx, bidTx, false); err != nil { return fmt.Errorf("invalid bid tx; failed to execute ante handler: %w", err) } @@ -268,27 +258,10 @@ func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error { return fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) } - // bid txs cannot be included in bundled txs - bidInfo, _ := l.GetAuctionBidInfo(bundledTx) - if bidInfo != nil { - return fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx") - } - - if ctx, err = l.verifyTx(ctx, bundledTx); err != nil { + if ctx, err = l.AnteVerifyTx(ctx, bundledTx, false); err != nil { return fmt.Errorf("invalid bid tx; failed to execute bundled transaction: %w", err) } } return nil } - -// verifyTx will execute the ante handler on the transaction and return the -// resulting context and error. -func (l *TOBLane) verifyTx(ctx sdk.Context, tx sdk.Tx) (sdk.Context, error) { - if l.Cfg.AnteHandler != nil { - newCtx, err := l.Cfg.AnteHandler(ctx, tx, false) - return newCtx, err - } - - return ctx, nil -} diff --git a/blockbuster/lanes/auction/auction_test.go b/blockbuster/lanes/auction/auction_test.go index 7e2eec4..ffe71ae 100644 --- a/blockbuster/lanes/auction/auction_test.go +++ b/blockbuster/lanes/auction/auction_test.go @@ -18,7 +18,6 @@ type IntegrationTestSuite struct { encCfg testutils.EncodingConfig config auction.Factory - mempool auction.Mempool ctx sdk.Context random *rand.Rand accounts []testutils.Account @@ -33,7 +32,6 @@ func (suite *IntegrationTestSuite) SetupTest() { // Mempool setup suite.encCfg = testutils.CreateTestEncodingConfig() suite.config = auction.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder()) - suite.mempool = auction.NewMempool(suite.encCfg.TxConfig.TxEncoder(), 0, suite.config) suite.ctx = sdk.NewContext(nil, cmtproto.Header{}, false, log.NewTestLogger(suite.T())) // Init accounts diff --git a/blockbuster/abci/check_tx.go b/blockbuster/lanes/auction/check_tx.go similarity index 92% rename from blockbuster/abci/check_tx.go rename to blockbuster/lanes/auction/check_tx.go index e9deaf9..089ee99 100644 --- a/blockbuster/abci/check_tx.go +++ b/blockbuster/lanes/auction/check_tx.go @@ -1,7 +1,6 @@ -package abci +package auction import ( - "context" "fmt" log "cosmossdk.io/log" @@ -29,7 +28,7 @@ type ( // TOBLane is utilized to retrieve the bid info of a transaction and to // insert a bid transaction into the application-side mempool. - tobLane TOBLane + tobLane TOBLaneI // anteHandler is utilized to verify the bid transaction against the latest // committed state. @@ -40,20 +39,6 @@ type ( // transaction. CheckTx func(req *cometabci.RequestCheckTx) (*cometabci.ResponseCheckTx, error) - // 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) (*types.BidInfo, error) - - // Insert is utilized to insert a transaction into the application-side mempool. - Insert(ctx context.Context, tx sdk.Tx) error - - // WrapBundleTransaction is utilized to wrap a transaction included in a bid transaction - // into an sdk.Tx. - WrapBundleTransaction(tx []byte) (sdk.Tx, error) - } - // BaseApp is an interface that allows us to call baseapp's CheckTx method // as well as retrieve the latest committed state. BaseApp interface { @@ -82,7 +67,7 @@ type ( func NewCheckTxHandler( baseApp BaseApp, txDecoder sdk.TxDecoder, - tobLane TOBLane, + tobLane TOBLaneI, anteHandler sdk.AnteHandler, ) *CheckTxHandler { return &CheckTxHandler{ diff --git a/blockbuster/lanes/auction/factory.go b/blockbuster/lanes/auction/factory.go index 37c9738..6032aa9 100644 --- a/blockbuster/lanes/auction/factory.go +++ b/blockbuster/lanes/auction/factory.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/skip-mev/pob/blockbuster" "github.com/skip-mev/pob/x/builder/types" ) @@ -21,6 +22,9 @@ type ( // GetAuctionBidInfo defines a function that returns the bid info from an auction transaction. GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) + + // MatchHandler defines a function that checks if a transaction matches the auction lane. + MatchHandler() blockbuster.MatchHandler } // DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions. @@ -91,6 +95,13 @@ func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*types.BidInf }, nil } +func (config *DefaultAuctionFactory) MatchHandler() blockbuster.MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + bidInfo, err := config.GetAuctionBidInfo(tx) + return bidInfo != nil && err == nil + } +} + // getBundleSigners defines a default function that returns the signers of all transactions in // a bundle. In the default case, each bundle transaction will be an sdk.Tx and the // signers are the signers of each sdk.Msg in the transaction. diff --git a/blockbuster/lanes/auction/lane.go b/blockbuster/lanes/auction/lane.go index 15be52d..14d7066 100644 --- a/blockbuster/lanes/auction/lane.go +++ b/blockbuster/lanes/auction/lane.go @@ -1,9 +1,10 @@ package auction import ( + "context" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/lanes/base" ) const ( @@ -11,10 +12,7 @@ const ( LaneName = "top-of-block" ) -var ( - _ blockbuster.Lane = (*TOBLane)(nil) - _ Factory = (*TOBLane)(nil) -) +var _ TOBLaneI = (*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. @@ -22,43 +20,53 @@ var ( // their bid price. The highest valid bid transaction is selected for inclusion in the // next block. The bundled transactions of the selected bid transaction are also // included in the next block. -type TOBLane struct { - // Mempool defines the mempool for the lane. - Mempool +type ( + // TOBLaneI defines the interface for the top-of-block auction lane. This interface + // is utilized by both the x/builder module and the checkTx handler. + TOBLaneI interface { + blockbuster.Lane + Factory + GetTopAuctionTx(ctx context.Context) sdk.Tx + } - // LaneConfig defines the base lane configuration. - *base.DefaultLane + TOBLane struct { + // LaneConfig defines the base lane configuration. + *blockbuster.LaneConstructor - // Factory defines the API/functionality which is responsible for determining - // if a transaction is a bid transaction and how to extract relevant - // information from the transaction (bid, timeout, bidder, etc.). - Factory -} + // Factory defines the API/functionality which is responsible for determining + // if a transaction is a bid transaction and how to extract relevant + // information from the transaction (bid, timeout, bidder, etc.). + Factory + } +) // NewTOBLane returns a new TOB lane. func NewTOBLane( - cfg blockbuster.BaseLaneConfig, - maxTx int, - af Factory, + cfg blockbuster.LaneConfig, + factory Factory, ) *TOBLane { - if err := cfg.ValidateBasic(); err != nil { - panic(err) + lane := &TOBLane{ + LaneConstructor: blockbuster.NewLaneConstructor( + cfg, + LaneName, + blockbuster.NewConstructorMempool[string]( + TxPriority(factory), + cfg.TxEncoder, + cfg.MaxTxs, + ), + factory.MatchHandler(), + ), + Factory: factory, } - return &TOBLane{ - Mempool: NewMempool(cfg.TxEncoder, maxTx, af), - DefaultLane: base.NewDefaultLane(cfg).WithName(LaneName), - Factory: af, - } -} - -// Match returns true if the transaction is a bid transaction. This is determined -// by the AuctionFactory. -func (l *TOBLane) Match(ctx sdk.Context, tx sdk.Tx) bool { - if l.MatchIgnoreList(ctx, tx) { - return false - } - - bidInfo, err := l.GetAuctionBidInfo(tx) - return bidInfo != nil && err == nil + // Set the prepare lane handler to the TOB one + lane.SetPrepareLaneHandler(lane.PrepareLaneHandler()) + + // Set the process lane handler to the TOB one + lane.SetProcessLaneHandler(lane.ProcessLaneHandler()) + + // Set the check order handler to the TOB one + lane.SetCheckOrderHandler(lane.CheckOrderHandler()) + + return lane } diff --git a/blockbuster/lanes/auction/mempool.go b/blockbuster/lanes/auction/mempool.go index 8c71798..68a3b81 100644 --- a/blockbuster/lanes/auction/mempool.go +++ b/blockbuster/lanes/auction/mempool.go @@ -2,48 +2,9 @@ package auction import ( "context" - "errors" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/utils" -) - -var _ Mempool = (*TOBMempool)(nil) - -type ( - // Mempool defines the interface of the auction mempool. - Mempool interface { - sdkmempool.Mempool - - // GetTopAuctionTx returns the highest bidding transaction in the auction mempool. - GetTopAuctionTx(ctx context.Context) sdk.Tx - - // Contains returns true if the transaction is contained in the mempool. - Contains(tx sdk.Tx) bool - } - - // TOBMempool defines an auction mempool. It can be seen as an extension of - // an SDK PriorityNonceMempool, i.e. a mempool that supports - // two-dimensional priority ordering, with the additional support of prioritizing - // and indexing auction bids. - TOBMempool struct { - // index defines an index of auction bids. - index sdkmempool.Mempool - - // txEncoder defines the sdk.Tx encoder that allows us to encode transactions - // to bytes. - txEncoder sdk.TxEncoder - - // txIndex is a map of all transactions in the mempool. It is used - // to quickly check if a transaction is already in the mempool. - txIndex map[string]struct{} - - // Factory implements the functionality required to process auction transactions. - Factory - } ) // TxPriority returns a TxPriority over auction bid transactions only. It @@ -89,78 +50,13 @@ func TxPriority(config Factory) blockbuster.TxPriority[string] { } } -// NewMempool returns a new auction mempool. -func NewMempool(txEncoder sdk.TxEncoder, maxTx int, config Factory) *TOBMempool { - return &TOBMempool{ - index: blockbuster.NewPriorityMempool( - blockbuster.PriorityNonceMempoolConfig[string]{ - TxPriority: TxPriority(config), - MaxTx: maxTx, - }, - ), - txEncoder: txEncoder, - txIndex: make(map[string]struct{}), - Factory: config, - } -} - -// Insert inserts a transaction into the auction mempool. -func (am *TOBMempool) Insert(ctx context.Context, tx sdk.Tx) error { - if err := am.index.Insert(ctx, tx); err != nil { - return fmt.Errorf("failed to insert tx into auction index: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return err - } - - am.txIndex[txHashStr] = struct{}{} - - return nil -} - -// Remove removes a transaction from the mempool based. -func (am *TOBMempool) Remove(tx sdk.Tx) error { - if err := am.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - return fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return fmt.Errorf("failed to get tx hash string: %w", err) - } - - delete(am.txIndex, txHashStr) - - return nil -} - // GetTopAuctionTx returns the highest bidding transaction in the auction mempool. -func (am *TOBMempool) GetTopAuctionTx(ctx context.Context) sdk.Tx { - iterator := am.index.Select(ctx, nil) +// This is primarily a helper function for the x/builder module. +func (l *TOBLane) GetTopAuctionTx(ctx context.Context) sdk.Tx { + iterator := l.Select(ctx, nil) if iterator == nil { return nil } return iterator.Tx() } - -func (am *TOBMempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { - return am.index.Select(ctx, txs) -} - -func (am *TOBMempool) CountTx() int { - return am.index.CountTx() -} - -// Contains returns true if the transaction is contained in the mempool. -func (am *TOBMempool) Contains(tx sdk.Tx) bool { - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return false - } - - _, ok := am.txIndex[txHashStr] - return ok -} diff --git a/blockbuster/lanes/base/abci.go b/blockbuster/lanes/base/abci.go deleted file mode 100644 index b8ada51..0000000 --- a/blockbuster/lanes/base/abci.go +++ /dev/null @@ -1,142 +0,0 @@ -package base - -import ( - "fmt" - - 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 default lane. It will select and include -// all valid transactions in the mempool that are not already in the partial proposal. -// The default lane orders transactions by the sdk.Context priority. -func (l *DefaultLane) PrepareLane( - ctx sdk.Context, - proposal blockbuster.BlockProposal, - maxTxBytes int64, - next blockbuster.PrepareLanesHandler, -) (blockbuster.BlockProposal, error) { - // Define all of the info we need to select transactions for the partial proposal. - var ( - 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, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tx) - if err != nil { - l.Logger().Info("failed to get hash of tx", "err", err) - - txsToRemove[tx] = struct{}{} - continue - } - - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(txBytes) { - l.Logger().Info( - "failed to select tx for lane; tx is already in proposal", - "tx_hash", hash, - "lane", l.Name(), - ) - - continue - } - - // If the transaction is too large, we break and do not attempt to include more txs. - txSize := int64(len(txBytes)) - if updatedSize := totalSize + txSize; updatedSize > maxTxBytes { - break - } - - // Verify the transaction. - if err := l.VerifyTx(ctx, tx); err != nil { - l.Logger().Info( - "failed to verify tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove[tx] = struct{}{} - continue - } - - totalSize += txSize - txs = append(txs, txBytes) - } - - // Remove all transactions that were invalid during the creation of the partial proposal. - if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil { - l.Logger().Error( - "failed to remove transactions from lane", - "err", err, - ) - - return proposal, err - } - - // Update the partial proposal with the selected transactions. If the proposal is unable to - // be updated, we return an error. The proposal will only be modified if it passes all - // of the invarient checks. - if err := proposal.UpdateProposal(l, txs); err != nil { - return proposal, err - } - - return next(ctx, proposal) -} - -// ProcessLane verifies the default lane's portion of a block proposal. Since the default lane's -// ProcessLaneBasic function ensures that all of the default transactions are in the correct order, -// we only need to verify the contiguous set of transactions that match to the default lane. -func (l *DefaultLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next blockbuster.ProcessLanesHandler) (sdk.Context, error) { - for index, tx := range txs { - if l.Match(ctx, tx) { - if err := l.VerifyTx(ctx, tx); err != nil { - return ctx, fmt.Errorf("failed to verify tx: %w", err) - } - } else { - return next(ctx, txs[index:]) - } - } - - // This means we have processed all transactions in the proposal. - return ctx, nil -} - -// transactions that belong to this lane are not misplaced in the block proposal i.e. -// the proposal only contains contiguous transactions that belong to this lane - there -// can be no interleaving of transactions from other lanes. -func (l *DefaultLane) ProcessLaneBasic(ctx sdk.Context, txs []sdk.Tx) error { - seenOtherLaneTx := false - lastSeenIndex := 0 - - for _, tx := range txs { - if l.Match(ctx, 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) - return err - } - - return nil -} diff --git a/blockbuster/lanes/base/abci_test.go b/blockbuster/lanes/base/abci_test.go new file mode 100644 index 0000000..d5c054d --- /dev/null +++ b/blockbuster/lanes/base/abci_test.go @@ -0,0 +1,547 @@ +package base_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/base" + "github.com/skip-mev/pob/blockbuster/utils/mocks" + testutils "github.com/skip-mev/pob/testutils" +) + +func (s *BaseTestSuite) TestPrepareLane() { + s.Run("should not build a proposal when amount configured to lane is too small", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + // Create a proposal + maxTxBytes := int64(len(txBz) - 1) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + }) + + s.Run("should not build a proposal when box space configured to lane is too small", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("0.000001"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + // Create a proposal + maxTxBytes := int64(len(txBz)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().Error(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + }) + + s.Run("should be able to build a proposal with a tx that just fits in", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + // Create a proposal + maxTxBytes := int64(len(txBz)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is not empty and contains the transaction + s.Require().Equal(1, proposal.GetNumTxs()) + s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) + s.Require().Equal(txBz, proposal.GetTxs()[0]) + }) + + s.Run("should not build a proposal with a that fails verify tx", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx: false, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + // Create a proposal + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + maxTxBytes := int64(len(txBz)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + + // Ensure the transaction is removed from the lane + s.Require().False(lane.Contains(tx)) + s.Require().Equal(0, lane.CountTx()) + }) + + s.Run("should order transactions correctly in the proposal", func() { + // Create a basic transaction that should not in the proposal + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx1: true, + tx2: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(lane.Insert(sdk.Context{}, tx2)) + + txBz1, err := s.encodingConfig.TxConfig.TxEncoder()(tx1) + s.Require().NoError(err) + + txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) + s.Require().NoError(err) + + maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is ordered correctly + s.Require().Equal(2, proposal.GetNumTxs()) + s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) + s.Require().Equal([][]byte{txBz1, txBz2}, proposal.GetTxs()) + }) + + s.Run("should order transactions correctly in the proposal (with different insertion)", func() { + // Create a basic transaction that should not in the proposal + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx1: true, + tx2: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(lane.Insert(sdk.Context{}, tx2)) + + txBz1, err := s.encodingConfig.TxConfig.TxEncoder()(tx1) + s.Require().NoError(err) + + txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) + s.Require().NoError(err) + + maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is ordered correctly + s.Require().Equal(2, proposal.GetNumTxs()) + s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) + s.Require().Equal([][]byte{txBz2, txBz1}, proposal.GetTxs()) + }) + + s.Run("should include tx that fits in proposal when other does not", func() { + // Create a basic transaction that should not in the proposal + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 10, // This tx is too large to fit in the proposal + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx1: true, + tx2: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}.WithPriority(10), tx1)) + s.Require().NoError(lane.Insert(sdk.Context{}.WithPriority(5), tx2)) + + txBz1, err := s.encodingConfig.TxConfig.TxEncoder()(tx1) + s.Require().NoError(err) + + txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) + s.Require().NoError(err) + + maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) - 1 + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is ordered correctly + s.Require().Equal(1, proposal.GetNumTxs()) + s.Require().Equal(int64(len(txBz1)), proposal.GetTotalTxBytes()) + s.Require().Equal([][]byte{txBz1}, proposal.GetTxs()) + }) +} + +func (s *BaseTestSuite) TestProcessLane() { + s.Run("should accept a proposal with valid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.NoOpProcessLanesHandler()) + s.Require().NoError(err) + }) + + s.Run("should not accept a proposal with invalid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: false, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal with some invalid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + tx3, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + tx3, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: false, + tx3: true, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) +} + +func (s *BaseTestSuite) TestCheckOrder() { + s.Run("should accept proposal with transactions in correct order", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + s.Require().NoError(lane.CheckOrder(sdk.Context{}, proposal)) + }) + + s.Run("should not accept a proposal with transactions that are not in the correct order", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + }) + + s.Run("should not accept a proposal where transactions are out of order relative to other lanes", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 2, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + mocklane := mocks.NewLane(s.T()) + mocklane.On("Match", sdk.Context{}, tx1).Return(true) + mocklane.On("Match", sdk.Context{}, tx2).Return(false) + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), nil) + lane.SetIgnoreList([]blockbuster.Lane{mocklane}) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + }) +} + +func (s *BaseTestSuite) initLane( + maxBlockSpace math.LegacyDec, + expectedExecution map[sdk.Tx]bool, +) *base.DefaultLane { + config := blockbuster.NewBaseLaneConfig( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + s.encodingConfig.TxConfig.TxDecoder(), + s.setUpAnteHandler(expectedExecution), + maxBlockSpace, + ) + + return base.NewDefaultLane(config) +} + +func (s *BaseTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk.AnteHandler { + txCache := make(map[string]bool) + for tx, pass := range expectedExecution { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + txCache[hashStr] = pass + } + + anteHandler := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + + pass, found := txCache[hashStr] + if !found { + return ctx, fmt.Errorf("tx not found") + } + + if pass { + return ctx, nil + } + + return ctx, fmt.Errorf("tx failed") + } + + return anteHandler +} diff --git a/blockbuster/lanes/base/base_test.go b/blockbuster/lanes/base/base_test.go new file mode 100644 index 0000000..7852dac --- /dev/null +++ b/blockbuster/lanes/base/base_test.go @@ -0,0 +1,32 @@ +package base_test + +import ( + "math/rand" + "testing" + + testutils "github.com/skip-mev/pob/testutils" + "github.com/stretchr/testify/suite" +) + +type BaseTestSuite struct { + suite.Suite + + encodingConfig testutils.EncodingConfig + random *rand.Rand + accounts []testutils.Account + gasTokenDenom string +} + +func TestBaseTestSuite(t *testing.T) { + suite.Run(t, new(BaseTestSuite)) +} + +func (s *BaseTestSuite) SetupTest() { + // Set up basic TX encoding config. + s.encodingConfig = testutils.CreateTestEncodingConfig() + + // Create a few random accounts + s.random = rand.New(rand.NewSource(1)) + s.accounts = testutils.RandomAccounts(s.random, 5) + s.gasTokenDenom = "stake" +} diff --git a/blockbuster/lanes/base/lane.go b/blockbuster/lanes/base/lane.go index a1c4b9b..cb997ae 100644 --- a/blockbuster/lanes/base/lane.go +++ b/blockbuster/lanes/base/lane.go @@ -1,9 +1,6 @@ package base import ( - "cosmossdk.io/log" - "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/pob/blockbuster" ) @@ -15,83 +12,29 @@ const ( var _ blockbuster.Lane = (*DefaultLane)(nil) // DefaultLane defines a default lane implementation. The default lane orders -// transactions by the sdk.Context priority. The default lane will accept any -// transaction that is not a part of the lane's IgnoreList. By default, the IgnoreList -// is empty and the default lane will accept any transaction. The default lane on its -// own implements the same functionality as the pre v0.47.0 tendermint mempool and proposal -// handlers. +// transactions by the transaction fees. The default lane accepts any transaction +// that is should not be ignored (as defined by the IgnoreList in the LaneConfig). +// The default lane builds and verifies blocks in a similar fashion to how the +// CometBFT/Tendermint consensus engine builds and verifies blocks pre SDK version +// 0.47.0. type DefaultLane struct { - // Mempool defines the mempool for the lane. - Mempool - - // LaneConfig defines the base lane configuration. - Cfg blockbuster.BaseLaneConfig - - // Name defines the name of the lane. - laneName string + *blockbuster.LaneConstructor } // NewDefaultLane returns a new default lane. -func NewDefaultLane(cfg blockbuster.BaseLaneConfig) *DefaultLane { - if err := cfg.ValidateBasic(); err != nil { - panic(err) +func NewDefaultLane(cfg blockbuster.LaneConfig) *DefaultLane { + lane := blockbuster.NewLaneConstructor( + cfg, + LaneName, + blockbuster.NewConstructorMempool[string]( + blockbuster.DefaultTxPriority(), + cfg.TxEncoder, + cfg.MaxTxs, + ), + blockbuster.DefaultMatchHandler(), + ) + + return &DefaultLane{ + LaneConstructor: lane, } - - lane := &DefaultLane{ - Mempool: NewDefaultMempool(cfg.TxEncoder), - Cfg: cfg, - laneName: LaneName, - } - - return lane -} - -// WithName returns a lane option that sets the lane's name. -func (l *DefaultLane) WithName(name string) *DefaultLane { - l.laneName = name - return l -} - -// Match returns true if the transaction belongs to this lane. Since -// this is the default lane, it always returns true except for transactions -// that belong to lanes in the ignore list. -func (l *DefaultLane) Match(ctx sdk.Context, tx sdk.Tx) bool { - return !l.MatchIgnoreList(ctx, tx) -} - -// MatchIgnoreList returns true if any of the lanes that are in the ignore list -// match the current transaction. -func (l *DefaultLane) MatchIgnoreList(ctx sdk.Context, tx sdk.Tx) bool { - for _, lane := range l.Cfg.IgnoreList { - if lane.Match(ctx, tx) { - return true - } - } - - return false -} - -// Name returns the name of the lane. -func (l *DefaultLane) Name() string { - return l.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() math.LegacyDec { - return l.Cfg.MaxBlockSpace -} - -// GetIgnoreList returns the lane's ignore list. -func (l *DefaultLane) GetIgnoreList() []blockbuster.Lane { - return l.Cfg.IgnoreList } diff --git a/blockbuster/lanes/base/mempool.go b/blockbuster/lanes/base/mempool.go deleted file mode 100644 index 54b2388..0000000 --- a/blockbuster/lanes/base/mempool.go +++ /dev/null @@ -1,105 +0,0 @@ -package base - -import ( - "context" - "errors" - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" - "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/utils" -) - -var _ sdkmempool.Mempool = (*DefaultMempool)(nil) - -type ( - // Mempool defines the interface of the default mempool. - Mempool interface { - sdkmempool.Mempool - - // Contains returns true if the transaction is contained in the mempool. - Contains(tx sdk.Tx) bool - } - - // DefaultMempool defines the most basic 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. - DefaultMempool struct { - // index defines an index transactions. - index sdkmempool.Mempool - - // txEncoder defines the sdk.Tx encoder that allows us to encode transactions - // to bytes. - txEncoder sdk.TxEncoder - - // txIndex is a map of all transactions in the mempool. It is used - // to quickly check if a transaction is already in the mempool. - txIndex map[string]struct{} - } -) - -// NewDefaultMempool returns a new default mempool instance. The default mempool -// orders transactions by the sdk.Context priority. -func NewDefaultMempool(txEncoder sdk.TxEncoder) *DefaultMempool { - return &DefaultMempool{ - index: blockbuster.NewPriorityMempool( - blockbuster.DefaultPriorityNonceMempoolConfig(), - ), - txEncoder: txEncoder, - txIndex: make(map[string]struct{}), - } -} - -// Insert inserts a transaction into the mempool based on the transaction type (normal or auction). -func (am *DefaultMempool) Insert(ctx context.Context, tx sdk.Tx) error { - if err := am.index.Insert(ctx, tx); err != nil { - return fmt.Errorf("failed to insert tx into auction index: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - am.Remove(tx) - 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 *DefaultMempool) Remove(tx sdk.Tx) error { - if err := am.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { - return fmt.Errorf("failed to remove transaction from the mempool: %w", err) - } - - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return fmt.Errorf("failed to get tx hash string: %w", err) - } - - delete(am.txIndex, txHashStr) - - return nil -} - -func (am *DefaultMempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { - return am.index.Select(ctx, txs) -} - -func (am *DefaultMempool) CountTx() int { - return am.index.CountTx() -} - -// Contains returns true if the transaction is contained in the mempool. -func (am *DefaultMempool) Contains(tx sdk.Tx) bool { - _, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx) - if err != nil { - return false - } - - _, ok := am.txIndex[txHashStr] - return ok -} diff --git a/blockbuster/lanes/base/mempool_test.go b/blockbuster/lanes/base/mempool_test.go new file mode 100644 index 0000000..4f12e73 --- /dev/null +++ b/blockbuster/lanes/base/mempool_test.go @@ -0,0 +1,240 @@ +package base_test + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + testutils "github.com/skip-mev/pob/testutils" +) + +func (s *BaseTestSuite) TestGetTxPriority() { + txPriority := blockbuster.DefaultTxPriority() + + s.Run("should be able to get the priority off a normal transaction with fees", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal(sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String(), priority) + }) + + s.Run("should not get a priority when the transaction does not have a fee", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal("", priority) + }) + + s.Run("should get a priority when the gas token is different", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin("random", math.NewInt(100)), + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal(sdk.NewCoin("random", math.NewInt(100)).String(), priority) + }) +} + +func (s *BaseTestSuite) TestCompareTxPriority() { + txPriority := blockbuster.DefaultTxPriority() + + s.Run("should return 0 when both priorities are nil", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(0)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(0)).String() + s.Require().Equal(0, txPriority.Compare(a, b)) + }) + + s.Run("should return 1 when the first priority is greater", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)).String() + s.Require().Equal(1, txPriority.Compare(a, b)) + }) + + s.Run("should return -1 when the second priority is greater", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + s.Require().Equal(-1, txPriority.Compare(a, b)) + }) + + s.Run("should return 0 when both priorities are equal", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + s.Require().Equal(0, txPriority.Compare(a, b)) + }) +} + +func (s *BaseTestSuite) TestInsert() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + s.Run("should be able to insert a transaction", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + }) + + s.Run("cannot insert more transactions than the max", func() { + for i := 0; i < 3; i++ { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + uint64(i), + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(int64(100*i))), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + } + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 10, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().Error(err) + s.Require().False(mempool.Contains(tx)) + }) +} + +func (s *BaseTestSuite) TestRemove() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + s.Run("should be able to remove a transaction", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + + mempool.Remove(tx) + s.Require().False(mempool.Contains(tx)) + }) + + s.Run("should not error when removing a transaction that does not exist", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + mempool.Remove(tx) + }) +} + +func (s *BaseTestSuite) TestSelect() { + s.Run("should be able to select transactions in the correct order", func() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200)), + ) + s.Require().NoError(err) + + // Insert the transactions into the mempool + s.Require().NoError(mempool.Insert(sdk.Context{}, tx1)) + s.Require().NoError(mempool.Insert(sdk.Context{}, tx2)) + s.Require().Equal(2, mempool.CountTx()) + + // Check that the transactions are in the correct order + iterator := mempool.Select(sdk.Context{}, nil) + s.Require().NotNil(iterator) + s.Require().Equal(tx2, iterator.Tx()) + + // Check the second transaction + iterator = iterator.Next() + s.Require().NotNil(iterator) + s.Require().Equal(tx1, iterator.Tx()) + }) + + s.Run("should be able to select a single transaction", func() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + // Insert the transactions into the mempool + s.Require().NoError(mempool.Insert(sdk.Context{}, tx1)) + s.Require().Equal(1, mempool.CountTx()) + + // Check that the transactions are in the correct order + iterator := mempool.Select(sdk.Context{}, nil) + s.Require().NotNil(iterator) + s.Require().Equal(tx1, iterator.Tx()) + + iterator = iterator.Next() + s.Require().Nil(iterator) + }) +} diff --git a/blockbuster/lanes/free/factory.go b/blockbuster/lanes/free/factory.go deleted file mode 100644 index c9bd0fa..0000000 --- a/blockbuster/lanes/free/factory.go +++ /dev/null @@ -1,47 +0,0 @@ -package free - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/staking/types" -) - -type ( - // Factory defines the interface for processing free transactions. It is - // a wrapper around all of the functionality that each application chain must implement - // in order for free processing to work. - Factory interface { - // IsFreeTx defines a function that checks if a transaction qualifies as free. - IsFreeTx(tx sdk.Tx) bool - } - - // DefaultFreeFactory defines a default implmentation for the free factory interface for processing free transactions. - DefaultFreeFactory struct { - txDecoder sdk.TxDecoder - } -) - -var _ Factory = (*DefaultFreeFactory)(nil) - -// NewDefaultFreeFactory returns a default free factory interface implementation. -func NewDefaultFreeFactory(txDecoder sdk.TxDecoder) Factory { - return &DefaultFreeFactory{ - txDecoder: txDecoder, - } -} - -// IsFreeTx defines a default function that checks if a transaction is free. In this case, -// any transaction that is a delegation/redelegation transaction is free. -func (config *DefaultFreeFactory) IsFreeTx(tx sdk.Tx) bool { - for _, msg := range tx.GetMsgs() { - switch msg.(type) { - case *types.MsgDelegate: - return true - case *types.MsgBeginRedelegate: - return true - case *types.MsgCancelUnbondingDelegation: - return true - } - } - - return false -} diff --git a/blockbuster/lanes/free/lane.go b/blockbuster/lanes/free/lane.go index 6413d1e..2c4c424 100644 --- a/blockbuster/lanes/free/lane.go +++ b/blockbuster/lanes/free/lane.go @@ -2,8 +2,8 @@ package free import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/skip-mev/pob/blockbuster" - "github.com/skip-mev/pob/blockbuster/lanes/base" ) const ( @@ -11,31 +11,52 @@ const ( LaneName = "free" ) -var _ blockbuster.Lane = (*Lane)(nil) +var _ blockbuster.Lane = (*FreeLane)(nil) // FreeLane defines the lane that is responsible for processing free transactions. -type Lane struct { - *base.DefaultLane - Factory +// By default, transactions that are staking related are considered free. +type FreeLane struct { //nolint + *blockbuster.LaneConstructor } // NewFreeLane returns a new free lane. -func NewFreeLane(cfg blockbuster.BaseLaneConfig, factory Factory) *Lane { - if err := cfg.ValidateBasic(); err != nil { - panic(err) - } +func NewFreeLane( + cfg blockbuster.LaneConfig, + txPriority blockbuster.TxPriority[string], + matchFn blockbuster.MatchHandler, +) *FreeLane { + lane := blockbuster.NewLaneConstructor( + cfg, + LaneName, + blockbuster.NewConstructorMempool[string]( + txPriority, + cfg.TxEncoder, + cfg.MaxTxs, + ), + matchFn, + ) - return &Lane{ - DefaultLane: base.NewDefaultLane(cfg).WithName(LaneName), - Factory: factory, + return &FreeLane{ + LaneConstructor: lane, } } -// Match returns true if the transaction is a free transaction. -func (l *Lane) Match(ctx sdk.Context, tx sdk.Tx) bool { - if l.MatchIgnoreList(ctx, tx) { +// DefaultMatchHandler returns the default match handler for the free lane. The +// default implementation matches transactions that are staking related. In particular, +// any transaction that is a MsgDelegate, MsgBeginRedelegate, or MsgCancelUnbondingDelegation. +func DefaultMatchHandler() blockbuster.MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + for _, msg := range tx.GetMsgs() { + switch msg.(type) { + case *types.MsgDelegate: + return true + case *types.MsgBeginRedelegate: + return true + case *types.MsgCancelUnbondingDelegation: + return true + } + } + return false } - - return l.IsFreeTx(tx) } diff --git a/blockbuster/lanes/terminator/lane.go b/blockbuster/lanes/terminator/lane.go index 159d69a..35e1453 100644 --- a/blockbuster/lanes/terminator/lane.go +++ b/blockbuster/lanes/terminator/lane.go @@ -2,7 +2,6 @@ package terminator import ( "context" - "fmt" "cosmossdk.io/log" "cosmossdk.io/math" @@ -11,6 +10,10 @@ import ( "github.com/skip-mev/pob/blockbuster" ) +const ( + LaneName = "Terminator" +) + // Terminator Lane will get added to the chain to simplify chaining code so that we // don't need to check if next == nil further up the chain // @@ -40,26 +43,42 @@ func (t Terminator) PrepareLane(_ sdk.Context, proposal blockbuster.BlockProposa return proposal, nil } +// ValidateLaneBasic is a no-op +func (t Terminator) CheckOrder(sdk.Context, []sdk.Tx) error { + return nil +} + // ProcessLane is a no-op func (t Terminator) ProcessLane(ctx sdk.Context, _ []sdk.Tx, _ blockbuster.ProcessLanesHandler) (sdk.Context, error) { return ctx, nil } +// GetMaxBlockSpace is a no-op +func (t Terminator) GetMaxBlockSpace() math.LegacyDec { + return math.LegacyZeroDec() +} + +// Logger is a no-op +func (t Terminator) Logger() log.Logger { + return log.NewNopLogger() +} + // Name returns the name of the lane func (t Terminator) Name() string { - return "Terminator" + return LaneName } +// SetAnteHandler is a no-op +func (t Terminator) SetAnteHandler(sdk.AnteHandler) {} + +// SetIgnoreList is a no-op +func (t Terminator) SetIgnoreList([]blockbuster.Lane) {} + // Match is a no-op func (t Terminator) Match(sdk.Context, sdk.Tx) bool { return false } -// VerifyTx is a no-op -func (t Terminator) VerifyTx(sdk.Context, sdk.Tx) error { - return fmt.Errorf("Terminator lane should not be called") -} - // Contains is a no-op func (t Terminator) Contains(sdk.Tx) bool { return false @@ -85,20 +104,7 @@ func (t Terminator) Select(context.Context, [][]byte) sdkmempool.Iterator { return nil } -// ValidateLaneBasic is a no-op -func (t Terminator) ProcessLaneBasic(sdk.Context, []sdk.Tx) 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() math.LegacyDec { - return math.LegacyZeroDec() +// HasHigherPriority is a no-op +func (t Terminator) Compare(sdk.Context, sdk.Tx, sdk.Tx) int { + return 0 } diff --git a/blockbuster/mempool.go b/blockbuster/mempool.go index 82f997b..938d2b6 100644 --- a/blockbuster/mempool.go +++ b/blockbuster/mempool.go @@ -31,11 +31,15 @@ type ( GetLane(name string) (Lane, error) } - // Mempool defines the Blockbuster mempool implement. It contains a registry + // BBMempool defines the Blockbuster mempool implementation. It contains a registry // of lanes, which allows for customizable block proposal construction. BBMempool struct { + logger log.Logger + + // registry contains the lanes in the mempool. The lanes are ordered + // according to their priority. The first lane in the registry has the + // highest priority and the last lane has the lowest priority. registry []Lane - logger log.Logger } ) @@ -47,8 +51,10 @@ type ( // registry. Each transaction should only belong in one lane but this is NOT enforced. // To enforce that each transaction belong to a single lane, you must configure the // ignore list of each lane to include all preceding lanes. Basic mempool API will -// attempt to insert, remove transactions from all lanes it belongs to. -func NewMempool(logger log.Logger, lanes ...Lane) *BBMempool { +// attempt to insert, remove transactions from all lanes it belongs to. It is recommended, +// that mutex is set to true when creating the mempool. This will ensure that each +// transaction cannot be inserted into the lanes before it. +func NewMempool(logger log.Logger, mutex bool, lanes ...Lane) *BBMempool { mempool := &BBMempool{ logger: logger, registry: lanes, @@ -58,6 +64,16 @@ func NewMempool(logger log.Logger, lanes ...Lane) *BBMempool { panic(err) } + // Set the ignore list for each lane + if mutex { + registry := mempool.registry + for index, lane := range mempool.registry { + if index > 0 { + lane.SetIgnoreList(registry[:index]) + } + } + } + return mempool } @@ -85,7 +101,14 @@ func (m *BBMempool) GetTxDistribution() map[string]int { // Insert will insert a transaction into the mempool. It inserts the transaction // into the first lane that it matches. -func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) error { +func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Insert", "err", r) + err = fmt.Errorf("panic in Insert: %v", r) + } + }() + var errors []string unwrappedCtx := sdk.UnwrapSDKContext(ctx) @@ -118,7 +141,14 @@ func (m *BBMempool) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator { } // Remove removes a transaction from all of the lanes it is currently in. -func (m *BBMempool) Remove(tx sdk.Tx) error { +func (m *BBMempool) Remove(tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Remove", "err", r) + err = fmt.Errorf("panic in Remove: %v", r) + } + }() + var errors []string for _, lane := range m.registry { @@ -149,7 +179,14 @@ func (m *BBMempool) Remove(tx sdk.Tx) error { } // Contains returns true if the transaction is contained in any of the lanes. -func (m *BBMempool) Contains(tx sdk.Tx) bool { +func (m *BBMempool) Contains(tx sdk.Tx) (contains bool) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Contains", "err", r) + contains = false + } + }() + for _, lane := range m.registry { if lane.Contains(tx) { return true @@ -164,7 +201,10 @@ func (m *BBMempool) Registry() []Lane { return m.registry } -// ValidateBasic validates the mempools configuration. +// ValidateBasic validates the mempools configuration. ValidateBasic ensures +// the following: +// - The sum of the lane max block space percentages is less than or equal to 1. +// - There is no unused block space. func (m *BBMempool) ValidateBasic() error { sum := math.LegacyZeroDec() seenZeroMaxBlockSpace := false diff --git a/blockbuster/mempool_test.go b/blockbuster/mempool_test.go index 2384a1b..84e470c 100644 --- a/blockbuster/mempool_test.go +++ b/blockbuster/mempool_test.go @@ -27,9 +27,10 @@ type BlockBusterTestSuite struct { encodingConfig testutils.EncodingConfig // Define all of the lanes utilized in the test suite - tobLane *auction.TOBLane - baseLane *base.DefaultLane - freeLane *free.Lane + tobLane *auction.TOBLane + baseLane *base.DefaultLane + freeLane *free.FreeLane + gasTokenDenom string lanes []blockbuster.Lane mempool blockbuster.Mempool @@ -55,7 +56,8 @@ func (suite *BlockBusterTestSuite) SetupTest() { // Lanes configuration // // TOB lane set up - tobConfig := blockbuster.BaseLaneConfig{ + suite.gasTokenDenom = "stake" + tobConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -64,37 +66,30 @@ func (suite *BlockBusterTestSuite) SetupTest() { } suite.tobLane = auction.NewTOBLane( tobConfig, - 0, // No bound on the number of transactions in the lane auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), ) // Free lane set up - freeConfig := blockbuster.BaseLaneConfig{ + freeConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - suite.tobLane, - }, } suite.freeLane = free.NewFreeLane( freeConfig, - free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), + blockbuster.DefaultTxPriority(), + free.DefaultMatchHandler(), ) // Base lane set up - baseConfig := blockbuster.BaseLaneConfig{ + baseConfig := blockbuster.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - suite.tobLane, - suite.freeLane, - }, } suite.baseLane = base.NewDefaultLane( baseConfig, @@ -102,7 +97,7 @@ func (suite *BlockBusterTestSuite) SetupTest() { // Mempool set up suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) + suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), true, suite.lanes...) // Accounts set up suite.accounts = testutils.RandomAccounts(suite.random, 10) @@ -328,12 +323,12 @@ func (suite *BlockBusterTestSuite) fillBaseLane(numTxs int) { // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) + priority := suite.random.Int63n(100) + 1 + tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs, sdk.NewCoin(suite.gasTokenDenom, math.NewInt(priority))) suite.Require().NoError(err) // insert the tx into the lane and update the account suite.nonces[acc.Address.String()]++ - priority := suite.random.Int63n(100) + 1 suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx)) } } @@ -348,7 +343,7 @@ func (suite *BlockBusterTestSuite) fillTOBLane(numTxs int) { // create a randomized auction transaction nonce := suite.nonces[acc.Address.String()] bidAmount := math.NewInt(int64(suite.random.Intn(1000) + 1)) - bid := sdk.NewCoin("stake", bidAmount) + bid := sdk.NewCoin(suite.gasTokenDenom, bidAmount) tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, nil) suite.Require().NoError(err) @@ -367,7 +362,7 @@ func (suite *BlockBusterTestSuite) fillFreeLane(numTxs int) { // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] - tx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, "val1", sdk.NewCoin("stake", math.NewInt(100))) + tx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, "val1", sdk.NewCoin(suite.gasTokenDenom, math.NewInt(100)), sdk.NewCoin(suite.gasTokenDenom, math.NewInt(100))) suite.Require().NoError(err) // insert the tx into the lane and update the account diff --git a/blockbuster/priority_nonce.go b/blockbuster/priority_nonce.go index 5b55c14..b561d50 100644 --- a/blockbuster/priority_nonce.go +++ b/blockbuster/priority_nonce.go @@ -313,14 +313,16 @@ func (i *PriorityNonceIterator[C]) Next() sdkmempool.Iterator { // We've reached a transaction with a priority lower than the next highest // priority in the pool. - if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) < 0 { - return i.iteratePriority() - } else if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) == 0 { - // Weight is incorporated into the priority index key only (not sender index) - // so we must fetch it here from the scores map. - weight := i.mempool.scores[txMeta[C]{nonce: key.nonce, sender: key.sender}].weight - if i.mempool.cfg.TxPriority.Compare(weight, i.priorityNode.Next().Key().(txMeta[C]).weight) < 0 { + if i.priorityNode.Next() != nil { + if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) < 0 { return i.iteratePriority() + } else if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) == 0 { + // Weight is incorporated into the priority index key only (not sender index) + // so we must fetch it here from the scores map. + weight := i.mempool.scores[txMeta[C]{nonce: key.nonce, sender: key.sender}].weight + if i.mempool.cfg.TxPriority.Compare(weight, i.priorityNode.Next().Key().(txMeta[C]).weight) < 0 { + return i.iteratePriority() + } } } diff --git a/blockbuster/proposals.go b/blockbuster/proposals.go index 4ea94a1..95da151 100644 --- a/blockbuster/proposals.go +++ b/blockbuster/proposals.go @@ -131,14 +131,6 @@ func (p *Proposal) UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte } p.totalTxBytes = updatedSize - lane.Logger().Info( - "adding transactions to proposal", - "lane", lane.Name(), - "num_txs", len(partialProposalTxs), - "total_tx_bytes", partialProposalSize, - "cumulative_size", updatedSize, - ) - p.txs = append(p.txs, partialProposalTxs...) for _, tx := range partialProposalTxs { @@ -146,8 +138,23 @@ func (p *Proposal) UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte txHashStr := hex.EncodeToString(txHash[:]) p.cache[txHashStr] = struct{}{} + + lane.Logger().Info( + "added transaction to proposal", + "lane", lane.Name(), + "tx_hash", txHashStr, + "tx_bytes", len(tx), + ) } + lane.Logger().Info( + "added transactions to proposal", + "lane", lane.Name(), + "num_txs", len(partialProposalTxs), + "total_tx_bytes", partialProposalSize, + "cumulative_size", updatedSize, + ) + return nil } diff --git a/blockbuster/types.go b/blockbuster/types.go new file mode 100644 index 0000000..d816863 --- /dev/null +++ b/blockbuster/types.go @@ -0,0 +1,164 @@ +package blockbuster + +import ( + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ( + // MatchHandler is utilized to determine if a transaction should be included in the lane. This + // function can be a stateless or stateful check on the transaction. + MatchHandler func(ctx sdk.Context, tx sdk.Tx) bool + + // PrepareLaneHandler is responsible for preparing transactions to be included in the block from a + // given lane. Given a lane, this function should return the transactions to include in the block, + // the transactions that must be removed from the lane, and an error if one occurred. + PrepareLaneHandler func( + ctx sdk.Context, + proposal BlockProposal, + maxTxBytes int64, + ) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) + + // ProcessLaneHandler is responsible for processing transactions that are included in a block and + // belong to a given lane. ProcessLaneHandler is executed after CheckOrderHandler so the transactions + // passed into this function SHOULD already be in order respecting the ordering rules of the lane and + // respecting the ordering rules of mempool relative to the lanes it has. + ProcessLaneHandler func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) + + // CheckOrderHandler is responsible for checking the order of transactions that belong to a given + // lane. This handler should be used to verify that the ordering of transactions passed into the + // function respect the ordering logic of the lane (if any transactions from the lane are included). + // This function should also ensure that transactions that belong to this lane are contiguous and do + // not have any transactions from other lanes in between them. + CheckOrderHandler func(ctx sdk.Context, txs []sdk.Tx) error + + // PrepareLanesHandler wraps all of the lanes' PrepareLane function into a single chained + // function. You can think of it like an AnteHandler, but for preparing proposals in the + // context of lanes instead of modules. + PrepareLanesHandler func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) + + // ProcessLanesHandler wraps all of the lanes' ProcessLane functions into a single chained + // function. You can think of it like an AnteHandler, but for processing proposals in the + // context of lanes instead of modules. + ProcessLanesHandler func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) + + // LaneConfig defines the basic functionality needed for a lane. + LaneConfig struct { + Logger log.Logger + TxEncoder sdk.TxEncoder + TxDecoder sdk.TxDecoder + AnteHandler sdk.AnteHandler + + // MaxBlockSpace defines the relative percentage of block space that can be + // used by this lane. NOTE: If this is set to zero, then there is no limit + // on the number of transactions that can be included in the block for this + // lane (up to maxTxBytes as provided by the request). This is useful for the default lane. + MaxBlockSpace math.LegacyDec + + // IgnoreList defines the list of lanes to ignore when processing transactions. This + // is useful for when you want lanes to exist after the default lane. For example, + // say there are two lanes: default and free. The free lane should be processed after + // the default lane. In this case, the free lane should be added to the ignore list + // of the default lane. Otherwise, the transactions that belong to the free lane + // will be processed by the default lane (which accepts all transactions by default). + IgnoreList []Lane + + // MaxTxs sets the maximum number of transactions allowed in the mempool with + // the semantics: + // - if MaxTx == 0, there is no cap on the number of transactions in the mempool + // - if MaxTx > 0, the mempool will cap the number of transactions it stores, + // and will prioritize transactions by their priority and sender-nonce + // (sequence number) when evicting transactions. + // - if MaxTx < 0, `Insert` is a no-op. + MaxTxs int + } +) + +// NewLaneConfig returns a new LaneConfig. This will be embedded in a lane. +func NewBaseLaneConfig( + logger log.Logger, + txEncoder sdk.TxEncoder, + txDecoder sdk.TxDecoder, + anteHandler sdk.AnteHandler, + maxBlockSpace math.LegacyDec, +) LaneConfig { + return LaneConfig{ + Logger: logger, + TxEncoder: txEncoder, + TxDecoder: txDecoder, + AnteHandler: anteHandler, + MaxBlockSpace: maxBlockSpace, + } +} + +// ValidateBasic validates the lane configuration. +func (c *LaneConfig) 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(math.LegacyOneDec()) { + return fmt.Errorf("max block space must be set to a value between 0 and 1") + } + + return nil +} + +// NoOpPrepareLanesHandler returns a no-op prepare lanes handler. +// This should only be used for testing. +func NoOpPrepareLanesHandler() PrepareLanesHandler { + return func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) { + return proposal, nil + } +} + +// NoOpPrepareLaneHandler returns a no-op prepare lane handler. +// This should only be used for testing. +func NoOpPrepareLaneHandler() PrepareLaneHandler { + return func(ctx sdk.Context, proposal BlockProposal, maxTxBytes int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) { + return nil, nil, nil + } +} + +// PanicPrepareLaneHandler returns a prepare lane handler that panics. +// This should only be used for testing. +func PanicPrepareLaneHandler() PrepareLaneHandler { + return func(sdk.Context, BlockProposal, int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) { + panic("panic prepare lanes handler") + } +} + +// NoOpProcessLanesHandler returns a no-op process lanes handler. +// This should only be used for testing. +func NoOpProcessLanesHandler() ProcessLanesHandler { + return func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) { + return ctx, nil + } +} + +// NoOpProcessLaneHandler returns a no-op process lane handler. +// This should only be used for testing. +func NoOpProcessLaneHandler() ProcessLaneHandler { + return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { + return txs, nil + } +} + +// PanicProcessLanesHandler returns a process lanes handler that panics. +// This should only be used for testing. +func PanicProcessLaneHandler() ProcessLaneHandler { + return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, error) { + panic("panic process lanes handler") + } +} diff --git a/blockbuster/utils/ante.go b/blockbuster/utils/ante.go index 418662e..fe9e12f 100644 --- a/blockbuster/utils/ante.go +++ b/blockbuster/utils/ante.go @@ -5,7 +5,7 @@ import ( ) type ( - // Lane defines the required functionality for a lane. The ignore decorator + // Lane defines the required API dependencies for the IgnoreDecorator. The ignore decorator // will check if a transaction belongs to a lane by calling the Match function. Lane interface { Match(ctx sdk.Context, tx sdk.Tx) bool diff --git a/blockbuster/utils/mocks/lane.go b/blockbuster/utils/mocks/lane.go new file mode 100644 index 0000000..16faa82 --- /dev/null +++ b/blockbuster/utils/mocks/lane.go @@ -0,0 +1,256 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + blockbuster "github.com/skip-mev/pob/blockbuster" + + log "cosmossdk.io/log" + + math "cosmossdk.io/math" + + mempool "github.com/cosmos/cosmos-sdk/types/mempool" + + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// Lane is an autogenerated mock type for the Lane type +type Lane struct { + mock.Mock +} + +// CheckOrder provides a mock function with given fields: ctx, txs +func (_m *Lane) CheckOrder(ctx types.Context, txs []types.Tx) error { + ret := _m.Called(ctx, txs) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, []types.Tx) error); ok { + r0 = rf(ctx, txs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Compare provides a mock function with given fields: ctx, this, other +func (_m *Lane) Compare(ctx types.Context, this types.Tx, other types.Tx) int { + ret := _m.Called(ctx, this, other) + + var r0 int + if rf, ok := ret.Get(0).(func(types.Context, types.Tx, types.Tx) int); ok { + r0 = rf(ctx, this, other) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Contains provides a mock function with given fields: tx +func (_m *Lane) Contains(tx types.Tx) bool { + ret := _m.Called(tx) + + var r0 bool + if rf, ok := ret.Get(0).(func(types.Tx) bool); ok { + r0 = rf(tx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CountTx provides a mock function with given fields: +func (_m *Lane) CountTx() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// GetMaxBlockSpace provides a mock function with given fields: +func (_m *Lane) GetMaxBlockSpace() math.LegacyDec { + ret := _m.Called() + + var r0 math.LegacyDec + if rf, ok := ret.Get(0).(func() math.LegacyDec); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(math.LegacyDec) + } + + return r0 +} + +// Insert provides a mock function with given fields: _a0, _a1 +func (_m *Lane) Insert(_a0 context.Context, _a1 types.Tx) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Logger provides a mock function with given fields: +func (_m *Lane) Logger() log.Logger { + ret := _m.Called() + + var r0 log.Logger + if rf, ok := ret.Get(0).(func() log.Logger); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(log.Logger) + } + } + + return r0 +} + +// Match provides a mock function with given fields: ctx, tx +func (_m *Lane) Match(ctx types.Context, tx types.Tx) bool { + ret := _m.Called(ctx, tx) + + var r0 bool + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) bool); ok { + r0 = rf(ctx, tx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *Lane) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// PrepareLane provides a mock function with given fields: ctx, proposal, maxTxBytes, next +func (_m *Lane) PrepareLane(ctx types.Context, proposal blockbuster.BlockProposal, maxTxBytes int64, next blockbuster.PrepareLanesHandler) (blockbuster.BlockProposal, error) { + ret := _m.Called(ctx, proposal, maxTxBytes, next) + + var r0 blockbuster.BlockProposal + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, blockbuster.BlockProposal, int64, blockbuster.PrepareLanesHandler) (blockbuster.BlockProposal, error)); ok { + return rf(ctx, proposal, maxTxBytes, next) + } + if rf, ok := ret.Get(0).(func(types.Context, blockbuster.BlockProposal, int64, blockbuster.PrepareLanesHandler) blockbuster.BlockProposal); ok { + r0 = rf(ctx, proposal, maxTxBytes, next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(blockbuster.BlockProposal) + } + } + + if rf, ok := ret.Get(1).(func(types.Context, blockbuster.BlockProposal, int64, blockbuster.PrepareLanesHandler) error); ok { + r1 = rf(ctx, proposal, maxTxBytes, next) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProcessLane provides a mock function with given fields: ctx, proposalTxs, next +func (_m *Lane) ProcessLane(ctx types.Context, proposalTxs []types.Tx, next blockbuster.ProcessLanesHandler) (types.Context, error) { + ret := _m.Called(ctx, proposalTxs, next) + + var r0 types.Context + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, []types.Tx, blockbuster.ProcessLanesHandler) (types.Context, error)); ok { + return rf(ctx, proposalTxs, next) + } + if rf, ok := ret.Get(0).(func(types.Context, []types.Tx, blockbuster.ProcessLanesHandler) types.Context); ok { + r0 = rf(ctx, proposalTxs, next) + } else { + r0 = ret.Get(0).(types.Context) + } + + if rf, ok := ret.Get(1).(func(types.Context, []types.Tx, blockbuster.ProcessLanesHandler) error); ok { + r1 = rf(ctx, proposalTxs, next) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Remove provides a mock function with given fields: _a0 +func (_m *Lane) Remove(_a0 types.Tx) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Tx) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Select provides a mock function with given fields: _a0, _a1 +func (_m *Lane) Select(_a0 context.Context, _a1 [][]byte) mempool.Iterator { + ret := _m.Called(_a0, _a1) + + var r0 mempool.Iterator + if rf, ok := ret.Get(0).(func(context.Context, [][]byte) mempool.Iterator); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(mempool.Iterator) + } + } + + return r0 +} + +// SetAnteHandler provides a mock function with given fields: antehander +func (_m *Lane) SetAnteHandler(antehander types.AnteHandler) { + _m.Called(antehander) +} + +// SetIgnoreList provides a mock function with given fields: ignoreList +func (_m *Lane) SetIgnoreList(ignoreList []blockbuster.Lane) { + _m.Called(ignoreList) +} + +// NewLane creates a new instance of Lane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLane(t interface { + mock.TestingT + Cleanup(func()) +}) *Lane { + mock := &Lane{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/blockbuster/utils/mocks/lane_mempool.go b/blockbuster/utils/mocks/lane_mempool.go new file mode 100644 index 0000000..d4116e0 --- /dev/null +++ b/blockbuster/utils/mocks/lane_mempool.go @@ -0,0 +1,117 @@ +// Code generated by mockery v2.30.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mempool "github.com/cosmos/cosmos-sdk/types/mempool" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// LaneMempool is an autogenerated mock type for the LaneMempool type +type LaneMempool struct { + mock.Mock +} + +// Compare provides a mock function with given fields: ctx, this, other +func (_m *LaneMempool) Compare(ctx types.Context, this types.Tx, other types.Tx) int { + ret := _m.Called(ctx, this, other) + + var r0 int + if rf, ok := ret.Get(0).(func(types.Context, types.Tx, types.Tx) int); ok { + r0 = rf(ctx, this, other) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Contains provides a mock function with given fields: tx +func (_m *LaneMempool) Contains(tx types.Tx) bool { + ret := _m.Called(tx) + + var r0 bool + if rf, ok := ret.Get(0).(func(types.Tx) bool); ok { + r0 = rf(tx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CountTx provides a mock function with given fields: +func (_m *LaneMempool) CountTx() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Insert provides a mock function with given fields: _a0, _a1 +func (_m *LaneMempool) Insert(_a0 context.Context, _a1 types.Tx) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Remove provides a mock function with given fields: _a0 +func (_m *LaneMempool) Remove(_a0 types.Tx) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Tx) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Select provides a mock function with given fields: _a0, _a1 +func (_m *LaneMempool) Select(_a0 context.Context, _a1 [][]byte) mempool.Iterator { + ret := _m.Called(_a0, _a1) + + var r0 mempool.Iterator + if rf, ok := ret.Get(0).(func(context.Context, [][]byte) mempool.Iterator); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(mempool.Iterator) + } + } + + return r0 +} + +// NewLaneMempool creates a new instance of LaneMempool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLaneMempool(t interface { + mock.TestingT + Cleanup(func()) +}) *LaneMempool { + mock := &LaneMempool{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/blockbuster/utils/utils.go b/blockbuster/utils/utils.go index 6edf364..a4b2ae0 100644 --- a/blockbuster/utils/utils.go +++ b/blockbuster/utils/utils.go @@ -40,8 +40,8 @@ func GetDecodedTxs(txDecoder sdk.TxDecoder, txs [][]byte) ([]sdk.Tx, error) { } // RemoveTxsFromLane removes the transactions from the given lane's mempool. -func RemoveTxsFromLane(txs map[sdk.Tx]struct{}, mempool sdkmempool.Mempool) error { - for tx := range txs { +func RemoveTxsFromLane(txs []sdk.Tx, mempool sdkmempool.Mempool) error { + for _, tx := range txs { if err := mempool.Remove(tx); err != nil { return err } diff --git a/go.mod b/go.mod index 837118b..535433e 100644 --- a/go.mod +++ b/go.mod @@ -20,14 +20,12 @@ require ( github.com/cosmos/cosmos-db v1.0.0 github.com/cosmos/cosmos-proto v1.0.0-beta.3 github.com/cosmos/cosmos-sdk v0.50.0-beta.0 - github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.4.10 github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.3 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/huandu/skiplist v1.2.0 - github.com/ory/dockertest/v3 v3.10.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 @@ -47,10 +45,7 @@ require ( filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/DataDog/zstd v1.5.5 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect - github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aws/aws-sdk-go v1.44.224 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect @@ -68,8 +63,8 @@ require ( github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230613231145-182959a1fad6 // indirect github.com/cometbft/cometbft-db v0.8.0 // indirect - github.com/containerd/continuity v0.3.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v1.0.0-beta.2 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect @@ -83,10 +78,6 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/docker/cli v23.0.1+incompatible // indirect - github.com/docker/docker v23.0.1+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/emicklei/dot v1.5.0 // indirect @@ -107,7 +98,6 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/s2a-go v0.1.4 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.11.0 // indirect @@ -128,7 +118,6 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect - github.com/imdario/mergo v0.3.13 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -148,12 +137,9 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230110094441-db37f07504ce // indirect github.com/oklog/run v1.1.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/opencontainers/runc v1.1.5 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/petermattis/goid v0.0.0-20230518223814-80aa455d8761 // indirect @@ -168,32 +154,27 @@ require ( github.com/rs/cors v1.8.3 // indirect github.com/rs/zerolog v1.30.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/tidwall/btree v1.6.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/zondax/hid v0.9.1 // indirect github.com/zondax/ledger-go v0.14.1 // indirect go.etcd.io/bbolt v1.3.7 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect - golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect - golang.org/x/tools v0.7.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.126.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index f814c4d..8a43bf1 100644 --- a/go.sum +++ b/go.sum @@ -221,7 +221,6 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -229,9 +228,7 @@ github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -334,7 +331,6 @@ github.com/cometbft/cometbft-db v0.8.0 h1:vUMDaH3ApkX8m0KZvOFFy9b5DZHBAjsnEuo9AK github.com/cometbft/cometbft-db v0.8.0/go.mod h1:6ASCP4pfhmrCBpfk01/9E1SI29nD3HfVHrY4PG8x5c0= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -374,7 +370,6 @@ github.com/creachadair/tomledit v0.0.24 h1:5Xjr25R2esu1rKCbQEmjZYlrhFkDspoAbAKb6 github.com/creachadair/tomledit v0.0.24/go.mod h1:9qHbShRWQzSCcn617cMzg4eab1vbLCOjOshAWSzWr8U= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= @@ -395,15 +390,9 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= -github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= -github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -475,7 +464,6 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -590,8 +578,6 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -693,8 +679,6 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -794,8 +778,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -837,9 +819,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -853,8 +833,6 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -943,7 +921,6 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1014,13 +991,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1117,7 +1087,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1321,7 +1290,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1417,7 +1385,6 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1691,7 +1658,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= diff --git a/tests/app/app.go b/tests/app/app.go index ab52db1..8ebc49a 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -139,7 +139,7 @@ type TestApp struct { FeeGrantKeeper feegrantkeeper.Keeper // custom checkTx handler - checkTxHandler abci.CheckTx + checkTxHandler auction.CheckTx } func init() { @@ -262,43 +262,39 @@ func New( // NOTE: The lanes are ordered by priority. The first lane is the highest priority // lane and the last lane is the lowest priority lane. // Top of block lane allows transactions to bid for inclusion at the top of the next block. - tobConfig := blockbuster.BaseLaneConfig{ + tobConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), - MaxBlockSpace: math.LegacyZeroDec(), + MaxBlockSpace: math.LegacyZeroDec(), // This means the lane has no limit on block space. + MaxTxs: 0, // This means the lane has no limit on the number of transactions it can store. } tobLane := auction.NewTOBLane( tobConfig, - 0, auction.NewDefaultAuctionFactory(app.txConfig.TxDecoder()), ) // Free lane allows transactions to be included in the next block for free. - freeConfig := blockbuster.BaseLaneConfig{ + freeConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - tobLane, - }, + MaxTxs: 0, } freeLane := free.NewFreeLane( freeConfig, - free.NewDefaultFreeFactory(app.txConfig.TxDecoder()), + blockbuster.DefaultTxPriority(), + free.DefaultMatchHandler(), ) // Default lane accepts all other transactions. - defaultConfig := blockbuster.BaseLaneConfig{ + defaultConfig := blockbuster.LaneConfig{ Logger: app.Logger(), TxEncoder: app.txConfig.TxEncoder(), TxDecoder: app.txConfig.TxDecoder(), MaxBlockSpace: math.LegacyZeroDec(), - IgnoreList: []blockbuster.Lane{ - tobLane, - freeLane, - }, + MaxTxs: 0, } defaultLane := base.NewDefaultLane(defaultConfig) @@ -308,7 +304,7 @@ func New( freeLane, defaultLane, } - mempool := blockbuster.NewMempool(app.Logger(), lanes...) + mempool := blockbuster.NewMempool(app.Logger(), true, lanes...) app.App.SetMempool(mempool) // Create a global ante handler that will be called on each transaction when @@ -337,17 +333,17 @@ func New( } app.App.SetAnteHandler(anteHandler) - // Set the proposal handlers on base app + // Set the abci handlers on base app proposalHandler := abci.NewProposalHandler( app.Logger(), app.TxConfig().TxDecoder(), - mempool, + lanes, ) app.App.SetPrepareProposal(proposalHandler.PrepareProposalHandler()) app.App.SetProcessProposal(proposalHandler.ProcessProposalHandler()) // Set the custom CheckTx handler on BaseApp. - checkTxHandler := abci.NewCheckTxHandler( + checkTxHandler := auction.NewCheckTxHandler( app.App, app.txConfig.TxDecoder(), tobLane, @@ -396,7 +392,7 @@ func (app *TestApp) CheckTx(req *cometabci.RequestCheckTx) (*cometabci.ResponseC } // SetCheckTx sets the checkTxHandler for the app. -func (app *TestApp) SetCheckTx(handler abci.CheckTx) { +func (app *TestApp) SetCheckTx(handler auction.CheckTx) { app.checkTxHandler = handler } diff --git a/tests/integration/chain_setup.go b/tests/integration/chain_setup.go index 5c94024..ba753a9 100644 --- a/tests/integration/chain_setup.go +++ b/tests/integration/chain_setup.go @@ -189,7 +189,6 @@ func BroadcastTxs(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, require.NoError(t, err) } else { require.Error(t, err) - } } diff --git a/tests/integration/pob_integration_test.go b/tests/integration/pob_integration_test.go index 4766666..e5b775d 100644 --- a/tests/integration/pob_integration_test.go +++ b/tests/integration/pob_integration_test.go @@ -5,9 +5,9 @@ import ( "testing" testutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/skip-mev/pob/tests/integration" buildertypes "github.com/skip-mev/pob/x/builder/types" "github.com/strangelove-ventures/interchaintest/v7" - "github.com/skip-mev/pob/tests/integration" "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v7/ibc" "github.com/stretchr/testify/suite" @@ -16,7 +16,7 @@ import ( var ( // config params numValidators = 4 - numFullNodes = 0 + numFullNodes = 0 denom = "stake" image = ibc.DockerImage{ diff --git a/testutils/utils.go b/testutils/utils.go index 2a77768..9dd0c5b 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -84,7 +84,7 @@ func RandomAccounts(r *rand.Rand, n int) []Account { return accs } -func CreateTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, msgs []sdk.Msg) (authsigning.Tx, error) { +func CreateTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, msgs []sdk.Msg, fees ...sdk.Coin) (authsigning.Tx, error) { txBuilder := txCfg.NewTxBuilder() if err := txBuilder.SetMsgs(msgs...); err != nil { return nil, err @@ -104,10 +104,12 @@ func CreateTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, msg txBuilder.SetTimeoutHeight(timeout) + txBuilder.SetFeeAmount(fees) + return txBuilder.GetTx(), nil } -func CreateFreeTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, validator string, amount sdk.Coin) (authsigning.Tx, error) { +func CreateFreeTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, validator string, amount sdk.Coin, fees ...sdk.Coin) (authsigning.Tx, error) { msgs := []sdk.Msg{ &stakingtypes.MsgDelegate{ DelegatorAddress: account.Address.String(), @@ -116,10 +118,10 @@ func CreateFreeTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, }, } - return CreateTx(txCfg, account, nonce, timeout, msgs) + return CreateTx(txCfg, account, nonce, timeout, msgs, fees...) } -func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64) (authsigning.Tx, error) { +func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64, fees ...sdk.Coin) (authsigning.Tx, error) { msgs := make([]sdk.Msg, numberMsgs) for i := 0; i < int(numberMsgs); i++ { msgs[i] = &banktypes.MsgSend{ @@ -147,6 +149,8 @@ func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, t txBuilder.SetTimeoutHeight(timeout) + txBuilder.SetFeeAmount(fees) + return txBuilder.GetTx(), nil } @@ -189,6 +193,53 @@ func CreateTxWithSigners(txCfg client.TxConfig, nonce, timeout uint64, signers [ return txBuilder.GetTx(), nil } +func CreateAuctionTx(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, timeout uint64, signers []Account) (authsigning.Tx, []authsigning.Tx, error) { + bidMsg := &buildertypes.MsgAuctionBid{ + Bidder: bidder.Address.String(), + Bid: bid, + Transactions: make([][]byte, len(signers)), + } + + txs := []authsigning.Tx{} + + for i := 0; i < len(signers); i++ { + randomMsg := CreateRandomMsgs(signers[i].Address, 1) + randomTx, err := CreateTx(txCfg, signers[i], 0, timeout, randomMsg) + if err != nil { + return nil, nil, err + } + + bz, err := txCfg.TxEncoder()(randomTx) + if err != nil { + return nil, nil, err + } + + bidMsg.Transactions[i] = bz + txs = append(txs, randomTx) + } + + txBuilder := txCfg.NewTxBuilder() + if err := txBuilder.SetMsgs(bidMsg); err != nil { + return nil, nil, err + } + + sigV2 := signing.SignatureV2{ + PubKey: bidder.PrivKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + Signature: nil, + }, + Sequence: nonce, + } + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, nil, err + } + + txBuilder.SetTimeoutHeight(timeout) + + return txBuilder.GetTx(), txs, nil +} + func CreateAuctionTxWithSigners(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, timeout uint64, signers []Account) (authsigning.Tx, error) { bidMsg := &buildertypes.MsgAuctionBid{ Bidder: bidder.Address.String(), diff --git a/x/builder/ante/ante_test.go b/x/builder/ante/ante_test.go index 1c93700..5608d0b 100644 --- a/x/builder/ante/ante_test.go +++ b/x/builder/ante/ante_test.go @@ -83,7 +83,7 @@ func (suite *AnteTestSuite) SetupTest() { // Lanes configuration // // TOB lane set up - tobConfig := blockbuster.BaseLaneConfig{ + tobConfig := blockbuster.LaneConfig{ Logger: suite.ctx.Logger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -92,12 +92,11 @@ func (suite *AnteTestSuite) SetupTest() { } suite.tobLane = auction.NewTOBLane( tobConfig, - 0, // No bound on the number of transactions in the lane auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()), ) // Base lane set up - baseConfig := blockbuster.BaseLaneConfig{ + baseConfig := blockbuster.LaneConfig{ Logger: suite.ctx.Logger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), @@ -109,7 +108,7 @@ func (suite *AnteTestSuite) SetupTest() { // Mempool set up suite.lanes = []blockbuster.Lane{suite.tobLane, suite.baseLane} - suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), suite.lanes...) + suite.mempool = blockbuster.NewMempool(log.NewTestLogger(suite.T()), true, suite.lanes...) } func (suite *AnteTestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {