diff --git a/README.md b/README.md index f388c95..13ac9c0 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,12 @@ $ go install github.com/skip-mev/pob ```go type App struct { - BuilderKeeper builderkeeper.Keeper ... + // BuilderKeeper is the keeper that handles processing auction transactions + BuilderKeeper builderkeeper.Keeper + + // Custom checkTx handler + checkTxHandler abci.CheckTx } ``` @@ -133,7 +137,7 @@ $ go install github.com/skip-mev/pob ``` d. Searchers bid to have their bundles executed at the top of the block - using `MsgAuctionBid` messages. While the builder `Keeper` is capable of + using `MsgAuctionBid` messages (by default). While the builder `Keeper` is capable of tracking valid bids, it is unable to correctly sequence the auction transactions alongside the normal transactions without having access to the application’s mempool. As such, we have to instantiate POB’s custom @@ -141,8 +145,17 @@ $ go install github.com/skip-mev/pob mempool - into the application. Note, this should be done after `BaseApp` is instantiated. + d.1. Application developers can choose to implement their own `AuctionFactory` implementation + or use the default implementation provided by POB. The `AuctionFactory` is responsible + for determining what is an auction bid transaction and how to extract the bid information + from the transaction. The default implementation provided by POB is `DefaultAuctionFactory` + which uses the `MsgAuctionBid` message to determine if a transaction is an auction bid + transaction and extracts the bid information from the message. + ```go - mempool := mempool.NewAuctionMempool(txConfig.TxDecoder(), GetMaxMempoolSize()) + config := mempool.NewDefaultAuctionFactory(txDecoder) + + mempool := mempool.NewAuctionMempool(txDecoder, txEncoder, maxTx, config) bApp.SetMempool(mempool) ``` @@ -153,18 +166,68 @@ $ go install github.com/skip-mev/pob will verify the contents of the block proposal by all validators. The combination of the `AuctionMempool`, `PrepareProposal` and `ProcessProposal` handlers allows the application to verifiably build valid blocks with - top-of-block block space reserved for auctions. + top-of-block block space reserved for auctions. Additionally, we override the + `BaseApp`'s `CheckTx` handler with our own custom `CheckTx` handler that will + be responsible for checking the validity of transactions. We override the + `CheckTx` handler so that we can verify auction transactions before they are + inserted into the mempool. With the POB `CheckTx`, we can verify the auction + transaction and all of the bundled transactions before inserting the auction + transaction into the mempool. This is important because we otherwise there may be + discrepencies between the auction transaction and the bundled transactions + are validated in `CheckTx` and `PrepareProposal` such that the auction can be + griefed. All other transactions will be executed with base app's `CheckTx`. ```go + // Create the entire chain of AnteDecorators for the application. + anteDecorators := []sdk.AnteDecorator{ + auction.NewAuctionDecorator( + app.BuilderKeeper, + txConfig.TxEncoder(), + mempool, + ), + ..., + } + + // Create the antehandler that will be used to check transactions throughout the lifecycle + // of the application. + anteHandler := sdk.ChainAnteDecorators(anteDecorators...) + app.SetAnteHandler(anteHandler) + + // Create the proposal handler that will be used to build and validate blocks. handler := proposalhandler.NewProposalHandler( mempool, bApp.Logger(), - bApp, + anteHandler, txConfig.TxEncoder(), txConfig.TxDecoder(), ) - bApp.SetPrepareProposal(handler.PrepareProposalHandler()) - bApp.SetProcessProposal(handler.ProcessProposalHandler()) + app.SetPrepareProposal(handler.PrepareProposalHandler()) + app.SetProcessProposal(handler.ProcessProposalHandler()) + + // Set the custom CheckTx handler on BaseApp. + checkTxHandler := pobabci.CheckTxHandler( + app.App, + app.TxDecoder, + mempool, + anteHandler, + chainID, + ) + app.SetCheckTx(checkTxHandler) + + ... + + // CheckTx will check the transaction with the provided checkTxHandler. We override the default + // handler so that we can verify bid transactions before they are inserted into the mempool. + // With the POB CheckTx, we can verify the bid transaction and all of the bundled transactions + // before inserting the bid transaction into the mempool. + func (app *TestApp) CheckTx(req cometabci.RequestCheckTx) cometabci.ResponseCheckTx { + return app.checkTxHandler(req) + } + + // SetCheckTx sets the checkTxHandler for the app. + func (app *TestApp) SetCheckTx(handler abci.CheckTx) { + app.checkTxHandler = handler + } ``` f. Finally, update the app's `InitGenesis` order and ante-handler chain. @@ -174,16 +237,6 @@ $ go install github.com/skip-mev/pob buildertypes.ModuleName, ..., } - - anteDecorators := []sdk.AnteDecorator{ - auction.NewAuctionDecorator( - app.BuilderKeeper, - txConfig.TxDecoder(), - txConfig.TxEncoder(), - mempool, - ), - ..., - } ``` ## Params diff --git a/abci/abci_test.go b/abci/abci_test.go index df78c6f..4968338 100644 --- a/abci/abci_test.go +++ b/abci/abci_test.go @@ -97,7 +97,7 @@ func (suite *ABCITestSuite) SetupTest() { ) err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) suite.Require().NoError(err) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) // Accounts set up suite.accounts = testutils.RandomAccounts(suite.random, 1) @@ -487,7 +487,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() { MinBidIncrement: suite.minBidIncrement, } suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) handler := suite.proposalHandler.PrepareProposalHandler() res := handler(suite.ctx, abcitypes.RequestPrepareProposal{ @@ -733,7 +733,7 @@ func (suite *ABCITestSuite) TestProcessProposal() { MinBidIncrement: suite.minBidIncrement, } suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid()) txs := suite.exportMempool(exportRefTxs) diff --git a/abci/check_tx.go b/abci/check_tx.go new file mode 100644 index 0000000..7d96f2d --- /dev/null +++ b/abci/check_tx.go @@ -0,0 +1,214 @@ +package abci + +import ( + "context" + "fmt" + + cometabci "github.com/cometbft/cometbft/abci/types" + log "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/skip-mev/pob/mempool" +) + +type ( + // CheckTxHandler is a wrapper around baseapp's CheckTx method that allows us to + // verify bid transactions against the latest committed state. All other transactions + // are executed normally using base app's CheckTx. This defines all of the + // dependencies that are required to verify a bid transaction. + CheckTxHandler struct { + // baseApp is utilized to retrieve the latest committed state and to call + // baseapp's CheckTx method. + baseApp BaseApp + + // txDecoder is utilized to decode transactions to determine if they are + // bid transactions. + txDecoder sdk.TxDecoder + + // mempool is utilized to retrieve the bid info of a transaction and to + // insert a transaction into the application-side mempool. + mempool CheckTxMempool + + // anteHandler is utilized to verify the bid transaction against the latest + // committed state. + anteHandler sdk.AnteHandler + + // chainID is the chain ID of the blockchain. + chainID string + } + + // CheckTx is baseapp's CheckTx method that checks the validity of a + // transaction. + CheckTx func(cometabci.RequestCheckTx) cometabci.ResponseCheckTx + + // CheckTxMempool is the interface that defines all of the dependencies that + // are required to interact with the application-side mempool. + CheckTxMempool interface { + // GetAuctionBidInfo is utilized to retrieve the bid info of a transaction. + GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error) + + // Insert is utilized to insert a transaction into the application-side mempool. + Insert(ctx context.Context, tx sdk.Tx) error + + // WrapBundleTransaction is utilized to wrap a transaction included in a bid transaction + // into an sdk.Tx. + WrapBundleTransaction(tx []byte) (sdk.Tx, error) + } + + // BaseApp is an interface that allows us to call baseapp's CheckTx method + // as well as retrieve the latest committed state. + BaseApp interface { + // CommitMultiStore is utilized to retrieve the latest committed state. + CommitMultiStore() sdk.CommitMultiStore + + // CheckTx is baseapp's CheckTx method that checks the validity of a + // transaction. + CheckTx(cometabci.RequestCheckTx) cometabci.ResponseCheckTx + + // Logger is utilized to log errors. + Logger() log.Logger + + // LastBlockHeight is utilized to retrieve the latest block height. + LastBlockHeight() int64 + + // GetConsensusParams is utilized to retrieve the consensus params. + GetConsensusParams(ctx sdk.Context) *tmproto.ConsensusParams + } +) + +// NewCheckTxHandler is a constructor for CheckTxHandler. +func NewCheckTxHandler(baseApp BaseApp, txDecoder sdk.TxDecoder, mempool CheckTxMempool, anteHandler sdk.AnteHandler, chainID string) *CheckTxHandler { + return &CheckTxHandler{ + baseApp: baseApp, + txDecoder: txDecoder, + mempool: mempool, + anteHandler: anteHandler, + chainID: chainID, + } +} + +// CheckTxHandler is a wrapper around baseapp's CheckTx method that allows us to +// verify bid transactions against the latest committed state. All other transactions +// are executed normally. We must verify each bid tx and all of its bundled transactions +// before we can insert it into the mempool against the latest commit state because +// otherwise the auction can be griefed. No state changes are applied to the state +// during this process. +func (handler *CheckTxHandler) CheckTx() CheckTx { + return func(req cometabci.RequestCheckTx) (resp cometabci.ResponseCheckTx) { + defer func() { + if err := recover(); err != nil { + resp = sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("panic in check tx handler: %s", err), 0, 0, nil, false) + } + }() + + tx, err := handler.txDecoder(req.Tx) + if err != nil { + return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to decode tx: %w", err), 0, 0, nil, false) + } + + // Attempt to get the bid info of the transaction. + bidInfo, err := handler.mempool.GetAuctionBidInfo(tx) + if err != nil { + return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to get auction bid info: %w", err), 0, 0, nil, false) + } + + // If this is not a bid transaction, we just execute it normally. + if bidInfo == nil { + return handler.baseApp.CheckTx(req) + } + + // We attempt to get the latest committed state in order to verify transactions + // as if they were to be executed at the top of the block. After verification, this + // context will be discarded and will not apply any state changes. + ctx := handler.GetContextForBidTx(req) + + // Verify the bid transaction. + gasInfo, err := handler.ValidateBidTx(ctx, tx, bidInfo) + if err != nil { + return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("invalid bid tx: %w", err), gasInfo.GasWanted, gasInfo.GasUsed, nil, false) + } + + // If the bid transaction is valid, we know we can insert it into the mempool for consideration in the next block. + if err := handler.mempool.Insert(ctx, tx); err != nil { + return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("invalid bid tx; failed to insert bid transaction into mempool: %w", err), gasInfo.GasWanted, gasInfo.GasUsed, nil, false) + } + + return cometabci.ResponseCheckTx{ + Code: cometabci.CodeTypeOK, + GasWanted: int64(gasInfo.GasWanted), + GasUsed: int64(gasInfo.GasUsed), + } + } +} + +// ValidateBidTx is utilized to verify the bid transaction against the latest committed state. +func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *mempool.AuctionBidInfo) (sdk.GasInfo, error) { + // Verify the bid transaction. + ctx, err := handler.anteHandler(ctx, bidTx, false) + if err != nil { + return sdk.GasInfo{}, fmt.Errorf("invalid bid tx; failed to execute ante handler: %w", err) + } + + // Store the gas info and priority of the bid transaction before applying changes with other transactions. + gasInfo := sdk.GasInfo{ + GasWanted: ctx.GasMeter().Limit(), + GasUsed: ctx.GasMeter().GasConsumed(), + } + + // Verify all of the bundled transactions. + for _, tx := range bidInfo.Transactions { + bundledTx, err := handler.mempool.WrapBundleTransaction(tx) + if err != nil { + return gasInfo, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) + } + + bidInfo, err := handler.mempool.GetAuctionBidInfo(bundledTx) + if err != nil { + return gasInfo, fmt.Errorf("invalid bid tx; failed to get auction bid info: %w", err) + } + + // Bid txs cannot be included in bundled txs. + if bidInfo != nil { + return gasInfo, fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx") + } + + if ctx, err = handler.anteHandler(ctx, bundledTx, false); err != nil { + return gasInfo, fmt.Errorf("invalid bid tx; failed to execute bundled transaction: %w", err) + } + } + + return gasInfo, nil +} + +// GetContextForBidTx is returns the latest committed state and sets the context given +// the checkTx request. +func (handler *CheckTxHandler) GetContextForBidTx(req cometabci.RequestCheckTx) sdk.Context { + // Retrieve the commit multi-store which is used to retrieve the latest committed state. + ms := handler.baseApp.CommitMultiStore().CacheMultiStore() + + // Create a new context based off of the latest committed state. + header := tmproto.Header{ + Height: handler.baseApp.LastBlockHeight(), + ChainID: handler.chainID, // TODO: Replace with actual chain ID. This is currently not exposed by the app. + } + ctx, _ := sdk.NewContext(ms, header, true, handler.baseApp.Logger()).CacheContext() + + // Set the context to the correct checking mode. + switch req.Type { + case cometabci.CheckTxType_New: + ctx = ctx.WithIsCheckTx(true) + case cometabci.CheckTxType_Recheck: + ctx = ctx.WithIsReCheckTx(true) + default: + panic("unknown check tx type") + } + + // Set the remaining important context values. + ctx = ctx. + WithTxBytes(req.Tx). + WithEventManager(sdk.NewEventManager()). + WithConsensusParams(handler.baseApp.GetConsensusParams(ctx)) + + return ctx +} diff --git a/abci/v2/abci_test.go b/abci/v2/abci_test.go index c1c3134..9fa5a10 100644 --- a/abci/v2/abci_test.go +++ b/abci/v2/abci_test.go @@ -95,7 +95,7 @@ func (suite *ABCITestSuite) SetupTest() { ) err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams()) suite.Require().NoError(err) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) // Accounts set up suite.accounts = testutils.RandomAccounts(suite.random, 10) diff --git a/abci/v2/proposals_test.go b/abci/v2/proposals_test.go index e411230..541f51c 100644 --- a/abci/v2/proposals_test.go +++ b/abci/v2/proposals_test.go @@ -296,7 +296,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() { MinBidIncrement: suite.minBidIncrement, } suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) // Reset the proposal handler with the new mempool. suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) @@ -749,7 +749,7 @@ func (suite *ABCITestSuite) TestProcessProposal() { MinBidIncrement: suite.minBidIncrement, } suite.builderKeeper.SetParams(suite.ctx, params) - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) // reset the proposal handler with the new mempool suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) diff --git a/tests/app/ante.go b/tests/app/ante.go index 76ac1c1..c7a6709 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -48,7 +48,7 @@ func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler { ante.NewSigGasConsumeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SigGasConsumer), ante.NewSigVerificationDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SignModeHandler), ante.NewIncrementSequenceDecorator(options.BaseOptions.AccountKeeper), - builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxDecoder, options.TxEncoder, options.Mempool), + builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxEncoder, options.Mempool), } return sdk.ChainAnteDecorators(anteDecorators...) diff --git a/tests/app/app.go b/tests/app/app.go index 9b61366..e09e705 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -9,6 +9,7 @@ import ( "cosmossdk.io/depinject" dbm "github.com/cometbft/cometbft-db" + cometabci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" @@ -72,6 +73,10 @@ import ( builderkeeper "github.com/skip-mev/pob/x/builder/keeper" ) +const ( + ChainID = "chain-id-0" +) + var ( BondDenom = sdk.DefaultBondDenom @@ -142,6 +147,9 @@ type TestApp struct { GroupKeeper groupkeeper.Keeper ConsensusParamsKeeper consensuskeeper.Keeper BuilderKeeper builderkeeper.Keeper + + // custom checkTx handler + checkTxHandler abci.CheckTx } func init() { @@ -275,7 +283,7 @@ func New( } anteHandler := NewPOBAnteHandler(options) - // Set the proposal handlers on the BaseApp. + // Set the proposal handlers on the BaseApp along with the custom antehandler. proposalHandlers := abci.NewProposalHandler( mempool, app.App.Logger(), @@ -285,6 +293,17 @@ func New( ) app.App.SetPrepareProposal(proposalHandlers.PrepareProposalHandler()) app.App.SetProcessProposal(proposalHandlers.ProcessProposalHandler()) + app.App.SetAnteHandler(anteHandler) + + // Set the custom CheckTx handler on BaseApp. + checkTxHandler := abci.NewCheckTxHandler( + app.App, + app.txConfig.TxDecoder(), + mempool, + anteHandler, + ChainID, + ) + app.SetCheckTx(checkTxHandler.CheckTx()) // load state streaming if enabled if _, _, err := streaming.LoadStreamingServices(app.App.BaseApp, appOpts, app.appCodec, logger, app.kvStoreKeys()); err != nil { @@ -320,6 +339,19 @@ func New( return app } +// CheckTx will check the transaction with the provided checkTxHandler. We override the default +// handler so that we can verify bid transactions before they are inserted into the mempool. +// With the POB CheckTx, we can verify the bid transaction and all of the bundled transactions +// before inserting the bid transaction into the mempool. +func (app *TestApp) CheckTx(req cometabci.RequestCheckTx) cometabci.ResponseCheckTx { + return app.checkTxHandler(req) +} + +// SetCheckTx sets the checkTxHandler for the app. +func (app *TestApp) SetCheckTx(handler abci.CheckTx) { + app.checkTxHandler = handler +} + // Name returns the name of the App func (app *TestApp) Name() string { return app.BaseApp.Name() } diff --git a/tests/e2e/chain.go b/tests/e2e/chain.go index bcca3b1..365e228 100644 --- a/tests/e2e/chain.go +++ b/tests/e2e/chain.go @@ -6,7 +6,6 @@ import ( dbm "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" - cometrand "github.com/cometbft/cometbft/libs/rand" "github.com/cosmos/cosmos-sdk/codec" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" "github.com/skip-mev/pob/tests/app" @@ -47,7 +46,7 @@ func newChain() (*chain, error) { } return &chain{ - id: "chain-" + cometrand.NewRand().Str(6), + id: app.ChainID, dataDir: tmpDir, }, nil } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 92a2ac5..b15733a 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -7,12 +7,13 @@ import ( "github.com/skip-mev/pob/tests/app" ) +// TestGetBuilderParams tests the query for the builder parameters. func (s *IntegrationTestSuite) TestGetBuilderParams() { params := s.queryBuilderParams() s.Require().NotNil(params) } -// TestBundles tests the execution of various auction bids. There are a few +// TestValidBids tests the execution of various valid auction bids. There are a few // invariants that are tested: // // 1. The order of transactions in a bundle is preserved when bids are valid. @@ -20,10 +21,10 @@ func (s *IntegrationTestSuite) TestGetBuilderParams() { // 3. The balance of the escrow account should be updated correctly. // 4. Top of block bids will be included in block proposals before other transactions // that are included in the same block. -func (s *IntegrationTestSuite) TestBundles() { +func (s *IntegrationTestSuite) TestValidBids() { // Create the accounts that will create transactions to be included in bundles initBalance := sdk.NewInt64Coin(app.BondDenom, 10000000000) - numAccounts := 3 + numAccounts := 4 accounts := s.createTestAccounts(numAccounts, initBalance) // basic send amount @@ -43,360 +44,35 @@ func (s *IntegrationTestSuite) TestBundles() { { name: "Valid auction bid", test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + // Create a bundle with a single transaction - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1, 1000), } // Create a bid transaction that includes the bundle and is valid bid := reserveFee height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("Valid auction bid", bidTxHash, bundle) + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("Valid auction bid", bidTx, bundle) // Wait for a block to be created s.waitForABlock() // Ensure that the block was correctly created and executed in the order expected - bundleHashes := s.bundleToTxHashes(bundle) + bundleHashes := s.bundleToTxHashes(bidTx, bundle) expectedExecution := map[string]bool{ - bidTxHash: true, bundleHashes[0]: true, + bundleHashes[1]: true, } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) + s.verifyBlock(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) - s.Require().Equal(expectedEscrowFee, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "invalid auction bid with a bid smaller than the reserve fee", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction (this should not be included in the block proposal) - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes a bid that is smaller than the reserve fee - bid := reserveFee.Sub(sdk.NewInt64Coin(app.BondDenom, 1)) - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("invalid bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure that no transactions were executed - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "invalid auction bid with too many transactions in the bundle", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with too many transactions - bundle := []string{} - for i := 0; i < int(maxBundleSize)+1; i++ { - bundle = append(bundle, s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, i, 1000)) - } - - // Create a bid transaction that includes the bundle - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("invalid bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure that no transactions were executed - expectedExecution := map[string]bool{ - bidTxHash: false, - } - - bundleHashes := s.bundleToTxHashes(bundle) - for _, hash := range bundleHashes { - expectedExecution[hash] = false - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "invalid auction bid that has an invalid timeout", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and has a bad timeout - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height, bundle) - s.displayExpectedBundle("invalid bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure that no transactions were executed - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Multiple transactions with second bid being smaller than min bid increment", - test: func() { - // Get escrow account balance - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("bid 1", bidTxHash, bundle) - - // Create a second bid transaction that includes the bundle and is valid (but smaller than the min bid increment) - badBid := reserveFee.Add(sdk.NewInt64Coin(app.BondDenom, 10)) - bidTxHash2 := s.execAuctionBidTx(0, badBid, height+1, bundle) - s.displayExpectedBundle("bid 2", bidTxHash2, bundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure only the first bid was executed - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: true, - bundleHashes[0]: true, - bidTxHash2: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - expectedEscrowFee := s.calculateProposerEscrowSplit(bid) - s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Multiple transactions with increasing bids but first bid has same bundle so it should fail", - test: func() { - // Get escrow account balance - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+2, bundle) - s.displayExpectedBundle("bid 1", bidTxHash, bundle) - - // Create a second bid transaction that includes the bundle and is valid - bid2 := reserveFee.Add(minBidIncrement) - bidTxHash2 := s.execAuctionBidTx(1, bid2, height+1, bundle) - s.displayExpectedBundle("bid 2", bidTxHash2, bundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure only the second bid was executed - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: true, - bidTxHash2: true, - } - s.verifyBlock(height+1, bidTxHash2, bundleHashes, expectedExecution) - - // Wait for a block to be created and ensure that the first bid was not executed - s.waitForABlock() - s.verifyBlock(height+2, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) - s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Multiple transactions with increasing bids and different bundles (both should execute)", - test: func() { - // Get escrow account balance - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction - firstBundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bundle with a single transaction - secondBundle := []string{ - s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+2, firstBundle) // height+2 to ensure it is executed after the second bid - s.displayExpectedBundle("bid 1", bidTxHash, firstBundle) - - // Create a second bid transaction that includes the bundle and is valid - bid2 := reserveFee.Add(minBidIncrement) - bidTxHash2 := s.execAuctionBidTx(1, bid2, height+1, secondBundle) - s.displayExpectedBundle("bid 2", bidTxHash2, secondBundle) - - // Wait for a block to be created - s.waitForABlock() - - // Ensure only the second bid was executed - firstBundleHashes := s.bundleToTxHashes(firstBundle) - secondBundleHashes := s.bundleToTxHashes(secondBundle) - expectedExecution := map[string]bool{ - bidTxHash2: true, - secondBundleHashes[0]: true, - } - s.verifyBlock(height+1, bidTxHash2, secondBundleHashes, expectedExecution) - - // Wait for a block to be created and ensure that the second bid is executed - s.waitForABlock() - expectedExecution[bidTxHash] = true - expectedExecution[firstBundleHashes[0]] = true - s.verifyBlock(height+2, bidTxHash, firstBundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - expectedEscrowFee := s.calculateProposerEscrowSplit(bid.Add(bid2)) - s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Invalid bid that includes an invalid bundle tx", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction that is invalid (sequence number is wrong) - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1000, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("bad bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Invalid bid that is attempting to front-run/sandwich", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a front-running bundle - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, 0, 1000), - s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := reserveFee - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("bad bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: false, - bundleHashes[1]: false, - bundleHashes[2]: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) - }, - }, - { - name: "Invalid bid that is attempting to bid more than their balance", - test: func() { - // Get escrow account balance to ensure that it is not changed - escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) - - // Create a bundle with a single transaction that is valid - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - } - - // Create a bid transaction that includes the bundle and is valid - bid := sdk.NewCoin(app.BondDenom, sdk.NewInt(999999999999999999)) - height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("bad bid", bidTxHash, bundle) - - // Wait for a block to be created - s.waitForABlock() - - bundleHashes := s.bundleToTxHashes(bundle) - expectedExecution := map[string]bool{ - bidTxHash: false, - bundleHashes[0]: false, - } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) - - // Ensure that the escrow account has the correct balance - s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) }, }, { @@ -406,11 +82,9 @@ func (s *IntegrationTestSuite) TestBundles() { escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) // Create a bundle with a multiple transaction that is valid - bundle := []string{ - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1, 1000), - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 2, 1000), - s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 3, 1000), + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) } // Wait for a block to ensure all transactions are included in the same block @@ -419,29 +93,216 @@ func (s *IntegrationTestSuite) TestBundles() { // Create a bid transaction that includes the bundle and is valid bid := reserveFee height := s.queryCurrentHeight() - bidTxHash := s.execAuctionBidTx(0, bid, height+1, bundle) - s.displayExpectedBundle("good bid", bidTxHash, bundle) + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("gud auction bid", bidTx, bundle) // Execute a few other messages to be included in the block after the bid and bundle - txHash1 := s.execMsgSendTx(1, accounts[0].Address, sdk.NewCoin(app.BondDenom, sdk.NewInt(100))) - txHash2 := s.execMsgSendTx(2, accounts[0].Address, sdk.NewCoin(app.BondDenom, sdk.NewInt(100))) - txHash3 := s.execMsgSendTx(3, accounts[0].Address, sdk.NewCoin(app.BondDenom, sdk.NewInt(100))) + normalTxs := make([][]byte, 3) + normalTxs[0] = s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 0, 1000) + normalTxs[1] = s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 1, 1000) + normalTxs[2] = s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 2, 1000) + + for _, tx := range normalTxs { + s.broadcastTx(tx, 0) + } // Wait for a block to be created s.waitForABlock() - bundleHashes := s.bundleToTxHashes(bundle) + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) expectedExecution := map[string]bool{ - bidTxHash: true, bundleHashes[0]: true, - bundleHashes[1]: true, - bundleHashes[2]: true, - bundleHashes[3]: true, - txHash1: true, - txHash2: true, - txHash3: true, } - s.verifyBlock(height+1, bidTxHash, bundleHashes, expectedExecution) + + for _, hash := range bundleHashes[1:] { + expectedExecution[hash] = true + } + + for _, hash := range s.normalTxsToTxHashes(normalTxs) { + expectedExecution[hash] = true + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) + }, + }, + { + name: "iterative bidding from the same account", + test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a multiple transaction that is valid + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("gud auction bid 1", bidTx, bundle) + + // Create another bid transaction that includes the bundle and is valid from the same account + // to verify that user can bid with the same account multiple times in the same block + bid2 := bid.Add(minBidIncrement) + bidTx2 := s.createAuctionBidTx(accounts[1], bid2, bundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("gud auction bid 2", bidTx2, bundle) + + // Create a third bid + bid3 := bid2.Add(minBidIncrement) + bidTx3 := s.createAuctionBidTx(accounts[1], bid3, bundle, 0, height+1) + s.broadcastTx(bidTx3, 0) + s.displayExpectedBundle("gud auction bid 3", bidTx3, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle) + bundleHashes3 := s.bundleToTxHashes(bidTx3, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes2[0]: false, + bundleHashes3[0]: true, + } + + for _, hash := range bundleHashes3[1:] { + expectedExecution[hash] = true + } + + s.verifyBlock(height+1, bundleHashes3, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid3) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) + }, + }, + { + name: "bid with a bundle with transactions that are already in the mempool", + test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a multiple transaction that is valid + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, bundle, 0, height+1) + s.displayExpectedBundle("gud auction bid", bidTx, bundle) + + // Broadcast all of the transactions in the bundle to the mempool + for _, tx := range bundle { + s.broadcastTx(tx, 0) + } + + // Broadcast the bid transaction + s.broadcastTx(bidTx, 0) + + // Broadcast some other transactions to the mempool + normalTxs := make([][]byte, 10) + for i := 0; i < 10; i++ { + normalTxs[i] = s.createMsgSendTx(accounts[1], accounts[3].Address.String(), defaultSendAmount, uint64(i), 1000) + s.broadcastTx(normalTxs[i], 0) + } + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: true, + } + + for _, hash := range bundleHashes[1:] { + expectedExecution[hash] = true + } + + for _, hash := range s.normalTxsToTxHashes(normalTxs) { + expectedExecution[hash] = true + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) + }, + }, + { + name: "searcher attempts to include several txs in the same block to invalidate auction (we extract bid regardless)", + test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a multiple transaction that is valid + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + + // Execute a few other messages to be included in the block after the bid and bundle + normalTxs := make([][]byte, 3) + normalTxs[0] = s.createMsgSendTx(accounts[1], accounts[1].Address.String(), defaultSendAmount, 0, 1000) + normalTxs[1] = s.createMsgSendTx(accounts[1], accounts[1].Address.String(), defaultSendAmount, 1, 1000) + normalTxs[2] = s.createMsgSendTx(accounts[1], accounts[1].Address.String(), defaultSendAmount, 2, 1000) + + for _, tx := range normalTxs { + s.broadcastTx(tx, 0) + } + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: true, + } + + // The entire bundle should land irrespective of the transactions submitted by the searcher + for _, hash := range bundleHashes[1:] { + expectedExecution[hash] = true + } + + // We expect only the first normal transaction to not be executed (due to a sequence number mismatch) + normalHashes := s.normalTxsToTxHashes(normalTxs) + expectedExecution[normalHashes[0]] = false + for _, hash := range normalHashes[1:] { + expectedExecution[hash] = true + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) // Ensure that the escrow account has the correct balance expectedEscrowFee := s.calculateProposerEscrowSplit(bid) @@ -455,3 +316,689 @@ func (s *IntegrationTestSuite) TestBundles() { s.Run(tc.name, tc.test) } } + +// TestMultipleBids tests the execution of various valid auction bids in the same block. There are a few +// invariants that are tested: +// +// 1. The order of transactions in a bundle is preserved when bids are valid. +// 2. All transactions execute as expected. +// 3. The balance of the escrow account should be updated correctly. +// 4. Top of block bids will be included in block proposals before other transactions +// that are included in the same block. +// 5. If there is a block that has multiple valid bids with timeouts that are sufficiently far apart, +// the bids should be executed respecting the highest bids until the timeout is reached. +func (s *IntegrationTestSuite) TestMultipleBids() { + // 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) + + // basic send amount + defaultSendAmount := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(10))) + + // auction parameters + params := s.queryBuilderParams() + reserveFee := params.ReserveFee + minBidIncrement := params.MinBidIncrement + maxBundleSize := params.MaxBundleSize + escrowAddress := params.EscrowAccountAddress + + testCases := []struct { + name string + test func() + }{ + { + name: "broadcasting bids to two different validators (both should execute over several blocks) with same bid", + test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a multiple transaction that is valid + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + bundle2 := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle2[i] = s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, bundle, 0, height+5) + + // Createa a second bid transaction that includes the bundle and is valid + bid2 := reserveFee.Add(sdk.NewCoin(app.BondDenom, sdk.NewInt(10))) + bidTx2 := s.createAuctionBidTx(accounts[3], bid2, bundle2, 0, height+5) + + // Broadcast the transactions to different validators + s.broadcastTx(bidTx, 0) + s.broadcastTx(bidTx2, 1) + + s.displayExpectedBundle("gud auction bid 1", bidTx, bundle) + s.displayExpectedBundle("gud auction bid 2", bidTx2, bundle2) + + // Wait for both blocks to be created to verify that both bids were executed + s.waitForABlock() + s.waitForABlock() + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle2) + expectedExecution := map[string]bool{ + bundleHashes[0]: true, + bundleHashes2[0]: true, + } + + for _, hash := range bundleHashes[1:] { + expectedExecution[hash] = true + } + + for _, hash := range bundleHashes2[1:] { + expectedExecution[hash] = true + } + + // Pass in nil since we don't know the order of transactions that ill be executed + s.verifyBlock(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)) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) + }, + }, + { + name: "multi-block auction bids with different bids", + test: func() { + // Get escrow account balance to ensure that it is updated correctly + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a multiple transaction that is valid + bundle := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + bundle2 := make([][]byte, maxBundleSize) + for i := 0; i < int(maxBundleSize); i++ { + bundle2[i] = s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, bundle, 0, height+2) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("gud auction bid 1", bidTx, bundle) + + // Create another bid transaction that includes the bundle and is valid from a different account + bid2 := bid.Add(minBidIncrement) + bidTx2 := s.createAuctionBidTx(accounts[3], bid2, bundle2, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("gud auction bid 2", bidTx2, bundle2) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle2) + expectedExecution := map[string]bool{ + bundleHashes2[0]: true, + } + + for _, hash := range bundleHashes2[1:] { + expectedExecution[hash] = true + } + + s.verifyBlock(height+1, bundleHashes2, expectedExecution) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that the block was correctly created and executed in the order expected + expectedExecution = map[string]bool{ + bundleHashes[0]: true, + } + + for _, hash := range bundleHashes[1:] { + expectedExecution[hash] = true + } + + s.verifyBlock(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)) + s.Require().Equal(escrowBalance.Add(expectedEscrowFee), s.queryBalanceOf(escrowAddress, app.BondDenom)) + }, + }, + { + name: "Multiple bid transactions with second bid being smaller than min bid increment (same account)", + test: func() { + // Get escrow account balance + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a single transaction + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1, 1000), + } + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bid 1", bidTx, bundle) + + // Create a second bid transaction that includes the bundle and is valid (but smaller than the min bid increment) + badBid := reserveFee.Add(sdk.NewInt64Coin(app.BondDenom, 10)) + bidTx2 := s.createAuctionBidTx(accounts[0], badBid, bundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("bid 2", bidTx2, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure only the first bid was executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: true, + bundleHashes[1]: true, + bundleHashes2[0]: false, + } + s.verifyBlock(height+1, bundleHashes, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid) + s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) + + // Wait another block to make sure the second bid is not executed + s.waitForABlock() + s.verifyBlock(height+2, bundleHashes2, expectedExecution) + }, + }, + { + name: "Multiple transactions with second bid being smaller than min bid increment (different account)", + test: func() { + // Get escrow account balance + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a single transaction + bundle := [][]byte{ + s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bid 1", bidTx, bundle) + + // Create a second bid transaction that includes the bundle and is valid (but smaller than the min bid increment) + badBid := reserveFee.Add(sdk.NewInt64Coin(app.BondDenom, 10)) + bidTx2 := s.createAuctionBidTx(accounts[1], badBid, bundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("bid 2", bidTx2, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure only the first bid was executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: true, + bundleHashes[1]: true, + bundleHashes2[0]: false, + } + s.verifyBlock(height+1, bundleHashes, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid) + s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) + + // Wait another block to make sure the second bid is not executed + s.waitForABlock() + s.verifyBlock(height+2, bundleHashes2, expectedExecution) + }, + }, + { + name: "Multiple transactions with increasing bids but first bid has same bundle so it should fail in later block (same account)", + test: func() { + // Get escrow account balance + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a single transaction + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1, 1000), + } + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+2) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bid 1", bidTx, bundle) + + // Create a second bid transaction that includes the bundle and is valid + bid2 := reserveFee.Add(minBidIncrement) + bidTx2 := s.createAuctionBidTx(accounts[0], bid2, bundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("bid 2", bidTx2, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure only the second bid was executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes2[0]: true, + bundleHashes2[1]: true, + } + s.verifyBlock(height+1, bundleHashes2, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) + s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) + + // Wait for a block to be created and ensure that the first bid was not executed + s.waitForABlock() + s.verifyBlock(height+2, bundleHashes, expectedExecution) + }, + }, + { + name: "Multiple transactions with increasing bids but first bid has same bundle so it should fail in later block (different account)", + test: func() { + // Get escrow account balance + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a single transaction + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, bundle, 0, height+2) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bid 1", bidTx, bundle) + + // Create a second bid transaction that includes the bundle and is valid + bid2 := reserveFee.Add(minBidIncrement) + bidTx2 := s.createAuctionBidTx(accounts[1], bid2, bundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("bid 2", bidTx2, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure only the second bid was executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes2[0]: true, + bundleHashes2[1]: true, + } + s.verifyBlock(height+1, bundleHashes2, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) + s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) + + // Wait for a block to be created and ensure that the first bid was not executed + s.waitForABlock() + s.verifyBlock(height+2, bundleHashes, expectedExecution) + }, + }, + { + name: "Multiple transactions with increasing bids and different bundles (one should execute)", + test: func() { + // Get escrow account balance + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Create a bundle with a single transaction + firstBundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bundle with a single transaction + secondBundle := [][]byte{ + s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle and is valid + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[2], bid, firstBundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bid 1", bidTx, firstBundle) + + // Create a second bid transaction that includes the bundle and is valid + bid2 := reserveFee.Add(minBidIncrement) + bidTx2 := s.createAuctionBidTx(accounts[3], bid2, secondBundle, 0, height+1) + s.broadcastTx(bidTx2, 0) + s.displayExpectedBundle("bid 2", bidTx2, secondBundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure only the second bid was executed + bundleHashes := s.bundleToTxHashes(bidTx, firstBundle) + bundleHashes2 := s.bundleToTxHashes(bidTx2, secondBundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + bundleHashes2[0]: true, + bundleHashes2[1]: true, + } + s.verifyBlock(height+1, bundleHashes2, expectedExecution) + + // Ensure that the escrow account has the correct balance + expectedEscrowFee := s.calculateProposerEscrowSplit(bid2) + s.Require().Equal(expectedEscrowFee.Add(escrowBalance), s.queryBalanceOf(escrowAddress, app.BondDenom)) + + // Wait for a block to be created and ensure that the second bid is executed + s.waitForABlock() + s.verifyBlock(height+2, bundleHashes, expectedExecution) + }, + }, + } + + for _, tc := range testCases { + s.waitForABlock() + s.Run(tc.name, tc.test) + } +} + +// TestInvalidBundles tests that the application correctly rejects invalid bundles. The balance of the escrow +// account should not be updated and bid + transactions in the bundle should not be executed unless if the transactions +// in the bundle were already in the mempool. +func (s *IntegrationTestSuite) TestInvalidBids() { + // 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) + + // basic send amount + defaultSendAmount := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(10))) + + // auction parameters + params := s.queryBuilderParams() + reserveFee := params.ReserveFee + maxBundleSize := params.MaxBundleSize + escrowAddress := params.EscrowAccountAddress + + testCases := []struct { + name string + test func() + }{ + { + name: "searcher is attempting to submit a bundle that includes another bid tx", + test: func() { + // Create a bundle with a multiple transaction that is valid + bundle := [][]byte{ + s.createAuctionBidTx(accounts[0], reserveFee, nil, 0, 1000), + } + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Create a bid transaction that includes the bundle + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bad auction bid", bidTx, bundle) + + // Ensure that the block was built correctly and that the bid was not executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "Invalid bid that is attempting to bid more than their balance", + test: func() { + // Create a bundle with a single transaction that is valid + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle that is attempting to bid more than their balance + bid := sdk.NewCoin(app.BondDenom, sdk.NewInt(999999999999999999)) + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("bad auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + } + + // Ensure that the block was built correctly and that the bid was not executed + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "Invalid bid that is attempting to front-run/sandwich", + test: func() { + // Create a front-running bundle + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + s.createMsgSendTx(accounts[1], accounts[0].Address.String(), defaultSendAmount, 0, 1000), + s.createMsgSendTx(accounts[2], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("front-running auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + bundleHashes[2]: false, + bundleHashes[3]: false, + } + + // Ensure that the block was built correctly and that the bid was not executed + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "Invalid bid that includes an invalid bundle tx", + test: func() { + // Create a bundle with a single transaction that is invalid (sequence number is wrong) + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1000, 1000), + } + + // Create a bid transaction that includes the bundle + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("invalid auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + } + + // Ensure that the block was built correctly and that the bid was not executed + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "invalid auction bid with a bid smaller than the reserve fee", + test: func() { + // Create a bundle with a single transaction (this should not be included in the block proposal) + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 1, 1000), + } + + // Create a bid transaction that includes a bid that is smaller than the reserve fee + bid := reserveFee.Sub(sdk.NewInt64Coin(app.BondDenom, 1)) + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("invalid auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that no transactions were executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "invalid auction bid with too many transactions in the bundle", + test: func() { + // Create a bundle with too many transactions + bundle := [][]byte{} + for i := 0; i < int(maxBundleSize)+1; i++ { + bundle = append(bundle, s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i+1), 1000)) + } + + // Create a bid transaction that includes the bundle + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height+1) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("invalid auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that no transactions were executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := make(map[string]bool) + + for _, hash := range bundleHashes { + expectedExecution[hash] = false + } + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "invalid auction bid that has an invalid timeout", + test: func() { + // Create a bundle with a single transaction + bundle := [][]byte{ + s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, 0, 1000), + } + + // Create a bid transaction that includes the bundle and has a bad timeout + bid := reserveFee + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[0], bid, bundle, 0, height) + s.broadcastTx(bidTx, 0) + s.displayExpectedBundle("invalid auction bid", bidTx, bundle) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that no transactions were executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := map[string]bool{ + bundleHashes[0]: false, + bundleHashes[1]: false, + } + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + { + name: "invalid bid that includes valid transactions that are in the mempool (only bundled txs should execute)", + test: func() { + // Create a bundle with multiple transactions + bundle := make([][]byte, 3) + for i := 0; i < 3; i++ { + bundle[i] = s.createMsgSendTx(accounts[0], accounts[1].Address.String(), defaultSendAmount, uint64(i), 1000) + } + + // Create a bid transaction that includes the bundle and is invalid + bid := reserveFee.Sub(sdk.NewInt64Coin(app.BondDenom, 1)) + height := s.queryCurrentHeight() + bidTx := s.createAuctionBidTx(accounts[1], bid, bundle, 0, height+1) + s.displayExpectedBundle("invalid auction bid", bidTx, bundle) + + // Wait for a block to ensure all transactions are included in the same block + s.waitForABlock() + + // Broadcast all of the transactions in the bundle + for _, tx := range bundle { + s.broadcastTx(tx, 0) + } + + // Broadcast the bid transaction + s.broadcastTx(bidTx, 0) + + // Wait for a block to be created + s.waitForABlock() + + // Ensure that only the transactions in the bundle were executed + bundleHashes := s.bundleToTxHashes(bidTx, bundle) + expectedExecution := make(map[string]bool) + + for _, hash := range bundleHashes { + expectedExecution[hash] = true + } + + expectedExecution[bundleHashes[0]] = false + + s.verifyBlock(height+1, bundleHashes, expectedExecution) + }, + }, + } + + for _, tc := range testCases { + escrowBalance := s.queryBalanceOf(escrowAddress, app.BondDenom) + + // Wait for a block to be created and run the test + s.waitForABlock() + s.Run(tc.name, tc.test) + + // Get escrow account balance to ensure that it is not changed + s.Require().Equal(escrowBalance, s.queryBalanceOf(escrowAddress, app.BondDenom)) + } +} diff --git a/tests/e2e/e2e_tx_test.go b/tests/e2e/e2e_tx_test.go index 6f0a30c..7632b40 100644 --- a/tests/e2e/e2e_tx_test.go +++ b/tests/e2e/e2e_tx_test.go @@ -3,7 +3,6 @@ package e2e import ( "bytes" "context" - "encoding/hex" "fmt" "strings" "time" @@ -16,68 +15,9 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/ory/dockertest/v3/docker" "github.com/skip-mev/pob/tests/app" + buildertypes "github.com/skip-mev/pob/x/builder/types" ) -// execAuctionBidTx executes an auction bid transaction on the given validator given the provided -// bid, timeout, and bundle. This function returns the transaction hash. It does not wait for the -// transaction to be committed. -func (s *IntegrationTestSuite) execAuctionBidTx(valIdx int, bid sdk.Coin, timeout int64, bundle []string) string { - address, err := s.chain.validators[valIdx].keyInfo.GetAddress() - s.Require().NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ - Context: ctx, - AttachStdout: true, - AttachStderr: true, - Container: s.valResources[valIdx].Container.ID, - User: "root", - Cmd: []string{ - "testappd", - "tx", - "builder", - "auction-bid", - address.String(), // bidder - bid.String(), // bid - strings.Join(bundle, ","), // bundle - fmt.Sprintf("--%s=%d", flags.FlagTimeoutHeight, timeout), // timeout - fmt.Sprintf("--%s=%s", flags.FlagFrom, s.chain.validators[valIdx].keyInfo.Name), - fmt.Sprintf("--%s=%s", flags.FlagChainID, s.chain.id), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoin(app.BondDenom, sdk.NewInt(1000000000)).String()), - "--keyring-backend=test", - "--broadcast-mode=sync", - "-y", - }, - }) - s.Require().NoError(err) - - var ( - outBuf bytes.Buffer - errBuf bytes.Buffer - ) - - err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ - Context: ctx, - Detach: false, - OutputStream: &outBuf, - ErrorStream: &errBuf, - }) - s.Require().NoErrorf(err, "stdout: %s, stderr: %s", outBuf.String(), errBuf.String()) - - output := outBuf.String() - resp := strings.Split(output, ":") - txHash := strings.TrimSpace(resp[len(resp)-1]) - - s.T().Logf( - "broadcasted bid tx %s with bid %s timeout %d and %d bundled txs", - txHash, bid, timeout, len(bundle), - ) - - return txHash -} - // execMsgSendTx executes a send transaction on the given validator given the provided // recipient and amount. This function returns the transaction hash. It does not wait for the // transaction to be committed. @@ -136,12 +76,22 @@ func (s *IntegrationTestSuite) execMsgSendTx(valIdx int, to sdk.AccAddress, amou return txHash } +// createAuctionBidTx creates a transaction that bids on an auction given the provided bidder, bid, and transactions. +func (s *IntegrationTestSuite) createAuctionBidTx(account TestAccount, bid sdk.Coin, transactions [][]byte, sequenceOffset, height uint64) []byte { + msgs := []sdk.Msg{ + &buildertypes.MsgAuctionBid{ + Bidder: account.Address.String(), + Bid: bid, + Transactions: transactions, + }, + } + + return s.createTx(account, msgs, sequenceOffset, height) +} + // createMsgSendTx creates a send transaction given the provided signer, recipient, amount, sequence number offset, and block height timeout. // This function is primarily used to create bundles of transactions. -func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress string, amount sdk.Coins, sequenceOffset, height int) string { - txConfig := encodingConfig.TxConfig - txBuilder := txConfig.NewTxBuilder() - +func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress string, amount sdk.Coins, sequenceOffset, height uint64) []byte { msgs := []sdk.Msg{ &banktypes.MsgSend{ FromAddress: account.Address.String(), @@ -150,15 +100,23 @@ func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress st }, } + 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 + txBuilder := txConfig.NewTxBuilder() + // Get account info of the sender to set the account number and sequence number baseAccount := s.queryAccount(account.Address) - sequenceNumber := baseAccount.Sequence + uint64(sequenceOffset) + sequenceNumber := baseAccount.Sequence + sequenceOffset // Set the messages, fees, and timeout. txBuilder.SetMsgs(msgs...) txBuilder.SetGasLimit(5000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(75000)))) - txBuilder.SetTimeoutHeight(uint64(height)) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(150000)))) + txBuilder.SetTimeoutHeight(height) sigV2 := signing.SignatureV2{ PubKey: account.PrivateKey.PubKey(), @@ -191,8 +149,5 @@ func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress st bz, err := txConfig.TxEncoder()(txBuilder.GetTx()) s.Require().NoError(err) - // Hex encode the transaction - hash := hex.EncodeToString(bz) - - return hash + return bz } diff --git a/tests/e2e/e2e_utils_test.go b/tests/e2e/e2e_utils_test.go index b9c12bd..51f6ac0 100644 --- a/tests/e2e/e2e_utils_test.go +++ b/tests/e2e/e2e_utils_test.go @@ -116,15 +116,29 @@ func (s *IntegrationTestSuite) waitForABlock() { } // bundleToTxHashes converts a bundle to a slice of transaction hashes. -func (s *IntegrationTestSuite) bundleToTxHashes(bundle []string) []string { - hashes := make([]string, len(bundle)) +func (s *IntegrationTestSuite) bundleToTxHashes(bidTx []byte, bundle [][]byte) []string { + hashes := make([]string, len(bundle)+1) - for i, tx := range bundle { - hashBz, err := hex.DecodeString(tx) - s.Require().NoError(err) + // encode the bid transaction into a hash + hashBz := sha256.Sum256(bidTx) + hash := hex.EncodeToString(hashBz[:]) + hashes[0] = hash - shaBz := sha256.Sum256(hashBz) - hashes[i] = hex.EncodeToString(shaBz[:]) + for i, hash := range s.normalTxsToTxHashes(bundle) { + hashes[i+1] = hash + } + + return hashes +} + +// normalTxsToTxHashes converts a slice of normal transactions to a slice of transaction hashes. +func (s *IntegrationTestSuite) normalTxsToTxHashes(txs [][]byte) []string { + hashes := make([]string, len(txs)) + + for i, tx := range txs { + hashBz := sha256.Sum256(tx) + hash := hex.EncodeToString(hashBz[:]) + hashes[i] = hash } return hashes @@ -132,13 +146,13 @@ func (s *IntegrationTestSuite) bundleToTxHashes(bundle []string) []string { // 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 int64, bidTx string, bundle []string, expectedExecution map[string]bool) { +func (s *IntegrationTestSuite) verifyBlock(height uint64, bundle []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. txs := s.queryBlockTxs(height) - s.displayBlock(txs, bidTx, bundle) + s.displayBlock(txs, bundle) // Ensure that all transactions executed as expected (i.e. landed or failed to land). for tx, landed := range expectedExecution { @@ -149,28 +163,32 @@ func (s *IntegrationTestSuite) verifyBlock(height int64, bidTx string, bundle [] // Check that the block contains the expected transactions in the expected order // iff the bid transaction was expected to execute. - if expectedExecution[bidTx] { - hashBz := sha256.Sum256(txs[0]) - hash := hex.EncodeToString(hashBz[:]) - s.Require().Equal(strings.ToUpper(bidTx), strings.ToUpper(hash)) + if len(bundle) > 0 && expectedExecution[bundle[0]] { + if expectedExecution[bundle[0]] { + hashBz := sha256.Sum256(txs[0]) + hash := hex.EncodeToString(hashBz[:]) + s.Require().Equal(strings.ToUpper(bundle[0]), strings.ToUpper(hash)) - for index, bundleTx := range bundle { - hashBz := sha256.Sum256(txs[index+1]) - txHash := hex.EncodeToString(hashBz[:]) + for index, bundleTx := range bundle[1:] { + hashBz := sha256.Sum256(txs[index+1]) + txHash := hex.EncodeToString(hashBz[:]) - s.Require().Equal(strings.ToUpper(bundleTx), strings.ToUpper(txHash)) + s.Require().Equal(strings.ToUpper(bundleTx), strings.ToUpper(txHash)) + } } } } // displayExpectedBlock displays the expected and actual blocks. -func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bidTx string, bundle []string) { - expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, bidTx) - for index, bundleTx := range bundle { - expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx) - } +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) + } - s.T().Logf(expectedBlock) + s.T().Logf(expectedBlock) + } // Display the actual block. if len(txs) == 0 { @@ -192,15 +210,35 @@ func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bidTx string, bundle [ } // displayExpectedBundle displays the expected order of the bid and bundled transactions. -func (s *IntegrationTestSuite) displayExpectedBundle(prefix, bidTx string, bundle []string) { - expectedBundle := fmt.Sprintf("%s expected bundle:\n\t(%d, %s)\n", prefix, 0, bidTx) - for index, bundleTx := range s.bundleToTxHashes(bundle) { +func (s *IntegrationTestSuite) displayExpectedBundle(prefix string, bidTx []byte, bundle [][]byte) { + // encode the bid transaction into a hash + hashes := s.bundleToTxHashes(bidTx, bundle) + + expectedBundle := fmt.Sprintf("%s expected bundle:\n\t(%d, %s)\n", prefix, 0, hashes[0]) + for index, bundleTx := range hashes[1:] { expectedBundle += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx) } s.T().Logf(expectedBundle) } +// broadcastTx broadcasts a transaction to the network using the given validator. +func (s *IntegrationTestSuite) broadcastTx(tx []byte, valIdx int) { + node := s.valResources[valIdx] + gRPCURI := node.GetHostPort("9090/tcp") + + grpcConn, err := grpc.Dial( + gRPCURI, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + + client := txtypes.NewServiceClient(grpcConn) + + req := &txtypes.BroadcastTxRequest{TxBytes: tx, Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC} + _, err = client.BroadcastTx(context.Background(), req) + s.Require().NoError(err) +} + // queryTx queries a transaction by its hash and returns whether there was an // error in including the transaction in a block. func (s *IntegrationTestSuite) queryTxPassed(txHash string) error { @@ -268,21 +306,21 @@ func (s *IntegrationTestSuite) queryAccount(address sdk.AccAddress) *authtypes.B } // queryCurrentHeight returns the current block height. -func (s *IntegrationTestSuite) queryCurrentHeight() int64 { +func (s *IntegrationTestSuite) queryCurrentHeight() uint64 { queryClient := tmclient.NewServiceClient(s.createClientContext()) req := &tmclient.GetLatestBlockRequest{} resp, err := queryClient.GetLatestBlock(context.Background(), req) s.Require().NoError(err) - return resp.SdkBlock.Header.Height + return uint64(resp.SdkBlock.Header.Height) } // queryBlockTxs returns the txs of the block at the given height. -func (s *IntegrationTestSuite) queryBlockTxs(height int64) [][]byte { +func (s *IntegrationTestSuite) queryBlockTxs(height uint64) [][]byte { queryClient := tmclient.NewServiceClient(s.createClientContext()) - req := &tmclient.GetBlockByHeightRequest{Height: height} + req := &tmclient.GetBlockByHeightRequest{Height: int64(height)} resp, err := queryClient.GetBlockByHeight(context.Background(), req) s.Require().NoError(err) diff --git a/x/builder/ante/ante.go b/x/builder/ante/ante.go index d0be9bd..00a2d49 100644 --- a/x/builder/ante/ante.go +++ b/x/builder/ante/ante.go @@ -14,24 +14,24 @@ import ( var _ sdk.AnteDecorator = BuilderDecorator{} type ( + // Mempool is an interface that defines the methods required to interact with the application-side mempool. Mempool interface { Contains(tx sdk.Tx) (bool, error) GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error) GetTopAuctionTx(ctx context.Context) sdk.Tx } + // BuilderDecorator is an AnteDecorator that validates the auction bid and bundled transactions. BuilderDecorator struct { builderKeeper keeper.Keeper - txDecoder sdk.TxDecoder txEncoder sdk.TxEncoder mempool Mempool } ) -func NewBuilderDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator { +func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator { return BuilderDecorator{ builderKeeper: ak, - txDecoder: txDecoder, txEncoder: txEncoder, mempool: mempool, } @@ -39,20 +39,20 @@ func NewBuilderDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, txEncoder sd // AnteHandle validates that the auction bid is valid if one exists. If valid it will deduct the entrance fee from the // bidder's account. -func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { +func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { // If comet is re-checking a transaction, we only need to check if the transaction is in the application-side mempool. if ctx.IsReCheckTx() { - contains, err := ad.mempool.Contains(tx) + contains, err := bd.mempool.Contains(tx) if err != nil { return ctx, err } if !contains { - return ctx, fmt.Errorf("transaction not found in application mempool") + return ctx, fmt.Errorf("transaction not found in application-side mempool") } } - bidInfo, err := ad.mempool.GetAuctionBidInfo(tx) + bidInfo, err := bd.mempool.GetAuctionBidInfo(tx) if err != nil { return ctx, err } @@ -60,8 +60,8 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // Validate the auction bid if one exists. if bidInfo != nil { // Auction transactions must have a timeout set to a valid block height. - if int64(bidInfo.Timeout) < ctx.BlockHeight() { - return ctx, fmt.Errorf("timeout height cannot be less than the current block height") + if err := bd.ValidateTimeout(ctx, int64(bidInfo.Timeout)); err != nil { + return ctx, err } // We only need to verify the auction bid relative to the local validator's mempool if the mode @@ -70,20 +70,20 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // poor liveness guarantees. topBid := sdk.Coin{} if ctx.IsCheckTx() || ctx.IsReCheckTx() { - if topBidTx := ad.mempool.GetTopAuctionTx(ctx); topBidTx != nil { - topBidBz, err := ad.txEncoder(topBidTx) + if topBidTx := bd.mempool.GetTopAuctionTx(ctx); topBidTx != nil { + topBidBz, err := bd.txEncoder(topBidTx) if err != nil { return ctx, err } - currentTxBz, err := ad.txEncoder(tx) + currentTxBz, err := bd.txEncoder(tx) if err != nil { return ctx, err } // Compare the bytes to see if the current transaction is the highest bidding transaction. if !bytes.Equal(topBidBz, currentTxBz) { - topBidInfo, err := ad.mempool.GetAuctionBidInfo(topBidTx) + topBidInfo, err := bd.mempool.GetAuctionBidInfo(topBidTx) if err != nil { return ctx, err } @@ -93,10 +93,31 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, } } - if err := ad.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo); err != nil { + if err := bd.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo); err != nil { return ctx, errors.Wrap(err, "failed to validate auction bid") } } return next(ctx, tx, simulate) } + +// ValidateTimeout validates that the timeout is greater than or equal to the expected block height +// the bid transaction will be executed in. +// +// TODO: This will be deprecated in favor of the pre-commit hook once this available on the SDK +// https://github.com/skip-mev/pob/issues/147 +func (bd BuilderDecorator) ValidateTimeout(ctx sdk.Context, timeout int64) error { + currentBlockHeight := ctx.BlockHeight() + + // If the mode is CheckTx or ReCheckTx, we increment the current block height by one to + // account for the fact that the transaction will be executed in the next block. + if ctx.IsCheckTx() || ctx.IsReCheckTx() { + currentBlockHeight++ + } + + if timeout < currentBlockHeight { + return fmt.Errorf("timeout height cannot be less than the current block height") + } + + return nil +} diff --git a/x/builder/ante/ante_test.go b/x/builder/ante/ante_test.go index 046f926..4253aa7 100644 --- a/x/builder/ante/ante_test.go +++ b/x/builder/ante/ante_test.go @@ -248,7 +248,7 @@ func (suite *AnteTestSuite) TestAnteHandler() { suite.Require().NoError(err) // Execute the ante handler - suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), mempool) + suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), mempool) _, err = suite.executeAnteHandler(auctionTx, balance) if tc.pass { suite.Require().NoError(err)