feat(v1): [ENG-790] Implementing vote extension handlers (#103)

Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
This commit is contained in:
David Terpay 2023-05-04 14:31:42 -04:00 committed by GitHub
parent 14827d56f8
commit 07c76b8330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 771 additions and 258 deletions

264
abci/abci_test.go Normal file
View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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
}