From cfe2147d52c0ba1fd8ebd2ca55a0d3b2a2b60bc7 Mon Sep 17 00:00:00 2001 From: David Terpay <35130517+davidterpay@users.noreply.github.com> Date: Wed, 7 Jun 2023 12:38:42 -0400 Subject: [PATCH] feat(BB): Free Lane (#167) --- blockbuster/lanes/free/factory.go | 47 ++++++ blockbuster/lanes/free/lane.go | 42 +++++ blockbuster/utils/ante.go | 43 ++++++ tests/app/ante.go | 15 +- tests/app/app.go | 9 ++ tests/e2e/e2e_test.go | 245 ++++++++++++++++++++++++++---- tests/e2e/e2e_tx_test.go | 15 ++ tests/e2e/e2e_utils_test.go | 55 ++++++- 8 files changed, 432 insertions(+), 39 deletions(-) create mode 100644 blockbuster/lanes/free/factory.go create mode 100644 blockbuster/lanes/free/lane.go create mode 100644 blockbuster/utils/ante.go diff --git a/blockbuster/lanes/free/factory.go b/blockbuster/lanes/free/factory.go new file mode 100644 index 0000000..c9bd0fa --- /dev/null +++ b/blockbuster/lanes/free/factory.go @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..09b8991 --- /dev/null +++ b/blockbuster/lanes/free/lane.go @@ -0,0 +1,42 @@ +package free + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/base" +) + +const ( + // LaneName defines the name of the free lane. + LaneName = "free" +) + +var _ blockbuster.Lane = (*Lane)(nil) + +// FreeLane defines the lane that is responsible for processing free transactions. +type Lane struct { + *base.DefaultLane + Factory +} + +// NewFreeLane returns a new free lane. +func NewFreeLane(cfg blockbuster.BaseLaneConfig, factory Factory) *Lane { + if err := cfg.ValidateBasic(); err != nil { + panic(err) + } + + return &Lane{ + DefaultLane: base.NewDefaultLane(cfg), + Factory: factory, + } +} + +// Match returns true if the transaction is a free transaction. +func (l *Lane) Match(tx sdk.Tx) bool { + return l.IsFreeTx(tx) +} + +// Name returns the name of the free lane. +func (l *Lane) Name() string { + return LaneName +} diff --git a/blockbuster/utils/ante.go b/blockbuster/utils/ante.go new file mode 100644 index 0000000..7d4708c --- /dev/null +++ b/blockbuster/utils/ante.go @@ -0,0 +1,43 @@ +package utils + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ( + // Lane defines the required functionality for a lane. The ignore decorator + // will check if a transaction belongs to a lane by calling the Match function. + Lane interface { + Match(tx sdk.Tx) bool + } + + // IgnoreDecorator is an AnteDecorator that wraps an existing AnteDecorator. It allows + // for the AnteDecorator to be ignored for specified lanes. + IgnoreDecorator struct { + decorator sdk.AnteDecorator + lanes []Lane + } +) + +// NewIgnoreDecorator returns a new IgnoreDecorator instance. +func NewIgnoreDecorator(decorator sdk.AnteDecorator, lanes ...Lane) *IgnoreDecorator { + return &IgnoreDecorator{ + decorator: decorator, + lanes: lanes, + } +} + +// AnteHandle implements the sdk.AnteDecorator interface. If the transaction belongs to +// one of the lanes, the next AnteHandler is called. Otherwise, the decorator's AnteHandler +// is called. +func (sd IgnoreDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + for _, lane := range sd.lanes { + if lane.Match(tx) { + return next(ctx, tx, simulate) + } + } + + return sd.decorator.AnteHandle(ctx, tx, simulate, next) +} diff --git a/tests/app/ante.go b/tests/app/ante.go index ed5d160..7fdbf91 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/utils" builderante "github.com/skip-mev/pob/x/builder/ante" builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) @@ -15,6 +16,7 @@ type POBHandlerOptions struct { TxDecoder sdk.TxDecoder TxEncoder sdk.TxEncoder BuilderKeeper builderkeeper.Keeper + FreeLane blockbuster.Lane } // NewPOBAnteHandler wraps all of the default Cosmos SDK AnteDecorators with the POB AnteHandler. @@ -38,11 +40,14 @@ func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler { ante.NewTxTimeoutHeightDecorator(), ante.NewValidateMemoDecorator(options.BaseOptions.AccountKeeper), ante.NewConsumeGasForTxSizeDecorator(options.BaseOptions.AccountKeeper), - ante.NewDeductFeeDecorator( - options.BaseOptions.AccountKeeper, - options.BaseOptions.BankKeeper, - options.BaseOptions.FeegrantKeeper, - options.BaseOptions.TxFeeChecker, + utils.NewIgnoreDecorator( + ante.NewDeductFeeDecorator( + options.BaseOptions.AccountKeeper, + options.BaseOptions.BankKeeper, + options.BaseOptions.FeegrantKeeper, + options.BaseOptions.TxFeeChecker, + ), + options.FreeLane, ), ante.NewSetPubKeyDecorator(options.BaseOptions.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewValidateSigCountDecorator(options.BaseOptions.AccountKeeper), diff --git a/tests/app/app.go b/tests/app/app.go index 290bc44..317bb0c 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -71,6 +71,7 @@ import ( "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" buildermodule "github.com/skip-mev/pob/x/builder" builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) @@ -287,10 +288,17 @@ func New( auction.NewDefaultAuctionFactory(app.txConfig.TxDecoder()), ) + // Free lane allows transactions to be included in the next block for free. + freeLane := free.NewFreeLane( + config, + free.NewDefaultFreeFactory(app.txConfig.TxDecoder()), + ) + // Default lane accepts all other transactions. defaultLane := base.NewDefaultLane(config) lanes := []blockbuster.Lane{ tobLane, + freeLane, defaultLane, } @@ -311,6 +319,7 @@ func New( BuilderKeeper: app.BuilderKeeper, TxDecoder: app.txConfig.TxDecoder(), TxEncoder: app.txConfig.TxEncoder(), + FreeLane: freeLane, TOBLane: tobLane, Mempool: mempool, } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index b15733a..d0cdd58 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -68,7 +68,7 @@ func (s *IntegrationTestSuite) TestValidBids() { bundleHashes[0]: true, bundleHashes[1]: true, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -124,7 +124,7 @@ func (s *IntegrationTestSuite) TestValidBids() { expectedExecution[hash] = true } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -183,7 +183,7 @@ func (s *IntegrationTestSuite) TestValidBids() { expectedExecution[hash] = true } - s.verifyBlock(height+1, bundleHashes3, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes3, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid3) @@ -243,7 +243,7 @@ func (s *IntegrationTestSuite) TestValidBids() { expectedExecution[hash] = true } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -302,7 +302,7 @@ func (s *IntegrationTestSuite) TestValidBids() { expectedExecution[hash] = true } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -405,7 +405,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { } // Pass in nil since we don't know the order of transactions that ill be executed - s.verifyBlock(height+2, nil, expectedExecution) + s.verifyTopOfBlockAuction(height+2, nil, expectedExecution) // Ensure that the escrow account has the correct balance (both bids should have been extracted by this point) expectedEscrowFee := s.calculateProposerEscrowSplit(bid).Add(s.calculateProposerEscrowSplit(bid2)) @@ -459,7 +459,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { expectedExecution[hash] = true } - s.verifyBlock(height+1, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes2, expectedExecution) // Wait for a block to be created s.waitForABlock() @@ -473,7 +473,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { expectedExecution[hash] = true } - s.verifyBlock(height+2, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance (both bids should have been extracted by this point) expectedEscrowFee := s.calculateProposerEscrowSplit(bid).Add(s.calculateProposerEscrowSplit(bid2)) @@ -515,7 +515,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { bundleHashes[1]: true, bundleHashes2[0]: false, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -523,7 +523,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { // Wait another block to make sure the second bid is not executed s.waitForABlock() - s.verifyBlock(height+2, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes2, expectedExecution) }, }, { @@ -561,7 +561,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { bundleHashes[1]: true, bundleHashes2[0]: false, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -569,7 +569,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { // Wait another block to make sure the second bid is not executed s.waitForABlock() - s.verifyBlock(height+2, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes2, expectedExecution) }, }, { @@ -607,7 +607,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { bundleHashes2[0]: true, bundleHashes2[1]: true, } - s.verifyBlock(height+1, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes2, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) @@ -615,7 +615,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { // Wait for a block to be created and ensure that the first bid was not executed s.waitForABlock() - s.verifyBlock(height+2, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes, expectedExecution) }, }, { @@ -653,7 +653,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { bundleHashes2[0]: true, bundleHashes2[1]: true, } - s.verifyBlock(height+1, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes2, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) @@ -661,7 +661,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { // Wait for a block to be created and ensure that the first bid was not executed s.waitForABlock() - s.verifyBlock(height+2, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes, expectedExecution) }, }, { @@ -705,7 +705,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { bundleHashes2[0]: true, bundleHashes2[1]: true, } - s.verifyBlock(height+1, bundleHashes2, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes2, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) @@ -713,7 +713,7 @@ func (s *IntegrationTestSuite) TestMultipleBids() { // Wait for a block to be created and ensure that the second bid is executed s.waitForABlock() - s.verifyBlock(height+2, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+2, bundleHashes, expectedExecution) }, }, } @@ -771,7 +771,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { bundleHashes[1]: false, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -799,7 +799,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { } // Ensure that the block was built correctly and that the bid was not executed - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -831,7 +831,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { } // Ensure that the block was built correctly and that the bid was not executed - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -859,7 +859,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { } // Ensure that the block was built correctly and that the bid was not executed - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -887,7 +887,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { bundleHashes[1]: false, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -916,7 +916,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { for _, hash := range bundleHashes { expectedExecution[hash] = false } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -944,7 +944,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { bundleHashes[1]: false, } - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, { @@ -986,7 +986,7 @@ func (s *IntegrationTestSuite) TestInvalidBids() { expectedExecution[bundleHashes[0]] = false - s.verifyBlock(height+1, bundleHashes, expectedExecution) + s.verifyTopOfBlockAuction(height+1, bundleHashes, expectedExecution) }, }, } @@ -1002,3 +1002,196 @@ func (s *IntegrationTestSuite) TestInvalidBids() { s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) } } + +// TestFreeLane tests that the application correctly handles free lanes. There are a few invariants that are tested: +// +// 1. Transactions that qualify as free should not be deducted any fees. +// 2. Transactions that do not qualify as free should be deducted the correct fees. +func (s *IntegrationTestSuite) TestFreeLane() { + // Create the accounts that will create transactions to be included in bundles + initBalance := sdk.NewInt64Coin(app.BondDenom, 10000000000) + numAccounts := 4 + accounts := s.createTestAccounts(numAccounts, initBalance) + + defaultSendAmount := sdk.NewCoin(app.BondDenom, sdk.NewInt(10)) + defaultStakeAmount := sdk.NewCoin(app.BondDenom, sdk.NewInt(10)) + defaultSendAmountCoins := sdk.NewCoins(defaultSendAmount) + + testCases := []struct { + name string + test func() + }{ + { + name: "valid free lane transaction", + test: func() { + balanceBeforeFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) + + // basic stake amount + validators := s.queryValidators() + validator := validators[0] + tx := s.createMsgDelegateTx(accounts[0], validator.OperatorAddress, defaultStakeAmount, 0, 1000) + + // Broadcast the transaction + 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) + s.Require().True(balanceAfterFreeTx.Add(defaultStakeAmount).IsGTE(balanceBeforeFreeTx)) + }, + }, + { + name: "normal tx with free tx in same block", + test: func() { + balanceBeforeFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) + balanceBeforeNormalTx := s.queryBalanceOf(accounts[1].Address.String(), app.BondDenom) + + // basic free transaction + validators := s.queryValidators() + validator := validators[0] + freeTx := s.createMsgDelegateTx(accounts[0], validator.OperatorAddress, defaultStakeAmount, 0, 1000) + + // other normal transaction + normalTx := s.createMsgSendTx(accounts[1], accounts[2].Address.String(), defaultSendAmountCoins, 0, 1000) + + // Broadcast the transactions + s.broadcastTx(freeTx, 0) + s.broadcastTx(normalTx, 0) + + // Wait for a block to be created + s.waitForABlock() + height := s.queryCurrentHeight() + + // Ensure that the transaction was executed + balanceAfterFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) + s.Require().True(balanceAfterFreeTx.Add(defaultStakeAmount).IsGTE(balanceBeforeFreeTx)) + + // The balance must be strictly less than to account for fees + balanceAfterNormalTx := s.queryBalanceOf(accounts[1].Address.String(), app.BondDenom) + s.Require().True(balanceAfterNormalTx.IsLT((balanceBeforeNormalTx.Sub(defaultSendAmount)))) + + hashes := s.normalTxsToTxHashes([][]byte{freeTx, normalTx}) + expectedExecution := map[string]bool{ + hashes[0]: true, + hashes[1]: true, + } + + // Ensure that the block was built correctly + s.verifyBlock(height, hashes, expectedExecution) + }, + }, + { + name: "multiple free transactions in same block", + test: func() { + balanceBeforeFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) + balanceBeforeFreeTx2 := s.queryBalanceOf(accounts[1].Address.String(), app.BondDenom) + + // basic free transaction + validators := s.queryValidators() + validator := validators[0] + freeTx := s.createMsgDelegateTx(accounts[0], validator.OperatorAddress, defaultStakeAmount, 0, 1000) + + // other normal transaction + freeTx2 := s.createMsgDelegateTx(accounts[1], validator.OperatorAddress, defaultStakeAmount, 0, 1000) + + // Broadcast the transactions + s.broadcastTx(freeTx, 0) + s.broadcastTx(freeTx2, 0) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the transaction was executed + balanceAfterFreeTx := s.queryBalanceOf(accounts[0].Address.String(), app.BondDenom) + s.Require().True(balanceAfterFreeTx.Add(defaultStakeAmount).IsGTE(balanceBeforeFreeTx)) + + balanceAfterFreeTx2 := s.queryBalanceOf(accounts[1].Address.String(), app.BondDenom) + s.Require().True(balanceAfterFreeTx2.Add(defaultStakeAmount).IsGTE(balanceBeforeFreeTx2)) + }, + }, + } + + for _, tc := range testCases { + s.waitForABlock() + s.Run(tc.name, tc.test) + } +} + +// TestLanes tests that the application correctly handles lanes. The biggest invarient that is +// test here is making sure that transactions are ordered in blocks respecting the lane order. +func (s *IntegrationTestSuite) TestLanes() { + // Create the accounts that will create transactions to be included in bundles + initBalance := sdk.NewInt64Coin(app.BondDenom, 10000000000) + numAccounts := 4 + accounts := s.createTestAccounts(numAccounts, initBalance) + + defaultSendAmount := sdk.NewCoin(app.BondDenom, sdk.NewInt(10)) + defaultStakeAmount := sdk.NewCoin(app.BondDenom, sdk.NewInt(10)) + defaultSendAmountCoins := sdk.NewCoins(defaultSendAmount) + + // auction parameters + params := s.queryBuilderParams() + reserveFee := params.ReserveFee + + testCases := []struct { + name string + test func() + }{ + { + name: "block with tob, free, and normal tx", + test: func() { + // basic free transaction + validators := s.queryValidators() + validator := validators[0] + freeTx := s.createMsgDelegateTx(accounts[0], validator.OperatorAddress, defaultStakeAmount, 0, 1000) + + // other normal transaction + normalTx := s.createMsgSendTx(accounts[1], accounts[2].Address.String(), defaultSendAmountCoins, 0, 1000) + + // Create a bid transaction that includes the bundle and is valid + bundle := [][]byte{ + s.createMsgSendTx(accounts[3], accounts[1].Address.String(), defaultSendAmountCoins, 0, 1000), + } + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, bundle, 0, height+5) + s.displayExpectedBundle("Valid auction bid", bidTx, bundle) + + // Broadcast the transactions + s.waitForABlock() + s.broadcastTx(freeTx, 0) + s.broadcastTx(normalTx, 0) + s.broadcastTx(bidTx, 0) + + // Wait for a block to be created + s.waitForABlock() + height = s.queryCurrentHeight() + + // Ensure that the transaction was executed + hashes := s.normalTxsToTxHashes([][]byte{ + bidTx, + bundle[0], + freeTx, + normalTx, + }) + expectedExecution := map[string]bool{ + hashes[0]: true, + hashes[1]: true, + hashes[2]: true, + hashes[3]: true, + } + + // Ensure that the block was built correctly + s.verifyBlock(height, hashes, expectedExecution) + }, + }, + } + + for _, tc := range testCases { + s.waitForABlock() + s.Run(tc.name, tc.test) + } +} diff --git a/tests/e2e/e2e_tx_test.go b/tests/e2e/e2e_tx_test.go index 7632b40..726ea02 100644 --- a/tests/e2e/e2e_tx_test.go +++ b/tests/e2e/e2e_tx_test.go @@ -13,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ory/dockertest/v3/docker" "github.com/skip-mev/pob/tests/app" buildertypes "github.com/skip-mev/pob/x/builder/types" @@ -103,6 +104,20 @@ func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress st return s.createTx(account, msgs, sequenceOffset, height) } +// createMsgDelegateTx creates a delegate transaction given the provided signer, validator, amount, sequence number offset +// and block height timeout. +func (s *IntegrationTestSuite) createMsgDelegateTx(account TestAccount, validator string, amount sdk.Coin, sequenceOffset, height uint64) []byte { + msgs := []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: account.Address.String(), + ValidatorAddress: validator, + Amount: amount, + }, + } + + return s.createTx(account, msgs, sequenceOffset, height) +} + // createTx creates a transaction given the provided messages, sequence number offset, and block height timeout. func (s *IntegrationTestSuite) createTx(account TestAccount, msgs []sdk.Msg, sequenceOffset, height uint64) []byte { txConfig := encodingConfig.TxConfig diff --git a/tests/e2e/e2e_utils_test.go b/tests/e2e/e2e_utils_test.go index 51f6ac0..10afcb2 100644 --- a/tests/e2e/e2e_utils_test.go +++ b/tests/e2e/e2e_utils_test.go @@ -16,6 +16,7 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 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" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -144,9 +145,8 @@ func (s *IntegrationTestSuite) normalTxsToTxHashes(txs [][]byte) []string { return hashes } -// verifyBlock verifies that the transactions in the block at the given height were seen -// and executed in the order they were submitted i.e. how they are broadcasted in the bundle. -func (s *IntegrationTestSuite) verifyBlock(height uint64, bundle []string, expectedExecution map[string]bool) { +// verifyTopOfBlockAuction verifies that blocks that include a bid transaction execute as expected. +func (s *IntegrationTestSuite) verifyTopOfBlockAuction(height uint64, bundle []string, expectedExecution map[string]bool) { s.waitForABlock() s.T().Logf("Verifying block %d", height) @@ -179,12 +179,40 @@ func (s *IntegrationTestSuite) verifyBlock(height uint64, bundle []string, expec } } +// verifyBlock verifies that the transactions in the block at the given height were seen +// and executed in the order they were submitted. +func (s *IntegrationTestSuite) verifyBlock(height uint64, txs []string, expectedExecution map[string]bool) { + s.waitForABlock() + s.T().Logf("Verifying block %d", height) + + // Get the block's transactions and display the expected and actual block for debugging. + blockTxs := s.queryBlockTxs(height) + s.displayBlock(blockTxs, txs) + + // Ensure that all transactions executed as expected (i.e. landed or failed to land). + for tx, landed := range expectedExecution { + s.T().Logf("Verifying tx %s executed as %t", tx, landed) + s.Require().Equal(landed, s.queryTxPassed(tx) == nil) + } + s.T().Logf("All txs executed as expected") + + // Check that the block contains the expected transactions in the expected order. + s.Require().Equal(len(txs), len(blockTxs)) + + hashBlockTxs := s.normalTxsToTxHashes(blockTxs) + for index, tx := range txs { + s.Require().Equal(strings.ToUpper(tx), strings.ToUpper(hashBlockTxs[index])) + } + + s.T().Logf("Block %d contains the expected transactions in the expected order", height) +} + // displayExpectedBlock displays the expected and actual blocks. -func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bundle []string) { - if len(bundle) != 0 { - expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, bundle[0]) - for index, bundleTx := range bundle[1:] { - expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx) +func (s *IntegrationTestSuite) displayBlock(txs [][]byte, expectedTxs []string) { + if len(expectedTxs) != 0 { + expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, expectedTxs[0]) + for index, expectedTx := range expectedTxs[1:] { + expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, expectedTx) } s.T().Logf(expectedBlock) @@ -326,3 +354,14 @@ func (s *IntegrationTestSuite) queryBlockTxs(height uint64) [][]byte { return resp.GetSdkBlock().Data.Txs } + +// queryValidators returns the validators of the network. +func (s *IntegrationTestSuite) queryValidators() []stakingtypes.Validator { + queryClient := stakingtypes.NewQueryClient(s.createClientContext()) + + req := &stakingtypes.QueryValidatorsRequest{} + resp, err := queryClient.Validators(context.Background(), req) + s.Require().NoError(err) + + return resp.Validators +}