package block_test import ( "math/rand" "testing" "time" "cosmossdk.io/log" "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" signer_extraction "github.com/skip-mev/block-sdk/v2/adapters/signer_extraction_adapter" "github.com/skip-mev/block-sdk/v2/block" "github.com/skip-mev/block-sdk/v2/block/base" defaultlane "github.com/skip-mev/block-sdk/v2/lanes/base" "github.com/skip-mev/block-sdk/v2/lanes/free" "github.com/skip-mev/block-sdk/v2/lanes/mev" testutils "github.com/skip-mev/block-sdk/v2/testutils" auctiontypes "github.com/skip-mev/block-sdk/v2/x/auction/types" ) type BlockBusterTestSuite struct { suite.Suite ctx sdk.Context // Define basic tx configuration encodingConfig testutils.EncodingConfig // Define all of the lanes utilized in the test suite mevLane *mev.MEVLane baseLane *base.BaseLane freeLane *base.BaseLane gasTokenDenom string lanes []block.Lane mempool *block.LanedMempool // account set up accounts []testutils.Account random *rand.Rand nonces map[string]uint64 } func TestBlockBusterTestSuite(t *testing.T) { suite.Run(t, new(BlockBusterTestSuite)) } func (suite *BlockBusterTestSuite) 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(auctiontypes.StoreKey) testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) suite.ctx = testCtx.Ctx.WithBlockHeight(1) // Lanes configuration // // TOB lane set up suite.gasTokenDenom = "stake" mevConfig := base.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), SignerExtractor: signer_extraction.NewDefaultAdapter(), AnteHandler: nil, MaxBlockSpace: math.LegacyMustNewDecFromStr("0.3"), } factory := mev.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder(), signer_extraction.NewDefaultAdapter()) suite.mevLane = mev.NewMEVLane( mevConfig, factory, factory.MatchHandler(), ) // Free lane set up freeConfig := base.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), SignerExtractor: signer_extraction.NewDefaultAdapter(), AnteHandler: nil, MaxBlockSpace: math.LegacyMustNewDecFromStr("0.3"), } suite.freeLane = free.NewFreeLane( freeConfig, base.DefaultTxPriority(), free.DefaultMatchHandler(), ) // Base lane set up baseConfig := base.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), SignerExtractor: signer_extraction.NewDefaultAdapter(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), } suite.baseLane = defaultlane.NewDefaultLane( baseConfig, base.DefaultMatchHandler(), ) // Mempool set up suite.lanes = []block.Lane{suite.mevLane, suite.freeLane, suite.baseLane} var err error suite.mempool, err = block.NewLanedMempool( log.NewNopLogger(), suite.lanes, ) suite.Require().NoError(err) // 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 } } func (suite *BlockBusterTestSuite) TestNewMempool() { defaultConfig := base.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), SignerExtractor: signer_extraction.NewDefaultAdapter(), AnteHandler: nil, MaxBlockSpace: math.LegacyZeroDec(), } baseConfig := base.LaneConfig{ Logger: log.NewNopLogger(), TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), SignerExtractor: signer_extraction.NewDefaultAdapter(), AnteHandler: nil, MaxBlockSpace: math.LegacyMustNewDecFromStr("0.3"), } defaultLane := defaultlane.NewDefaultLane(defaultConfig, base.DefaultMatchHandler()) factory := mev.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder(), signer_extraction.NewDefaultAdapter()) mevLane := mev.NewMEVLane( baseConfig, factory, factory.MatchHandler(), ) freeLane := free.NewFreeLane( baseConfig, base.DefaultTxPriority(), free.DefaultMatchHandler(), ) invalidFreeLane := free.NewFreeLane( defaultConfig, base.DefaultTxPriority(), free.DefaultMatchHandler(), ) suite.Run("works with a single lane", func() { lanes := []block.Lane{defaultLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works mev and default lane", func() { lanes := []block.Lane{mevLane, defaultLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works mev and default lane in reverse order", func() { lanes := []block.Lane{mevLane, defaultLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works with mev, free, and default lane", func() { lanes := []block.Lane{mevLane, freeLane, defaultLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works with mev, default, free lane", func() { lanes := []block.Lane{mevLane, defaultLane, freeLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works with free, mev, and default lane", func() { lanes := []block.Lane{freeLane, mevLane, defaultLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("works with default, free, mev lanes", func() { lanes := []block.Lane{defaultLane, freeLane, mevLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().NoError(err) }) suite.Run("default lane not included - invalid total space", func() { lanes := []block.Lane{mevLane, freeLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().Error(err) }) suite.Run("two lanes with unlimited blockspace", func() { lanes := []block.Lane{defaultLane, invalidFreeLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().Error(err) }) suite.Run("duplicate lanes", func() { lanes := []block.Lane{mevLane, defaultLane, mevLane} _, err := block.NewLanedMempool( log.NewNopLogger(), lanes, ) suite.Require().Error(err) }) } func (suite *BlockBusterTestSuite) TestInsert() { cases := []struct { name string insertDistribution map[string]uint64 }{ { "insert 1 mev tx", map[string]uint64{ suite.mevLane.Name(): 1, }, }, { "insert 10 mev txs", map[string]uint64{ suite.mevLane.Name(): 10, }, }, { "insert 1 base tx", map[string]uint64{ suite.baseLane.Name(): 1, }, }, { "insert 10 base txs and 10 mev txs", map[string]uint64{ suite.baseLane.Name(): 10, suite.mevLane.Name(): 10, }, }, { "insert 100 base txs and 100 mev txs", map[string]uint64{ suite.baseLane.Name(): 100, suite.mevLane.Name(): 100, }, }, { "insert 100 base txs, 100 mev txs, and 100 free txs", map[string]uint64{ suite.baseLane.Name(): 100, suite.mevLane.Name(): 100, suite.freeLane.Name(): 100, }, }, { "insert 10 free txs", map[string]uint64{ suite.freeLane.Name(): 10, }, }, { "insert 10 free txs and 10 base txs", map[string]uint64{ suite.freeLane.Name(): 10, suite.baseLane.Name(): 10, }, }, { "insert 10 mev txs and 10 free txs", map[string]uint64{ suite.mevLane.Name(): 10, suite.freeLane.Name(): 10, }, }, } for _, tc := range cases { suite.Run(tc.name, func() { suite.SetupTest() // reset // Fill the base lane with numBaseTxs transactions suite.fillBaseLane(tc.insertDistribution[suite.baseLane.Name()]) // Fill the TOB lane with numTobTxs transactions suite.fillTOBLane(tc.insertDistribution[suite.mevLane.Name()]) // Fill the Free lane with numFreeTxs transactions suite.fillFreeLane(tc.insertDistribution[suite.freeLane.Name()]) sum := uint64(0) for _, v := range tc.insertDistribution { sum += v } // Validate the mempool suite.Require().Equal(int(sum), suite.mempool.CountTx()) // Validate the lanes suite.Require().Equal(tc.insertDistribution[suite.mevLane.Name()], uint64(suite.mevLane.CountTx())) suite.Require().Equal(tc.insertDistribution[suite.baseLane.Name()], uint64(suite.baseLane.CountTx())) suite.Require().Equal(tc.insertDistribution[suite.freeLane.Name()], uint64(suite.freeLane.CountTx())) // Validate the lane counts laneCounts := suite.mempool.GetTxDistribution() // Ensure that the lane counts are correct suite.Require().Equal(tc.insertDistribution[suite.mevLane.Name()], laneCounts[suite.mevLane.Name()]) suite.Require().Equal(tc.insertDistribution[suite.baseLane.Name()], laneCounts[suite.baseLane.Name()]) suite.Require().Equal(tc.insertDistribution[suite.freeLane.Name()], laneCounts[suite.freeLane.Name()]) }) } } func (suite *BlockBusterTestSuite) TestRemove() { cases := []struct { name string numTobTxs uint64 numBaseTxs uint64 }{ { "insert 1 mev tx", 1, 0, }, { "insert 10 mev txs", 10, 0, }, { "insert 1 base tx", 0, 1, }, { "insert 10 base txs and 10 mev txs", 10, 10, }, { "insert 100 base txs and 100 mev txs", 100, 100, }, } for _, tc := range cases { suite.Run(tc.name, func() { suite.SetupTest() // reset // Fill the base lane with numBaseTxs transactions suite.fillBaseLane(tc.numBaseTxs) // Fill the TOB lane with numTobTxs transactions suite.fillTOBLane(tc.numTobTxs) // Remove all transactions from the lanes mevCount := tc.numTobTxs baseCount := tc.numBaseTxs for iterator := suite.baseLane.Select(suite.ctx, nil); iterator != nil; { tx := iterator.Tx() // Remove the transaction from the mempool suite.Require().NoError(suite.mempool.Remove(tx)) // Ensure that the transaction is no longer in the mempool suite.Require().Equal(false, suite.mempool.Contains(tx)) // Ensure the number of transactions in the lane is correct baseCount-- suite.Require().Equal(suite.baseLane.CountTx(), int(baseCount)) distribution := suite.mempool.GetTxDistribution() suite.Require().Equal(distribution[suite.baseLane.Name()], baseCount) iterator = suite.baseLane.Select(suite.ctx, nil) } suite.Require().Equal(0, suite.baseLane.CountTx()) suite.Require().Equal(int(mevCount), suite.mevLane.CountTx()) // Remove all transactions from the lanes for iterator := suite.mevLane.Select(suite.ctx, nil); iterator != nil; { tx := iterator.Tx() // Remove the transaction from the mempool suite.Require().NoError(suite.mempool.Remove(tx)) // Ensure that the transaction is no longer in the mempool suite.Require().Equal(false, suite.mempool.Contains(tx)) // Ensure the number of transactions in the lane is correct mevCount-- suite.Require().Equal(suite.mevLane.CountTx(), int(mevCount)) distribution := suite.mempool.GetTxDistribution() suite.Require().Equal(distribution[suite.mevLane.Name()], mevCount) iterator = suite.mevLane.Select(suite.ctx, nil) } suite.Require().Equal(0, suite.mevLane.CountTx()) suite.Require().Equal(0, suite.baseLane.CountTx()) suite.Require().Equal(0, suite.mempool.CountTx()) // Validate the lane counts distribution := suite.mempool.GetTxDistribution() // Ensure that the lane counts are correct suite.Require().Equal(distribution[suite.mevLane.Name()], uint64(0)) suite.Require().Equal(distribution[suite.baseLane.Name()], uint64(0)) }) } } // fillBaseLane fills the base lane with numTxs transactions that are randomly created. func (suite *BlockBusterTestSuite) fillBaseLane(numTxs uint64) { for i := uint64(0); i < numTxs; i++ { // randomly select an account to create the tx randomIndex := suite.random.Intn(len(suite.accounts)) acc := suite.accounts[randomIndex] // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) 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()]++ suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx)) } } // fillTOBLane fills the TOB lane with numTxs transactions that are randomly created. func (suite *BlockBusterTestSuite) fillTOBLane(numTxs uint64) { for i := uint64(0); i < numTxs; i++ { // randomly select a bidder to create the tx randomIndex := suite.random.Intn(len(suite.accounts)) acc := suite.accounts[randomIndex] // create a randomized auction transaction nonce := suite.nonces[acc.Address.String()] bidAmount := math.NewInt(int64(suite.random.Intn(1000) + 1)) bid := sdk.NewCoin(suite.gasTokenDenom, bidAmount) tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, nil) suite.Require().NoError(err) // insert the auction tx into the global mempool suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) suite.nonces[acc.Address.String()]++ } } // filleFreeLane fills the free lane with numTxs transactions that are randomly created. func (suite *BlockBusterTestSuite) fillFreeLane(numTxs uint64) { for i := uint64(0); i < numTxs; i++ { // randomly select an account to create the tx randomIndex := suite.random.Intn(len(suite.accounts)) acc := suite.accounts[randomIndex] // create a few random msgs and construct the tx nonce := suite.nonces[acc.Address.String()] 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 suite.nonces[acc.Address.String()]++ suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) } }