From 07c76b8330ceddc69f81a2c0ce050e7a07e8c201 Mon Sep 17 00:00:00 2001 From: David Terpay <35130517+davidterpay@users.noreply.github.com> Date: Thu, 4 May 2023 14:31:42 -0400 Subject: [PATCH] feat(v1): [ENG-790] Implementing vote extension handlers (#103) Co-authored-by: Aleksandr Bezobchuk --- abci/abci_test.go | 264 +++++++++++++++++++++++++ abci/proposals_test.go | 255 +----------------------- abci/vote_extensions.go | 140 ++++++++++++- abci/vote_extensions_test.go | 370 +++++++++++++++++++++++++++++++++++ 4 files changed, 771 insertions(+), 258 deletions(-) create mode 100644 abci/abci_test.go diff --git a/abci/abci_test.go b/abci/abci_test.go new file mode 100644 index 0000000..30a1364 --- /dev/null +++ b/abci/abci_test.go @@ -0,0 +1,264 @@ +package abci_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/skip-mev/pob/abci" + "github.com/skip-mev/pob/mempool" + testutils "github.com/skip-mev/pob/testutils" + "github.com/skip-mev/pob/x/builder/ante" + "github.com/skip-mev/pob/x/builder/keeper" + buildertypes "github.com/skip-mev/pob/x/builder/types" + "github.com/stretchr/testify/suite" +) + +type ABCITestSuite struct { + suite.Suite + ctx sdk.Context + + // mempool setup + mempool *mempool.AuctionMempool + logger log.Logger + encodingConfig testutils.EncodingConfig + proposalHandler *abci.ProposalHandler + voteExtensionHandler *abci.VoteExtensionHandler + config mempool.Config + txs map[string]struct{} + + // auction bid setup + auctionBidAmount sdk.Coin + minBidIncrement sdk.Coin + + // builder setup + builderKeeper keeper.Keeper + bankKeeper *testutils.MockBankKeeper + accountKeeper *testutils.MockAccountKeeper + distrKeeper *testutils.MockDistributionKeeper + stakingKeeper *testutils.MockStakingKeeper + builderDecorator ante.BuilderDecorator + key *storetypes.KVStoreKey + authorityAccount sdk.AccAddress + + // account set up + accounts []testutils.Account + balances sdk.Coins + random *rand.Rand + nonces map[string]uint64 +} + +func TestABCISuite(t *testing.T) { + suite.Run(t, new(ABCITestSuite)) +} + +func (suite *ABCITestSuite) SetupTest() { + // General config + suite.encodingConfig = testutils.CreateTestEncodingConfig() + suite.random = rand.New(rand.NewSource(time.Now().Unix())) + suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey) + testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) + suite.ctx = testCtx.Ctx + + // Mempool set up + suite.config = mempool.NewDefaultConfig(suite.encodingConfig.TxConfig.TxDecoder()) + suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) + suite.txs = make(map[string]struct{}) + suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(1000000000)) + suite.minBidIncrement = sdk.NewCoin("foo", sdk.NewInt(1000)) + + // Mock keepers set up + ctrl := gomock.NewController(suite.T()) + suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) + suite.accountKeeper.EXPECT().GetModuleAddress(buildertypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes() + suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) + suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) + suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) + suite.authorityAccount = sdk.AccAddress([]byte("authority")) + + // Builder keeper / decorator set up + suite.builderKeeper = keeper.NewKeeper( + suite.encodingConfig.Codec, + suite.key, + suite.accountKeeper, + suite.bankKeeper, + suite.distrKeeper, + suite.stakingKeeper, + suite.authorityAccount.String(), + ) + 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) + + // Accounts set up + suite.accounts = testutils.RandomAccounts(suite.random, 1) + suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000))) + suite.nonces = make(map[string]uint64) + for _, acc := range suite.accounts { + suite.nonces[acc.Address.String()] = 0 + } + + // Proposal handler set up + suite.logger = log.NewNopLogger() + suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) + suite.voteExtensionHandler = abci.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler) +} + +func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + signer := tx.GetMsgs()[0].GetSigners()[0] + suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances) + + next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + return ctx, nil + } + + ctx, err := suite.builderDecorator.AnteHandle(ctx, tx, false, next) + if err != nil { + return ctx, err + } + + bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) + if err != nil { + return ctx, err + } + + if !simulate { + hash := sha256.Sum256(bz) + txHash := hex.EncodeToString(hash[:]) + if _, ok := suite.txs[txHash]; ok { + return ctx, fmt.Errorf("tx already in mempool") + } + suite.txs[txHash] = struct{}{} + } + + return ctx, nil +} + +func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int { + suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) + + // Insert a bunch of normal transactions into the global mempool + for i := 0; i < numNormalTxs; i++ { + // randomly select an account to create the tx + randomIndex := suite.random.Intn(len(suite.accounts)) + acc := suite.accounts[randomIndex] + + // create a few random msgs + randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) + + nonce := suite.nonces[acc.Address.String()] + randomTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) + suite.Require().NoError(err) + + suite.nonces[acc.Address.String()]++ + priority := suite.random.Int63n(100) + 1 + suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), randomTx)) + } + + suite.Require().Equal(numNormalTxs, suite.mempool.CountTx()) + suite.Require().Equal(0, suite.mempool.CountAuctionTx()) + + // Insert a bunch of auction transactions into the global mempool and auction mempool + for i := 0; i < numAuctionTxs; i++ { + // randomly select a bidder to create the tx + randomIndex := suite.random.Intn(len(suite.accounts)) + acc := suite.accounts[randomIndex] + + // create a new auction bid msg with numBundledTxs bundled transactions + nonce := suite.nonces[acc.Address.String()] + bidMsg, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, acc, suite.auctionBidAmount, nonce, numBundledTxs) + suite.nonces[acc.Address.String()] += uint64(numBundledTxs) + suite.Require().NoError(err) + + // create the auction tx + nonce = suite.nonces[acc.Address.String()] + auctionTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg}) + suite.Require().NoError(err) + + // insert the auction tx into the global mempool + priority := suite.random.Int63n(100) + 1 + suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx)) + suite.nonces[acc.Address.String()]++ + + if insertRefTxs { + for _, refRawTx := range bidMsg.GetTransactions() { + refTx, err := suite.encodingConfig.TxConfig.TxDecoder()(refRawTx) + suite.Require().NoError(err) + priority := suite.random.Int63n(100) + 1 + suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx)) + } + } + + // decrement the bid amount for the next auction tx + suite.auctionBidAmount = suite.auctionBidAmount.Sub(suite.minBidIncrement) + } + + numSeenGlobalTxs := 0 + for iterator := suite.mempool.Select(suite.ctx, nil); iterator != nil; iterator = iterator.Next() { + numSeenGlobalTxs++ + } + + numSeenAuctionTxs := 0 + for iterator := suite.mempool.AuctionBidSelect(suite.ctx); iterator != nil; iterator = iterator.Next() { + numSeenAuctionTxs++ + } + + var totalNumTxs int + suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx()) + if insertRefTxs { + totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs) + suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) + suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) + } else { + totalNumTxs = numNormalTxs + suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) + suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) + } + + suite.Require().Equal(numAuctionTxs, numSeenAuctionTxs) + + return totalNumTxs +} + +func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte { + txs := make([][]byte, 0) + seenTxs := make(map[string]bool) + + auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) + for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { + auctionTx := auctionIterator.Tx() + txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx) + suite.Require().NoError(err) + + txs = append(txs, txBz) + + if exportRefTxs { + for _, refRawTx := range auctionTx.GetMsgs()[0].(*buildertypes.MsgAuctionBid).GetTransactions() { + txs = append(txs, refRawTx) + seenTxs[string(refRawTx)] = true + } + } + + seenTxs[string(txBz)] = true + } + + iterator := suite.mempool.Select(suite.ctx, nil) + for ; iterator != nil; iterator = iterator.Next() { + txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx()) + suite.Require().NoError(err) + + if !seenTxs[string(txBz)] { + txs = append(txs, txBz) + } + } + + return txs +} diff --git a/abci/proposals_test.go b/abci/proposals_test.go index 0a7cca1..6875f59 100644 --- a/abci/proposals_test.go +++ b/abci/proposals_test.go @@ -2,264 +2,15 @@ package abci_test import ( "bytes" - "crypto/sha256" - "encoding/hex" - "fmt" - "math/rand" - "testing" - "time" abcitypes "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/log" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/golang/mock/gomock" "github.com/skip-mev/pob/abci" - "github.com/skip-mev/pob/mempool" testutils "github.com/skip-mev/pob/testutils" "github.com/skip-mev/pob/x/builder/ante" - "github.com/skip-mev/pob/x/builder/keeper" buildertypes "github.com/skip-mev/pob/x/builder/types" - "github.com/stretchr/testify/suite" ) -type ABCITestSuite struct { - suite.Suite - ctx sdk.Context - - // mempool setup - mempool *mempool.AuctionMempool - logger log.Logger - encodingConfig testutils.EncodingConfig - proposalHandler *abci.ProposalHandler - txs map[string]struct{} - - // auction bid setup - auctionBidAmount sdk.Coin - minBidIncrement sdk.Coin - - // builder setup - builderKeeper keeper.Keeper - bankKeeper *testutils.MockBankKeeper - accountKeeper *testutils.MockAccountKeeper - distrKeeper *testutils.MockDistributionKeeper - stakingKeeper *testutils.MockStakingKeeper - builderDecorator ante.BuilderDecorator - key *storetypes.KVStoreKey - authorityAccount sdk.AccAddress - - // account set up - accounts []testutils.Account - balances sdk.Coins - random *rand.Rand - nonces map[string]uint64 -} - -func TestABCISuite(t *testing.T) { - suite.Run(t, new(ABCITestSuite)) -} - -func (suite *ABCITestSuite) SetupTest() { - // General config - suite.encodingConfig = testutils.CreateTestEncodingConfig() - suite.random = rand.New(rand.NewSource(time.Now().Unix())) - suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test")) - suite.ctx = testCtx.Ctx - - // Mempool set up - config := mempool.NewDefaultConfig(suite.encodingConfig.TxConfig.TxDecoder()) - suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, config) - suite.txs = make(map[string]struct{}) - suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(1000000000)) - suite.minBidIncrement = sdk.NewCoin("foo", sdk.NewInt(1000)) - - // Mock keepers set up - ctrl := gomock.NewController(suite.T()) - suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl) - suite.accountKeeper.EXPECT().GetModuleAddress(buildertypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes() - suite.bankKeeper = testutils.NewMockBankKeeper(ctrl) - suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl) - suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl) - suite.authorityAccount = sdk.AccAddress([]byte("authority")) - - // Builder keeper / decorator set up - suite.builderKeeper = keeper.NewKeeper( - suite.encodingConfig.Codec, - suite.key, - suite.accountKeeper, - suite.bankKeeper, - suite.distrKeeper, - suite.stakingKeeper, - suite.authorityAccount.String(), - ) - 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) - - // Accounts set up - suite.accounts = testutils.RandomAccounts(suite.random, 1) - suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000))) - suite.nonces = make(map[string]uint64) - for _, acc := range suite.accounts { - suite.nonces[acc.Address.String()] = 0 - } - - // Proposal handler set up - suite.logger = log.NewNopLogger() - suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) -} - -func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { - signer := tx.GetMsgs()[0].GetSigners()[0] - suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances) - - next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { - return ctx, nil - } - - ctx, err := suite.builderDecorator.AnteHandle(ctx, tx, false, next) - if err != nil { - return ctx, err - } - - bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx) - if err != nil { - return ctx, err - } - - if !simulate { - hash := sha256.Sum256(bz) - txHash := hex.EncodeToString(hash[:]) - if _, ok := suite.txs[txHash]; ok { - return ctx, fmt.Errorf("tx already in mempool") - } - suite.txs[txHash] = struct{}{} - } - - return ctx, nil -} - -func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int { - // Insert a bunch of normal transactions into the global mempool - for i := 0; i < numNormalTxs; i++ { - // randomly select an account to create the tx - randomIndex := suite.random.Intn(len(suite.accounts)) - acc := suite.accounts[randomIndex] - - // create a few random msgs - randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3) - - nonce := suite.nonces[acc.Address.String()] - randomTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs) - suite.Require().NoError(err) - - suite.nonces[acc.Address.String()]++ - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), randomTx)) - } - - suite.Require().Equal(numNormalTxs, suite.mempool.CountTx()) - suite.Require().Equal(0, suite.mempool.CountAuctionTx()) - - // Insert a bunch of auction transactions into the global mempool and auction mempool - for i := 0; i < numAuctionTxs; i++ { - // randomly select a bidder to create the tx - randomIndex := suite.random.Intn(len(suite.accounts)) - acc := suite.accounts[randomIndex] - - // create a new auction bid msg with numBundledTxs bundled transactions - nonce := suite.nonces[acc.Address.String()] - bidMsg, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, acc, suite.auctionBidAmount, nonce, numBundledTxs) - suite.nonces[acc.Address.String()] += uint64(numBundledTxs) - suite.Require().NoError(err) - - // create the auction tx - nonce = suite.nonces[acc.Address.String()] - auctionTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg}) - suite.Require().NoError(err) - - // insert the auction tx into the global mempool - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx)) - suite.nonces[acc.Address.String()]++ - - if insertRefTxs { - for _, refRawTx := range bidMsg.GetTransactions() { - refTx, err := suite.encodingConfig.TxConfig.TxDecoder()(refRawTx) - suite.Require().NoError(err) - priority := suite.random.Int63n(100) + 1 - suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx)) - } - } - - // decrement the bid amount for the next auction tx - suite.auctionBidAmount = suite.auctionBidAmount.Sub(suite.minBidIncrement) - } - - numSeenGlobalTxs := 0 - for iterator := suite.mempool.Select(suite.ctx, nil); iterator != nil; iterator = iterator.Next() { - numSeenGlobalTxs++ - } - - numSeenAuctionTxs := 0 - for iterator := suite.mempool.AuctionBidSelect(suite.ctx); iterator != nil; iterator = iterator.Next() { - numSeenAuctionTxs++ - } - - var totalNumTxs int - suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx()) - if insertRefTxs { - totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs) - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } else { - totalNumTxs = numNormalTxs - suite.Require().Equal(totalNumTxs, suite.mempool.CountTx()) - suite.Require().Equal(totalNumTxs, numSeenGlobalTxs) - } - - suite.Require().Equal(numAuctionTxs, numSeenAuctionTxs) - - return totalNumTxs -} - -func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte { - txs := make([][]byte, 0) - seenTxs := make(map[string]bool) - - auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx) - for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { - auctionTx := auctionIterator.Tx() - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx) - suite.Require().NoError(err) - - txs = append(txs, txBz) - - if exportRefTxs { - for _, refRawTx := range auctionTx.GetMsgs()[0].(*buildertypes.MsgAuctionBid).GetTransactions() { - txs = append(txs, refRawTx) - seenTxs[string(refRawTx)] = true - } - } - - seenTxs[string(txBz)] = true - } - - iterator := suite.mempool.Select(suite.ctx, nil) - for ; iterator != nil; iterator = iterator.Next() { - txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx()) - suite.Require().NoError(err) - - if !seenTxs[string(txBz)] { - txs = append(txs, txBz) - } - } - - return txs -} - func (suite *ABCITestSuite) TestPrepareProposal() { var ( // the modified transactions cannot exceed this size @@ -490,6 +241,9 @@ func (suite *ABCITestSuite) TestPrepareProposal() { suite.builderKeeper.SetParams(suite.ctx, params) suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool) + // reset the proposal handler with the new mempool + suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) + handler := suite.proposalHandler.PrepareProposalHandler() res := handler(suite.ctx, abcitypes.RequestPrepareProposal{ MaxTxBytes: maxTxBytes, @@ -723,6 +477,9 @@ func (suite *ABCITestSuite) TestProcessProposal() { suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs) + // reset the proposal handler with the new mempool + suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder()) + if frontRunningTx != nil { suite.Require().NoError(suite.mempool.Insert(suite.ctx, frontRunningTx)) } diff --git a/abci/vote_extensions.go b/abci/vote_extensions.go index 9bcfda5..733ce8f 100644 --- a/abci/vote_extensions.go +++ b/abci/vote_extensions.go @@ -2,6 +2,9 @@ package abci import ( "context" + "crypto/sha256" + "encoding/hex" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" @@ -14,15 +17,30 @@ type ( Remove(tx sdk.Tx) error AuctionBidSelect(ctx context.Context) sdkmempool.Iterator IsAuctionTx(tx sdk.Tx) (bool, error) + GetBundledTransactions(tx sdk.Tx) ([][]byte, error) + WrapBundleTransaction(tx []byte) (sdk.Tx, error) } // VoteExtensionHandler contains the functionality and handlers required to // process, validate and build vote extensions. VoteExtensionHandler struct { - mempool VoteExtensionMempool - txDecoder sdk.TxDecoder - txEncoder sdk.TxEncoder + mempool VoteExtensionMempool + + // txDecoder is used to decode the top bidding auction transaction + txDecoder sdk.TxDecoder + + // txEncoder is used to encode the top bidding auction transaction + txEncoder sdk.TxEncoder + + // anteHandler is used to validate the vote extension anteHandler sdk.AnteHandler + + // cache is used to store the results of the vote extension verification + // for a given block height. + cache map[string]error + + // currentHeight is the block height the cache is valid for. + currentHeight int64 } ) @@ -32,10 +50,12 @@ func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, ah sdk.AnteHandler, ) *VoteExtensionHandler { return &VoteExtensionHandler{ - mempool: mp, - txDecoder: txDecoder, - txEncoder: txEncoder, - anteHandler: ah, + mempool: mp, + txDecoder: txDecoder, + txEncoder: txEncoder, + anteHandler: ah, + cache: make(map[string]error), + currentHeight: 0, } } @@ -44,7 +64,22 @@ func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder, // returns it in its vote extension. func (h *VoteExtensionHandler) ExtendVoteHandler() ExtendVoteHandler { return func(ctx sdk.Context, req *RequestExtendVote) (*ResponseExtendVote, error) { - panic("implement me") + // Iterate through auction bids until we find a valid one + auctionIterator := h.mempool.AuctionBidSelect(ctx) + + for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() { + bidTx := auctionIterator.Tx() + + // Verify the bid tx can be encoded and included in vote extension + if bidBz, err := h.txEncoder(bidTx); err == nil { + // Validate the auction transaction + if err := h.verifyAuctionTx(ctx, bidTx); err == nil { + return &ResponseExtendVote{VoteExtension: bidBz}, nil + } + } + } + + return &ResponseExtendVote{VoteExtension: nil}, nil } } @@ -53,6 +88,93 @@ func (h *VoteExtensionHandler) ExtendVoteHandler() ExtendVoteHandler { // In particular, it verifies that the vote extension is a valid auction transaction. func (h *VoteExtensionHandler) VerifyVoteExtensionHandler() VerifyVoteExtensionHandler { return func(ctx sdk.Context, req *RequestVerifyVoteExtension) (*ResponseVerifyVoteExtension, error) { - panic("implement me") + txBz := req.VoteExtension + if len(txBz) == 0 { + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_ACCEPT}, nil + } + + // Reset the cache if necessary + h.resetCache(ctx.BlockHeight()) + + hashBz := sha256.Sum256(txBz) + hash := hex.EncodeToString(hashBz[:]) + + // Short circuit if we have already verified this vote extension + if err, ok := h.cache[hash]; ok { + if err != nil { + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_REJECT}, err + } + + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_ACCEPT}, nil + } + + // Decode the vote extension which should be a valid auction transaction + bidTx, err := h.txDecoder(txBz) + if err != nil { + h.cache[hash] = err + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_REJECT}, err + } + + // Verify the auction transaction and cache the result + if err = h.verifyAuctionTx(ctx, bidTx); err != nil { + h.cache[hash] = err + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_REJECT}, err + } + + h.cache[hash] = nil + + return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_ACCEPT}, nil } } + +// checkStaleCache checks if the current height differs than the previous height at which +// the vote extensions were verified in. If so, it resets the cache to allow transactions to be +// reverified. +func (h *VoteExtensionHandler) resetCache(blockHeight int64) { + if h.currentHeight != blockHeight { + h.cache = make(map[string]error) + h.currentHeight = blockHeight + } +} + +// verifyAuctionTx verifies a transaction against the application's state. +func (h *VoteExtensionHandler) verifyAuctionTx(ctx sdk.Context, bidTx sdk.Tx) error { + // Verify the vote extension is a auction transaction + isAuctionTx, err := h.mempool.IsAuctionTx(bidTx) + if err != nil { + return err + } + + if !isAuctionTx { + return fmt.Errorf("vote extension is not a valid auction transaction") + } + + if h.anteHandler == nil { + return nil + } + + // Cache context is used to avoid state changes + cache, _ := ctx.CacheContext() + if _, err := h.anteHandler(cache, bidTx, false); err != nil { + return err + } + + bundledTxs, err := h.mempool.GetBundledTransactions(bidTx) + if err != nil { + return err + } + + // Verify all bundled transactions + for _, tx := range bundledTxs { + wrappedTx, err := h.mempool.WrapBundleTransaction(tx) + if err != nil { + return err + } + + if _, err := h.anteHandler(cache, wrappedTx, false); err != nil { + return err + } + } + + return nil +} diff --git a/abci/vote_extensions_test.go b/abci/vote_extensions_test.go index 8478231..dd8aba3 100644 --- a/abci/vote_extensions_test.go +++ b/abci/vote_extensions_test.go @@ -1 +1,371 @@ package abci_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/abci" + "github.com/skip-mev/pob/mempool" + testutils "github.com/skip-mev/pob/testutils" + "github.com/skip-mev/pob/x/builder/types" +) + +func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() { + params := types.Params{ + MaxBundleSize: 5, + ReserveFee: sdk.NewCoin("foo", sdk.NewInt(10)), + MinBuyInFee: sdk.NewCoin("foo", sdk.NewInt(10)), + FrontRunningProtection: true, + MinBidIncrement: suite.minBidIncrement, + } + + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + testCases := []struct { + name string + getExpectedVE func() []byte + }{ + { + "empty mempool", + func() []byte { + suite.createFilledMempool(0, 0, 0, false) + return nil + }, + }, + { + "filled mempool with no auction transactions", + func() []byte { + suite.createFilledMempool(100, 0, 0, false) + return nil + }, + }, + { + "mempool with invalid auction transaction (too many bundled transactions)", + func() []byte { + suite.createFilledMempool(0, 1, int(params.MaxBundleSize)+1, true) + return nil + }, + }, + { + "mempool with invalid auction transaction (invalid bid)", + func() []byte { + bidder := suite.accounts[0] + bid := params.ReserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1))) + signers := []testutils.Account{bidder} + timeout := 1 + + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) + suite.Require().NoError(err) + + suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) + err = suite.mempool.Insert(suite.ctx, bidTx) + suite.Require().NoError(err) + + // this should return nothing since the top bid is not valid + return nil + }, + }, + { + "mempool contains only invalid auction bids (bid is too low)", + func() []byte { + params.ReserveFee = suite.auctionBidAmount + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + // this way all of the bids will be too small + suite.auctionBidAmount = params.ReserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1))) + + suite.createFilledMempool(100, 100, 2, true) + + return nil + }, + }, + { + "mempool contains bid that has bundled txs that are invalid", + func() []byte { + params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10)) + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + bidder := suite.accounts[0] + bid := params.ReserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1))) + + msgAuctionBid, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, bidder, bid, 0, 0) + suite.Require().NoError(err) + msgAuctionBid.Transactions = [][]byte{[]byte("invalid tx")} + + bidTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, bidder, 0, 10, []sdk.Msg{msgAuctionBid}) + suite.Require().NoError(err) + + suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) + err = suite.mempool.Insert(suite.ctx, bidTx) + suite.Require().NoError(err) + + // this should return nothing since the top bid is not valid + return nil + }, + }, + { + "mempool contains bid that has an invalid timeout", + func() []byte { + bidder := suite.accounts[0] + bid := params.ReserveFee + signers := []testutils.Account{bidder} + timeout := 0 + + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) + suite.Require().NoError(err) + + suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config) + err = suite.mempool.Insert(suite.ctx, bidTx) + suite.Require().NoError(err) + + // this should return nothing since the top bid is not valid + return nil + }, + }, + { + "top bid is invalid but next best is valid", + func() []byte { + bidder := suite.accounts[0] + bid := suite.auctionBidAmount.Add(suite.minBidIncrement) + signers := []testutils.Account{bidder} + timeout := 0 + + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) + suite.Require().NoError(err) + + suite.createFilledMempool(100, 100, 2, true) + + topBidTx := suite.mempool.GetTopAuctionTx(suite.ctx) + + err = suite.mempool.Insert(suite.ctx, bidTx) + suite.Require().NoError(err) + + bz, err := suite.encodingConfig.TxConfig.TxEncoder()(topBidTx) + suite.Require().NoError(err) + + return bz + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + expectedVE := tc.getExpectedVE() + + // Reset the handler with the new mempool + suite.voteExtensionHandler = abci.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler) + + handler := suite.voteExtensionHandler.ExtendVoteHandler() + resp, err := handler(suite.ctx, nil) + + suite.Require().NoError(err) + suite.Require().Equal(expectedVE, resp.VoteExtension) + }) + } +} + +func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() { + params := types.Params{ + MaxBundleSize: 5, + ReserveFee: sdk.NewCoin("foo", sdk.NewInt(100)), + MinBuyInFee: sdk.NewCoin("foo", sdk.NewInt(100)), + FrontRunningProtection: true, + MinBidIncrement: sdk.NewCoin("foo", sdk.NewInt(10)), // can't be tested atm + } + + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + testCases := []struct { + name string + req func() *abci.RequestVerifyVoteExtension + expectedErr bool + }{ + { + "invalid vote extension bytes", + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ + VoteExtension: []byte("invalid vote extension"), + } + }, + true, + }, + { + "empty vote extension bytes", + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ + VoteExtension: []byte{}, + } + }, + false, + }, + { + "nil vote extension bytes", + func() *abci.RequestVerifyVoteExtension { + return &abci.RequestVerifyVoteExtension{ + VoteExtension: nil, + } + }, + false, + }, + { + "invalid extension with bid tx with bad timeout", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(10)) + signers := []testutils.Account{bidder} + timeout := 0 + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + true, + }, + { + "invalid vote extension with bid tx with bad bid", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := sdk.NewCoin("foo", sdk.NewInt(0)) + signers := []testutils.Account{bidder} + timeout := 10 + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + true, + }, + { + "valid vote extension", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := params.ReserveFee + signers := []testutils.Account{bidder} + timeout := 10 + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + false, + }, + { + "invalid vote extension with front running bid tx", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := params.ReserveFee + timeout := 10 + + bundlee := testutils.RandomAccounts(suite.random, 1)[0] + signers := []testutils.Account{bidder, bundlee} + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + true, + }, + { + "invalid vote extension with too many bundle txs", + func() *abci.RequestVerifyVoteExtension { + // disable front running protection + params.FrontRunningProtection = false + err := suite.builderKeeper.SetParams(suite.ctx, params) + suite.Require().NoError(err) + + bidder := suite.accounts[0] + bid := params.ReserveFee + signers := testutils.RandomAccounts(suite.random, int(params.MaxBundleSize)+1) + timeout := 10 + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + true, + }, + { + "invalid vote extension with a failing bundle tx", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := params.ReserveFee + + msgAuctionBid, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, bidder, bid, 0, 0) + suite.Require().NoError(err) + + // Create a failing tx + msgAuctionBid.Transactions = [][]byte{{0x01}} + + bidTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, suite.accounts[0], 0, 1, []sdk.Msg{msgAuctionBid}) + suite.Require().NoError(err) + + bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx) + suite.Require().NoError(err) + + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + true, + }, + { + "valid vote extension + no comparison to local mempool", + func() *abci.RequestVerifyVoteExtension { + bidder := suite.accounts[0] + bid := params.ReserveFee + signers := []testutils.Account{bidder} + timeout := 10 + + bz := suite.createAuctionTxBz(bidder, bid, signers, timeout) + + // Add a bid to the mempool that is greater than the one in the vote extension + bid = bid.Add(params.MinBidIncrement) + bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 10, 1, signers) + suite.Require().NoError(err) + + err = suite.mempool.Insert(suite.ctx, bidTx) + suite.Require().NoError(err) + + tx := suite.mempool.GetTopAuctionTx(suite.ctx) + suite.Require().NotNil(tx) + + return &abci.RequestVerifyVoteExtension{ + VoteExtension: bz, + } + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + req := tc.req() + + handler := suite.voteExtensionHandler.VerifyVoteExtensionHandler() + _, err := handler(suite.ctx, req) + + if tc.expectedErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *ABCITestSuite) createAuctionTxBz(bidder testutils.Account, bid sdk.Coin, signers []testutils.Account, timeout int) []byte { + auctionTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers) + suite.Require().NoError(err) + + txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx) + suite.Require().NoError(err) + + return txBz +}