diff --git a/blockbuster/abci/abci.go b/blockbuster/abci/abci.go index 532ecf0..6b9c263 100644 --- a/blockbuster/abci/abci.go +++ b/blockbuster/abci/abci.go @@ -132,8 +132,6 @@ func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandle // Write the cache to the context since we know that the lane successfully prepared // the partial proposal. write() - - lane.Logger().Info("prepared lane", "lane", lane.Name()) } }() diff --git a/blockbuster/abci/abci_test.go b/blockbuster/abci/abci_test.go new file mode 100644 index 0000000..c1d1f10 --- /dev/null +++ b/blockbuster/abci/abci_test.go @@ -0,0 +1,1055 @@ +package abci_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/skip-mev/pob/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 { + suite.Suite + logger log.Logger + ctx sdk.Context + + // 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 +} + +func TestBlockBusterTestSuite(t *testing.T) { + suite.Run(t, new(ABCITestSuite)) +} + +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 := sdk.NewKVStoreKey(buildertypes.StoreKey) + testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) + suite.ctx = testCtx.Ctx.WithBlockHeight(1) + + // Lanes configuration + config := blockbuster.BaseLaneConfig{ + Logger: log.NewNopLogger(), + TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: suite.anteHandler, + MaxBlockSpace: sdk.ZeroDec(), // It can be as big as it wants (up to maxTxBytes) + } + + // Top of block lane set up + suite.tobConfig = config + 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.freeConfig = config + suite.freeLane = free.NewFreeLane( + suite.freeConfig, + free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), + ) + + // Base lane set up + suite.baseConfig = config + suite.baseLane = base.NewDefaultLane( + suite.baseConfig, + ) + + // Mempool set up + suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} + suite.mempool = blockbuster.NewMempool(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.NewNopLogger(), suite.mempool) +} + +func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) { + signer := tx.GetMsgs()[0].GetSigners()[0] + suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return( + sdk.NewCoins( + sdk.NewCoin("foo", sdk.NewInt(100000000000000)), + ), + ) + + next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + return ctx, nil + } + + return suite.builderDecorator.AnteHandle(ctx, tx, false, next) +} + +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(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("foo", sdk.NewInt(1000)) + minBidIncrement = sdk.NewCoin("foo", sdk.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 + }, + 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("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a 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("foo", sdk.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 = sdk.NewDecWithPrec(1, 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("foo", sdk.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 + freeSize - 1 + suite.tobConfig.MaxBlockSpace = sdk.ZeroDec() + + 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("foo", sdk.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 + freeSize + normalSize + 1 + + // Tob can take up as much space as it wants + suite.tobConfig.MaxBlockSpace = sdk.ZeroDec() + + // Free can take up less space than the tx + suite.freeConfig.MaxBlockSpace = sdk.MustNewDecFromStr("0.01") + + // Default can take up as much space as it wants + suite.baseConfig.MaxBlockSpace = sdk.ZeroDec() + + 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 = sdk.ZeroDec() + suite.freeConfig.MaxBlockSpace = sdk.ZeroDec() + suite.baseConfig.MaxBlockSpace = sdk.ZeroDec() + + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.NewInt(1))) // bid is less than the reserve fee + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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) + + txs = []sdk.Tx{normalTx} + auctionTxs = []sdk.Tx{} + winningBidTx = nil + insertBundledTxs = false + }, + 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("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a valid default transaction + account := suite.accounts[1] + nonce = suite.nonces[account.Address.String()] + 1 + numberMsgs := uint64(3) + normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout) + suite.Require().NoError(err) + + 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("foo", sdk.NewInt(1000000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder, bidder, suite.accounts[1]} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.NewInt(10000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[2], bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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 an valid tob transaction + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(10000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.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} + winningBidTx = bidTx + insertBundledTxs = false + }, + 5, + map[string]int{ + base.LaneName: 1, + auction.LaneName: 1, + free.LaneName: 1, + }, + }, + } + + for _, tc := range cases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset + tc.malleate() + suite.resetLanesWithNewConfig() + + // Insert all of the normal transactions into the default lane + for _, tx := range txs { + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) + } + + // Insert all of the auction transactions into the TOB lane + for _, tx := range auctionTxs { + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) + } + + // Insert all of the bundled transactions into the TOB lane if desired + if insertBundledTxs { + for _, tx := range auctionTxs { + bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx) + suite.Require().NoError(err) + + for _, txBz := range bidInfo.Transactions { + tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz) + suite.Require().NoError(err) + + suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx)) + } + } + } + + // Create a new auction + params := buildertypes.Params{ + MaxBundleSize: maxBundleSize, + ReserveFee: reserveFee, + FrontRunningProtection: frontRunningProtection, + 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(suite.logger, suite.mempool) + handler := suite.proposalHandler.PrepareProposalHandler() + res := handler(suite.ctx, abcitypes.RequestPrepareProposal{ + MaxTxBytes: maxTxBytes, + }) + + // -------------------- 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) + + lane, err := suite.mempool.Match(tx) + suite.Require().NoError(err) + + // 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 lane.Name() == suite.tobLane.Name(): + 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(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)) + } + }) + } +} + +func (suite *ABCITestSuite) TestProcessProposal() { + var ( + // mempool configuration + txs []sdk.Tx + auctionTxs []sdk.Tx + insertRefTxs = false + + // auction configuration + maxBundleSize uint32 = 10 + reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000)) + frontRunningProtection = true + ) + + 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) + + 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("foo", sdk.NewInt(1000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.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("foo", sdk.NewInt(10000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.NewInt(10000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[2], bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a valid tob transaction + bidder = suite.accounts[1] + bid = sdk.NewCoin("foo", sdk.NewInt(1000)) + nonce = suite.nonces[bidder.Address.String()] + timeout = uint64(100) + signers = []testutils.Account{bidder} + bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + 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("foo", sdk.NewInt(10000000)) + nonce := suite.nonces[bidder.Address.String()] + timeout := uint64(100) + signers := []testutils.Account{suite.accounts[2], bidder} + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers) + suite.Require().NoError(err) + + // Create a 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("foo", sdk.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("foo", sdk.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 := handler(suite.ctx, abcitypes.RequestProcessProposal{ + Txs: proposalTxs, + }) + + // Check if the response is valid + suite.Require().Equal(tc.response, res.Status) + }) + } +} diff --git a/blockbuster/lanes/auction/abci.go b/blockbuster/lanes/auction/abci.go index 80eb200..4c5776d 100644 --- a/blockbuster/lanes/auction/abci.go +++ b/blockbuster/lanes/auction/abci.go @@ -102,11 +102,10 @@ selectBidTxLoop: break selectBidTxLoop } - txsToRemove[tmpBidTx] = struct{}{} l.Cfg.Logger.Info( - "failed to select auction bid tx; tx size is too large", + "failed to select auction bid tx for lane; tx size is too large", "tx_size", bidTxSize, - "max_size", proposal.MaxTxBytes, + "max_size", maxTxBytes, ) } diff --git a/blockbuster/mempool.go b/blockbuster/mempool.go index f9316e8..9e4c316 100644 --- a/blockbuster/mempool.go +++ b/blockbuster/mempool.go @@ -2,6 +2,7 @@ package blockbuster import ( "context" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" @@ -22,6 +23,12 @@ type ( // GetTxDistribution returns the number of transactions in each lane. GetTxDistribution() map[string]int + + // Match will return the lane that the transaction belongs to. + Match(tx sdk.Tx) (Lane, error) + + // GetLane returns the lane with the given name. + GetLane(name string) (Lane, error) } // Mempool defines the Blockbuster mempool implement. It contains a registry @@ -58,16 +65,27 @@ func (m *BBMempool) GetTxDistribution() map[string]int { return counts } -// Insert inserts a transaction into every lane that it matches. Insertion will -// be attempted on all lanes, even if an error is encountered. -func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) error { +// Match will return the lane that the transaction belongs to. It matches to +// the first lane where lane.Match(tx) is true. +func (m *BBMempool) Match(tx sdk.Tx) (Lane, error) { for _, lane := range m.registry { if lane.Match(tx) { - return lane.Insert(ctx, tx) + return lane, nil } } - return nil + return nil, fmt.Errorf("no lane matched transaction") +} + +// 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 { + lane, err := m.Match(tx) + if err != nil { + return err + } + + return lane.Insert(ctx, tx) } // Insert returns a nil iterator. @@ -80,30 +98,40 @@ func (m *BBMempool) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator { return nil } -// Remove removes a transaction from the mempool. It removes the transaction -// from the first lane that it matches. +// Remove removes a transaction from the mempool based on the first lane that +// it matches. func (m *BBMempool) Remove(tx sdk.Tx) error { - for _, lane := range m.registry { - if lane.Match(tx) { - return lane.Remove(tx) - } + lane, err := m.Match(tx) + if err != nil { + return err } - return nil + return lane.Remove(tx) } -// Contains returns true if the transaction is contained in the mempool. +// Contains returns true if the transaction is contained in the mempool. It +// checks the first lane that it matches to. func (m *BBMempool) Contains(tx sdk.Tx) (bool, error) { - for _, lane := range m.registry { - if lane.Match(tx) { - return lane.Contains(tx) - } + lane, err := m.Match(tx) + if err != nil { + return false, err } - return false, nil + return lane.Contains(tx) } // Registry returns the mempool's lane registry. func (m *BBMempool) Registry() []Lane { return m.registry } + +// GetLane returns the lane with the given name. +func (m *BBMempool) GetLane(name string) (Lane, error) { + for _, lane := range m.registry { + if lane.Name() == name { + return lane, nil + } + } + + return nil, fmt.Errorf("lane %s not found", name) +} diff --git a/blockbuster/mempool_test.go b/blockbuster/mempool_test.go new file mode 100644 index 0000000..8df8fcc --- /dev/null +++ b/blockbuster/mempool_test.go @@ -0,0 +1,361 @@ +package blockbuster_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + "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" + buildertypes "github.com/skip-mev/pob/x/builder/types" + "github.com/stretchr/testify/suite" +) + +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 + tobLane *auction.TOBLane + baseLane *base.DefaultLane + freeLane *free.Lane + + lanes []blockbuster.Lane + mempool blockbuster.Mempool + + // 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 := sdk.NewKVStoreKey(buildertypes.StoreKey) + testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) + suite.ctx = testCtx.Ctx.WithBlockHeight(1) + + // Lanes configuration + // + // TOB lane set up + config := blockbuster.BaseLaneConfig{ + Logger: log.NewNopLogger(), + TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: nil, + MaxBlockSpace: sdk.ZeroDec(), + } + + // Top of block lane set up + suite.tobLane = auction.NewTOBLane( + config, + 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( + config, + free.NewDefaultFreeFactory(suite.encodingConfig.TxConfig.TxDecoder()), + ) + + // Base lane set up + suite.baseLane = base.NewDefaultLane( + config, + ) + + // Mempool set up + suite.lanes = []blockbuster.Lane{suite.tobLane, suite.freeLane, suite.baseLane} + suite.mempool = blockbuster.NewMempool(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 + } +} + +func (suite *BlockBusterTestSuite) TestInsert() { + cases := []struct { + name string + insertDistribution map[string]int + }{ + { + "insert 1 tob tx", + map[string]int{ + suite.tobLane.Name(): 1, + }, + }, + { + "insert 10 tob txs", + map[string]int{ + suite.tobLane.Name(): 10, + }, + }, + { + "insert 1 base tx", + map[string]int{ + suite.baseLane.Name(): 1, + }, + }, + { + "insert 10 base txs and 10 tob txs", + map[string]int{ + suite.baseLane.Name(): 10, + suite.tobLane.Name(): 10, + }, + }, + { + "insert 100 base txs and 100 tob txs", + map[string]int{ + suite.baseLane.Name(): 100, + suite.tobLane.Name(): 100, + }, + }, + { + "insert 100 base txs, 100 tob txs, and 100 free txs", + map[string]int{ + suite.baseLane.Name(): 100, + suite.tobLane.Name(): 100, + suite.freeLane.Name(): 100, + }, + }, + { + "insert 10 free txs", + map[string]int{ + suite.freeLane.Name(): 10, + }, + }, + { + "insert 10 free txs and 10 base txs", + map[string]int{ + suite.freeLane.Name(): 10, + suite.baseLane.Name(): 10, + }, + }, + { + "insert 10 tob txs and 10 free txs", + map[string]int{ + suite.tobLane.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.tobLane.Name()]) + + // Fill the Free lane with numFreeTxs transactions + suite.fillFreeLane(tc.insertDistribution[suite.freeLane.Name()]) + + sum := 0 + for _, v := range tc.insertDistribution { + sum += v + } + + // Validate the mempool + suite.Require().Equal(suite.mempool.CountTx(), sum) + + // Validate the lanes + suite.Require().Equal(suite.baseLane.CountTx(), tc.insertDistribution[suite.baseLane.Name()]) + suite.Require().Equal(suite.tobLane.CountTx(), tc.insertDistribution[suite.tobLane.Name()]) + suite.Require().Equal(suite.freeLane.CountTx(), tc.insertDistribution[suite.freeLane.Name()]) + + // Validate the lane counts + laneCounts := suite.mempool.GetTxDistribution() + + // Ensure that the lane counts are correct + suite.Require().Equal(laneCounts[suite.tobLane.Name()], tc.insertDistribution[suite.tobLane.Name()]) + suite.Require().Equal(laneCounts[suite.baseLane.Name()], tc.insertDistribution[suite.baseLane.Name()]) + suite.Require().Equal(laneCounts[suite.freeLane.Name()], tc.insertDistribution[suite.freeLane.Name()]) + }) + } +} + +func (suite *BlockBusterTestSuite) TestRemove() { + cases := []struct { + name string + numTobTxs int + numBaseTxs int + }{ + { + "insert 1 tob tx", + 1, + 0, + }, + { + "insert 10 tob txs", + 10, + 0, + }, + { + "insert 1 base tx", + 0, + 1, + }, + { + "insert 10 base txs and 10 tob txs", + 10, + 10, + }, + { + "insert 100 base txs and 100 tob 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 + tobCount := 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 + contains, err := suite.mempool.Contains(tx) + suite.Require().NoError(err) + suite.Require().False(contains) + + // Ensure the number of transactions in the lane is correct + baseCount-- + suite.Require().Equal(suite.baseLane.CountTx(), 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(tobCount, suite.tobLane.CountTx()) + + // Remove all transactions from the lanes + for iterator := suite.tobLane.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 + contains, err := suite.mempool.Contains(tx) + suite.Require().NoError(err) + suite.Require().False(contains) + + // Ensure the number of transactions in the lane is correct + tobCount-- + suite.Require().Equal(suite.tobLane.CountTx(), tobCount) + + distribution := suite.mempool.GetTxDistribution() + suite.Require().Equal(distribution[suite.tobLane.Name()], tobCount) + + iterator = suite.tobLane.Select(suite.ctx, nil) + } + + suite.Require().Equal(0, suite.tobLane.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.tobLane.Name()], 0) + suite.Require().Equal(distribution[suite.baseLane.Name()], 0) + }) + } +} + +// fillBaseLane fills the base lane with numTxs transactions that are randomly created. +func (suite *BlockBusterTestSuite) fillBaseLane(numTxs int) { + for i := 0; i < numTxs; i++ { + // randomly select an account to create the tx + randomIndex := suite.random.Intn(len(suite.accounts)) + acc := suite.accounts[randomIndex] + + // create a few random msgs 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) + 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)) + } +} + +// fillTOBLane fills the TOB lane with numTxs transactions that are randomly created. +func (suite *BlockBusterTestSuite) fillTOBLane(numTxs int) { + for i := 0; i < numTxs; i++ { + // randomly select a bidder to create the tx + randomIndex := suite.random.Intn(len(suite.accounts)) + acc := suite.accounts[randomIndex] + + // create a randomized auction transaction + nonce := suite.nonces[acc.Address.String()] + bidAmount := sdk.NewInt(int64(suite.random.Intn(1000) + 1)) + bid := sdk.NewCoin("foo", 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 int) { + for i := 0; i < numTxs; i++ { + // randomly select an account to create the tx + randomIndex := suite.random.Intn(len(suite.accounts)) + acc := suite.accounts[randomIndex] + + // create a few random msgs and construct the tx + nonce := suite.nonces[acc.Address.String()] + tx, err := testutils.CreateFreeTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, "val1", sdk.NewCoin("foo", sdk.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)) + } +} diff --git a/blockbuster/utils/utils_test.go b/blockbuster/utils/utils_test.go new file mode 100644 index 0000000..62e47a3 --- /dev/null +++ b/blockbuster/utils/utils_test.go @@ -0,0 +1,90 @@ +package utils + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" +) + +func TestGetMaxTxBytesForLane(t *testing.T) { + testCases := []struct { + name string + proposal *blockbuster.Proposal + ratio sdk.Dec + expected int64 + }{ + { + "ratio is zero", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 50, + }, + sdk.ZeroDec(), + 50, + }, + { + "ratio is zero", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 100, + }, + sdk.ZeroDec(), + 0, + }, + { + "ratio is zero", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 150, + }, + sdk.ZeroDec(), + 0, + }, + { + "ratio is 1", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 50, + }, + sdk.OneDec(), + 100, + }, + { + "ratio is 10%", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 50, + }, + sdk.MustNewDecFromStr("0.1"), + 10, + }, + { + "ratio is 25%", + &blockbuster.Proposal{ + MaxTxBytes: 100, + TotalTxBytes: 50, + }, + sdk.MustNewDecFromStr("0.25"), + 25, + }, + { + "ratio is 50%", + &blockbuster.Proposal{ + MaxTxBytes: 101, + TotalTxBytes: 50, + }, + sdk.MustNewDecFromStr("0.5"), + 50, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := GetMaxTxBytesForLane(tc.proposal, tc.ratio) + if actual != tc.expected { + t.Errorf("expected %d, got %d", tc.expected, actual) + } + }) + } +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index d0cdd58..c10c408 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -1032,11 +1032,11 @@ func (s *IntegrationTestSuite) TestFreeLane() { tx := s.createMsgDelegateTx(accounts[0], validator.OperatorAddress, defaultStakeAmount, 0, 1000) // Broadcast the transaction + s.waitForABlock() s.broadcastTx(tx, 0) // Wait for a block to be created s.waitForABlock() - s.waitForABlock() // Ensure that the transaction was executed correctly balanceAfterFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) @@ -1058,6 +1058,7 @@ func (s *IntegrationTestSuite) TestFreeLane() { normalTx := s.createMsgSendTx(accounts[1], accounts[2].Address.String(), defaultSendAmountCoins, 0, 1000) // Broadcast the transactions + s.waitForABlock() s.broadcastTx(freeTx, 0) s.broadcastTx(normalTx, 0) @@ -1098,6 +1099,7 @@ func (s *IntegrationTestSuite) TestFreeLane() { freeTx2 := s.createMsgDelegateTx(accounts[1], validator.OperatorAddress, defaultStakeAmount, 0, 1000) // Broadcast the transactions + s.waitForABlock() s.broadcastTx(freeTx, 0) s.broadcastTx(freeTx2, 0) diff --git a/testutils/utils.go b/testutils/utils.go index 3a97516..818f1c6 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -15,6 +15,7 @@ import ( authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/cosmos/cosmos-sdk/x/auth/tx" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" buildertypes "github.com/skip-mev/pob/x/builder/types" ) @@ -32,6 +33,7 @@ func CreateTestEncodingConfig() EncodingConfig { banktypes.RegisterInterfaces(interfaceRegistry) cryptocodec.RegisterInterfaces(interfaceRegistry) buildertypes.RegisterInterfaces(interfaceRegistry) + stakingtypes.RegisterInterfaces(interfaceRegistry) codec := codec.NewProtoCodec(interfaceRegistry) @@ -94,6 +96,18 @@ func CreateTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, msg return txBuilder.GetTx(), nil } +func CreateFreeTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, validator string, amount sdk.Coin) (authsigning.Tx, error) { + msgs := []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: account.Address.String(), + ValidatorAddress: validator, + Amount: amount, + }, + } + + return CreateTx(txCfg, account, nonce, timeout, msgs) +} + func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64) (authsigning.Tx, error) { msgs := make([]sdk.Msg, numberMsgs) for i := 0; i < int(numberMsgs); i++ {