feat(bb): Defer in proposal handlers, more interfaces for lanes, clean up (#165)
This commit is contained in:
parent
61b6759e92
commit
b7780a3140
285
abci/abci.go
285
abci/abci.go
@ -1,285 +0,0 @@
|
||||
package abci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
abci "github.com/cometbft/cometbft/abci/types"
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
)
|
||||
|
||||
type (
|
||||
Mempool interface {
|
||||
sdkmempool.Mempool
|
||||
|
||||
// The AuctionFactory interface is utilized to retrieve, validate, and wrap bid
|
||||
// information into the block proposal.
|
||||
mempool.AuctionFactory
|
||||
|
||||
// AuctionBidSelect returns an iterator that iterates over the top bid
|
||||
// transactions in the mempool.
|
||||
AuctionBidSelect(ctx context.Context) sdkmempool.Iterator
|
||||
}
|
||||
|
||||
ProposalHandler struct {
|
||||
mempool Mempool
|
||||
logger log.Logger
|
||||
anteHandler sdk.AnteHandler
|
||||
txEncoder sdk.TxEncoder
|
||||
txDecoder sdk.TxDecoder
|
||||
}
|
||||
)
|
||||
|
||||
func NewProposalHandler(
|
||||
mp Mempool,
|
||||
logger log.Logger,
|
||||
anteHandler sdk.AnteHandler,
|
||||
txEncoder sdk.TxEncoder,
|
||||
txDecoder sdk.TxDecoder,
|
||||
) *ProposalHandler {
|
||||
return &ProposalHandler{
|
||||
mempool: mp,
|
||||
logger: logger,
|
||||
anteHandler: anteHandler,
|
||||
txEncoder: txEncoder,
|
||||
txDecoder: txDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs
|
||||
// top-of-block auctioning and general block proposal construction.
|
||||
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
|
||||
var (
|
||||
selectedTxs [][]byte
|
||||
totalTxBytes int64
|
||||
)
|
||||
|
||||
bidTxIterator := h.mempool.AuctionBidSelect(ctx)
|
||||
txsToRemove := make(map[sdk.Tx]struct{}, 0)
|
||||
seenTxs := make(map[string]struct{}, 0)
|
||||
|
||||
// Attempt to select the highest bid transaction that is valid and whose
|
||||
// bundled transactions are valid.
|
||||
selectBidTxLoop:
|
||||
for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() {
|
||||
cacheCtx, write := ctx.CacheContext()
|
||||
tmpBidTx := bidTxIterator.Tx()
|
||||
|
||||
bidTxBz, err := h.PrepareProposalVerifyTx(cacheCtx, tmpBidTx)
|
||||
if err != nil {
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
bidTxSize := int64(len(bidTxBz))
|
||||
if bidTxSize <= req.MaxTxBytes {
|
||||
bidInfo, err := h.mempool.GetAuctionBidInfo(tmpBidTx)
|
||||
if err != nil {
|
||||
// Some transactions in the bundle may be malformatted or invalid, so
|
||||
// we remove the bid transaction and try the next top bid.
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
// store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal
|
||||
bundledTransactions := bidInfo.Transactions
|
||||
sdkTxBytes := make([][]byte, len(bundledTransactions))
|
||||
|
||||
// Ensure that the bundled transactions are valid
|
||||
for index, rawRefTx := range bundledTransactions {
|
||||
refTx, err := h.mempool.WrapBundleTransaction(rawRefTx)
|
||||
if err != nil {
|
||||
// Malformed bundled transaction, so we remove the bid transaction
|
||||
// and try the next top bid.
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
txBz, err := h.PrepareProposalVerifyTx(cacheCtx, refTx)
|
||||
if err != nil {
|
||||
// Invalid bundled transaction, so we remove the bid transaction
|
||||
// and try the next top bid.
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
sdkTxBytes[index] = txBz
|
||||
}
|
||||
|
||||
// At this point, both the bid transaction itself and all the bundled
|
||||
// transactions are valid. So we select the bid transaction along with
|
||||
// all the bundled transactions. We also mark these transactions as seen and
|
||||
// update the total size selected thus far.
|
||||
totalTxBytes += bidTxSize
|
||||
selectedTxs = append(selectedTxs, bidTxBz)
|
||||
selectedTxs = append(selectedTxs, sdkTxBytes...)
|
||||
|
||||
for _, refTxRaw := range sdkTxBytes {
|
||||
hash := sha256.Sum256(refTxRaw)
|
||||
txHash := hex.EncodeToString(hash[:])
|
||||
seenTxs[txHash] = struct{}{}
|
||||
}
|
||||
|
||||
// Write the cache context to the original context when we know we have a
|
||||
// valid top of block bundle.
|
||||
write()
|
||||
|
||||
break selectBidTxLoop
|
||||
}
|
||||
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
h.logger.Info(
|
||||
"failed to select auction bid tx; tx size is too large",
|
||||
"tx_size", bidTxSize,
|
||||
"max_size", req.MaxTxBytes,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove all invalid transactions from the mempool.
|
||||
for tx := range txsToRemove {
|
||||
h.RemoveTx(tx)
|
||||
}
|
||||
|
||||
iterator := h.mempool.Select(ctx, nil)
|
||||
txsToRemove = map[sdk.Tx]struct{}{}
|
||||
|
||||
// Select remaining transactions for the block proposal until we've reached
|
||||
// size capacity.
|
||||
selectTxLoop:
|
||||
for ; iterator != nil; iterator = iterator.Next() {
|
||||
memTx := iterator.Tx()
|
||||
|
||||
// If the transaction is already included in the proposal, then we skip it.
|
||||
txBz, err := h.txEncoder(memTx)
|
||||
if err != nil {
|
||||
txsToRemove[memTx] = struct{}{}
|
||||
continue selectTxLoop
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(txBz)
|
||||
txHash := hex.EncodeToString(hash[:])
|
||||
if _, ok := seenTxs[txHash]; ok {
|
||||
continue selectTxLoop
|
||||
}
|
||||
|
||||
txBz, err = h.PrepareProposalVerifyTx(ctx, memTx)
|
||||
if err != nil {
|
||||
txsToRemove[memTx] = struct{}{}
|
||||
continue selectTxLoop
|
||||
}
|
||||
|
||||
txSize := int64(len(txBz))
|
||||
if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes {
|
||||
selectedTxs = append(selectedTxs, txBz)
|
||||
} else {
|
||||
// We've reached capacity per req.MaxTxBytes so we cannot select any
|
||||
// more transactions.
|
||||
break selectTxLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all invalid transactions from the mempool.
|
||||
for tx := range txsToRemove {
|
||||
h.RemoveTx(tx)
|
||||
}
|
||||
|
||||
return abci.ResponsePrepareProposal{Txs: selectedTxs}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs
|
||||
// block proposal verification.
|
||||
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal {
|
||||
for index, txBz := range req.Txs {
|
||||
tx, err := h.ProcessProposalVerifyTx(ctx, txBz)
|
||||
if err != nil {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
bidInfo, err := h.mempool.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
// If the transaction is an auction bid, then we need to ensure that it is
|
||||
// the first transaction in the block proposal and that the order of
|
||||
// transactions in the block proposal follows the order of transactions in
|
||||
// the bid.
|
||||
if bidInfo != nil {
|
||||
if index != 0 {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
bundledTransactions := bidInfo.Transactions
|
||||
if len(req.Txs) < len(bundledTransactions)+1 {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
for i, refTxRaw := range bundledTransactions {
|
||||
// Wrap and then encode the bundled transaction to ensure that the underlying
|
||||
// reference transaction can be processed as an sdk.Tx.
|
||||
wrappedTx, err := h.mempool.WrapBundleTransaction(refTxRaw)
|
||||
if err != nil {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
refTxBz, err := h.txEncoder(wrappedTx)
|
||||
if err != nil {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
if !bytes.Equal(refTxBz, req.Txs[i+1]) {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareProposalVerifyTx encodes a transaction and verifies it.
|
||||
func (h *ProposalHandler) PrepareProposalVerifyTx(ctx sdk.Context, tx sdk.Tx) ([]byte, error) {
|
||||
txBz, err := h.txEncoder(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBz, h.verifyTx(ctx, tx)
|
||||
}
|
||||
|
||||
// ProcessProposalVerifyTx decodes a transaction and verifies it.
|
||||
func (h *ProposalHandler) ProcessProposalVerifyTx(ctx sdk.Context, txBz []byte) (sdk.Tx, error) {
|
||||
tx, err := h.txDecoder(txBz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, h.verifyTx(ctx, tx)
|
||||
}
|
||||
|
||||
// VerifyTx verifies a transaction against the application's state.
|
||||
func (h *ProposalHandler) verifyTx(ctx sdk.Context, tx sdk.Tx) error {
|
||||
if h.anteHandler != nil {
|
||||
_, err := h.anteHandler(ctx, tx, false)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ProposalHandler) RemoveTx(tx sdk.Tx) {
|
||||
if err := h.mempool.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,20 @@
|
||||
package abci_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
abcitypes "github.com/cometbft/cometbft/abci/types"
|
||||
comettypes "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"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/base"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/ante"
|
||||
"github.com/skip-mev/pob/x/builder/keeper"
|
||||
@ -28,17 +26,15 @@ type ABCITestSuite struct {
|
||||
suite.Suite
|
||||
ctx sdk.Context
|
||||
|
||||
// mempool setup
|
||||
mempool *mempool.AuctionMempool
|
||||
logger log.Logger
|
||||
encodingConfig testutils.EncodingConfig
|
||||
proposalHandler *abci.ProposalHandler
|
||||
config mempool.AuctionFactory
|
||||
txs map[string]struct{}
|
||||
// mempool and lane set up
|
||||
mempool blockbuster.Mempool
|
||||
tobLane *auction.TOBLane
|
||||
baseLane *base.DefaultLane
|
||||
|
||||
// auction bid setup
|
||||
auctionBidAmount sdk.Coin
|
||||
minBidIncrement sdk.Coin
|
||||
logger log.Logger
|
||||
encodingConfig testutils.EncodingConfig
|
||||
proposalHandler *abci.ProposalHandler
|
||||
voteExtensionHandler *abci.VoteExtensionHandler
|
||||
|
||||
// builder setup
|
||||
builderKeeper keeper.Keeper
|
||||
@ -68,13 +64,34 @@ func (suite *ABCITestSuite) SetupTest() {
|
||||
suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey)
|
||||
testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test"))
|
||||
suite.ctx = testCtx.Ctx.WithBlockHeight(1)
|
||||
suite.logger = log.NewNopLogger()
|
||||
|
||||
// Lanes configuration
|
||||
//
|
||||
// TOB lane set up
|
||||
config := blockbuster.BaseLaneConfig{
|
||||
Logger: suite.logger,
|
||||
TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
AnteHandler: suite.anteHandler,
|
||||
MaxBlockSpace: sdk.ZeroDec(),
|
||||
}
|
||||
suite.tobLane = auction.NewTOBLane(
|
||||
config,
|
||||
0, // No bound on the number of transactions in the lane
|
||||
auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()),
|
||||
)
|
||||
|
||||
// Base lane set up
|
||||
suite.baseLane = base.NewDefaultLane(
|
||||
config,
|
||||
)
|
||||
|
||||
// Mempool set up
|
||||
suite.config = mempool.NewDefaultAuctionFactory(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))
|
||||
suite.mempool = blockbuster.NewMempool(
|
||||
suite.tobLane,
|
||||
suite.baseLane,
|
||||
)
|
||||
|
||||
// Mock keepers set up
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
@ -97,10 +114,10 @@ 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.TxEncoder(), suite.mempool)
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
|
||||
|
||||
// Accounts set up
|
||||
suite.accounts = testutils.RandomAccounts(suite.random, 1)
|
||||
suite.accounts = testutils.RandomAccounts(suite.random, 10)
|
||||
suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000)))
|
||||
suite.nonces = make(map[string]uint64)
|
||||
for _, acc := range suite.accounts {
|
||||
@ -108,663 +125,172 @@ func (suite *ABCITestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
// 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.proposalHandler = abci.NewProposalHandler(
|
||||
[]blockbuster.Lane{suite.baseLane}, // only the base lane is used for proposal handling
|
||||
suite.tobLane,
|
||||
suite.logger,
|
||||
suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
)
|
||||
suite.voteExtensionHandler = abci.NewVoteExtensionHandler(
|
||||
suite.tobLane,
|
||||
suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
signer := tx.GetMsgs()[0].GetSigners()[0]
|
||||
suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances)
|
||||
|
||||
next := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
|
||||
next := func(ctx sdk.Context, _ sdk.Tx, _ 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
|
||||
return suite.builderDecorator.AnteHandle(ctx, tx, false, next)
|
||||
}
|
||||
|
||||
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++ {
|
||||
// fillBaseLane fills the base lane with numTxs transactions that are randomly created.
|
||||
func (suite *ABCITestSuite) fillBaseLane(numTxs int) {
|
||||
for i := 0; i < numTxs; i++ {
|
||||
// randomly select an account to create the tx
|
||||
randomIndex := suite.random.Intn(len(suite.accounts))
|
||||
acc := suite.accounts[randomIndex]
|
||||
|
||||
// create a few random msgs
|
||||
randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3)
|
||||
|
||||
// create a few random msgs and construct the tx
|
||||
nonce := suite.nonces[acc.Address.String()]
|
||||
randomTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs)
|
||||
randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3)
|
||||
tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// insert the tx into the lane and update the account
|
||||
suite.nonces[acc.Address.String()]++
|
||||
priority := suite.random.Int63n(100) + 1
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), randomTx))
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx))
|
||||
}
|
||||
}
|
||||
|
||||
suite.Require().Equal(numNormalTxs, suite.mempool.CountTx())
|
||||
suite.Require().Equal(0, suite.mempool.CountAuctionTx())
|
||||
|
||||
// fillTOBLane fills the TOB lane with numTxs transactions that are randomly created.
|
||||
func (suite *ABCITestSuite) fillTOBLane(numTxs int, numBundledTxs int) {
|
||||
// Insert a bunch of auction transactions into the global mempool and auction mempool
|
||||
for i := 0; i < numAuctionTxs; i++ {
|
||||
for i := 0; i < numTxs; i++ {
|
||||
// randomly select a bidder to create the tx
|
||||
randomIndex := suite.random.Intn(len(suite.accounts))
|
||||
acc := suite.accounts[randomIndex]
|
||||
|
||||
// create a new auction bid msg with numBundledTxs bundled transactions
|
||||
// create a randomized auction transaction
|
||||
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)
|
||||
bidAmount := sdk.NewInt(int64(suite.random.Intn(1000) + 1))
|
||||
bid := sdk.NewCoin("foo", bidAmount)
|
||||
|
||||
// create the auction tx
|
||||
nonce = suite.nonces[acc.Address.String()]
|
||||
auctionTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg})
|
||||
signers := []testutils.Account{}
|
||||
for j := 0; j < numBundledTxs; j++ {
|
||||
signers = append(signers, suite.accounts[0])
|
||||
}
|
||||
|
||||
tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, signers)
|
||||
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.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
|
||||
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)
|
||||
func (suite *ABCITestSuite) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal {
|
||||
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
|
||||
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
auctionIterator := suite.tobLane.Select(suite.ctx, nil)
|
||||
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
|
||||
auctionTx := auctionIterator.Tx()
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx)
|
||||
tx := auctionIterator.Tx()
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
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
|
||||
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: txBz,
|
||||
})
|
||||
}
|
||||
|
||||
iterator := suite.mempool.Select(suite.ctx, nil)
|
||||
for ; iterator != nil; iterator = iterator.Next() {
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx())
|
||||
return comettypes.RequestPrepareProposal{
|
||||
MaxTxBytes: maxBytes,
|
||||
LocalLastCommit: comettypes.ExtendedCommitInfo{
|
||||
Votes: voteExtensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxs(txs []sdk.Tx) comettypes.ExtendedCommitInfo {
|
||||
voteExtensions := make([][]byte, 0)
|
||||
for _, tx := range txs {
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
if !seenTxs[string(txBz)] {
|
||||
txs = append(txs, txBz)
|
||||
voteExtensions = append(voteExtensions, bz)
|
||||
}
|
||||
|
||||
return suite.createExtendedCommitInfo(voteExtensions)
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedVoteInfo(voteExtensions [][]byte) []comettypes.ExtendedVoteInfo {
|
||||
commitInfo := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
for _, voteExtension := range voteExtensions {
|
||||
info := comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: voteExtension,
|
||||
}
|
||||
|
||||
commitInfo = append(commitInfo, info)
|
||||
}
|
||||
|
||||
return txs
|
||||
return commitInfo
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) TestPrepareProposal() {
|
||||
var (
|
||||
// the modified transactions cannot exceed this size
|
||||
maxTxBytes int64 = 1000000000000000000
|
||||
|
||||
// mempool configuration
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 100
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
|
||||
// auction configuration
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
frontRunningProtection = true
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
expectedNumberProposalTxs int
|
||||
expectedNumberTxsInMempool int
|
||||
isTopBidValid bool
|
||||
}{
|
||||
{
|
||||
"single bundle in the mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
},
|
||||
4,
|
||||
3,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, no ref txs in mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
},
|
||||
4,
|
||||
0,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, not valid",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
},
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, not valid with ref txs in mempool",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
},
|
||||
3,
|
||||
3,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"multiple bundles in the mempool, no normal txs + no ref txs in mempool",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
},
|
||||
4,
|
||||
0,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"multiple bundles in the mempool, no normal txs + ref txs in mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
},
|
||||
31,
|
||||
30,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"normal txs only",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
},
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"many normal txs only",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
},
|
||||
100,
|
||||
100,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"single normal tx, single auction tx",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
},
|
||||
2,
|
||||
1,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"single normal tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
},
|
||||
5,
|
||||
1,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"single normal tx, single failing auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000)) // this will fail the ante handler
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000000000))
|
||||
},
|
||||
4,
|
||||
4,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"many normal tx, single auction tx with no ref txs",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000000))
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
},
|
||||
101,
|
||||
100,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"many normal tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
},
|
||||
104,
|
||||
103,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"many normal tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
},
|
||||
104,
|
||||
100,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"many normal tx, many auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 100
|
||||
numBundledTxs = 1
|
||||
insertRefTxs = true
|
||||
},
|
||||
201,
|
||||
200,
|
||||
true,
|
||||
},
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfo(voteExtensions [][]byte) comettypes.ExtendedCommitInfo {
|
||||
commitInfo := comettypes.ExtendedCommitInfo{
|
||||
Votes: suite.createExtendedVoteInfo(voteExtensions),
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
tc.malleate()
|
||||
return commitInfo
|
||||
}
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxBzs(txs [][]byte) []byte {
|
||||
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
|
||||
// create a new auction
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: frontRunningProtection,
|
||||
MinBidIncrement: suite.minBidIncrement,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
|
||||
|
||||
handler := suite.proposalHandler.PrepareProposalHandler()
|
||||
res := handler(suite.ctx, abcitypes.RequestPrepareProposal{
|
||||
MaxTxBytes: maxTxBytes,
|
||||
})
|
||||
|
||||
// -------------------- Check Invariants -------------------- //
|
||||
// 1. The auction tx must fail if we know it is invalid
|
||||
suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid())
|
||||
|
||||
// 2. total bytes must be less than or equal to maxTxBytes
|
||||
totalBytes := int64(0)
|
||||
if suite.isTopBidValid() {
|
||||
totalBytes += int64(len(res.Txs[0]))
|
||||
|
||||
for _, tx := range res.Txs[1+numBundledTxs:] {
|
||||
totalBytes += int64(len(tx))
|
||||
}
|
||||
} else {
|
||||
for _, tx := range res.Txs {
|
||||
totalBytes += int64(len(tx))
|
||||
}
|
||||
}
|
||||
suite.Require().LessOrEqual(totalBytes, maxTxBytes)
|
||||
|
||||
// 3. the number of transactions in the response must be equal to the number of expected transactions
|
||||
suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs))
|
||||
|
||||
// 4. if there are auction transactions, the first transaction must be the top bid
|
||||
// and the rest of the bundle must be in the response
|
||||
if suite.isTopBidValid() {
|
||||
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
for index, tx := range bidInfo.Transactions {
|
||||
suite.Require().Equal(tx, res.Txs[index+1])
|
||||
}
|
||||
}
|
||||
|
||||
// 5. All of the transactions must be unique
|
||||
uniqueTxs := make(map[string]bool)
|
||||
for _, tx := range res.Txs {
|
||||
suite.Require().False(uniqueTxs[string(tx)])
|
||||
uniqueTxs[string(tx)] = true
|
||||
}
|
||||
|
||||
// 6. The number of transactions in the mempool must be correct
|
||||
suite.Require().Equal(tc.expectedNumberTxsInMempool, suite.mempool.CountTx())
|
||||
for _, txBz := range txs {
|
||||
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: txBz,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) TestProcessProposal() {
|
||||
var (
|
||||
// mempool set up
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
exportRefTxs = true
|
||||
frontRunningTx sdk.Tx
|
||||
|
||||
// auction set up
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
frontRunningProtection = true
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
isTopBidValid bool
|
||||
response abcitypes.ResponseProcessProposal_ProposalStatus
|
||||
}{
|
||||
{
|
||||
"single normal tx, no auction tx",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
},
|
||||
false,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx, no normal txs",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx, single auction tx",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx, single auction tx with no ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
insertRefTxs = false
|
||||
exportRefTxs = false
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs, single normal tx",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 2
|
||||
numBundledTxs = 4
|
||||
insertRefTxs = true
|
||||
exportRefTxs = true
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction txs, multiple normal tx",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single invalid auction tx, multiple normal tx",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000000000000000))
|
||||
insertRefTxs = true
|
||||
},
|
||||
false,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single valid auction txs but missing ref txs",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
insertRefTxs = false
|
||||
exportRefTxs = false
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single valid auction txs but missing ref txs, with many normal txs",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
insertRefTxs = false
|
||||
exportRefTxs = false
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"auction tx with frontrunning",
|
||||
func() {
|
||||
randomAccount := testutils.RandomAccounts(suite.random, 1)[0]
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(696969696969))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount})
|
||||
suite.Require().NotNil(frontRunningTx)
|
||||
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
insertRefTxs = true
|
||||
exportRefTxs = true
|
||||
},
|
||||
false,
|
||||
abcitypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"auction tx with frontrunning, but frontrunning protection disabled",
|
||||
func() {
|
||||
randomAccount := testutils.RandomAccounts(suite.random, 1)[0]
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(696969696969))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount})
|
||||
suite.Require().NotNil(frontRunningTx)
|
||||
|
||||
numAuctionTxs = 0
|
||||
frontRunningProtection = false
|
||||
},
|
||||
true,
|
||||
abcitypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
commitInfo := comettypes.ExtendedCommitInfo{
|
||||
Votes: voteExtensions,
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
tc.malleate()
|
||||
commitInfoBz, err := commitInfo.Marshal()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
if frontRunningTx != nil {
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, frontRunningTx))
|
||||
}
|
||||
|
||||
// create a new auction
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: frontRunningProtection,
|
||||
MinBidIncrement: suite.minBidIncrement,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
|
||||
suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid())
|
||||
|
||||
txs := suite.exportMempool(exportRefTxs)
|
||||
|
||||
if frontRunningTx != nil {
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(frontRunningTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Require().True(bytes.Equal(txs[0], txBz))
|
||||
}
|
||||
|
||||
handler := suite.proposalHandler.ProcessProposalHandler()
|
||||
res := handler(suite.ctx, abcitypes.RequestProcessProposal{
|
||||
Txs: txs,
|
||||
})
|
||||
|
||||
// Check if the response is valid
|
||||
suite.Require().Equal(tc.response, res.Status)
|
||||
})
|
||||
}
|
||||
return commitInfoBz
|
||||
}
|
||||
|
||||
// isTopBidValid returns true if the top bid is valid. We purposefully insert invalid
|
||||
// auction transactions into the mempool to test the handlers.
|
||||
func (suite *ABCITestSuite) isTopBidValid() bool {
|
||||
iterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
if iterator == nil {
|
||||
return false
|
||||
func (suite *ABCITestSuite) createAuctionInfoFromTxBzs(txs [][]byte, numTxs uint64) []byte {
|
||||
auctionInfo := abci.AuctionInfo{
|
||||
ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs),
|
||||
NumTxs: numTxs,
|
||||
MaxTxBytes: int64(len(txs[0])),
|
||||
}
|
||||
|
||||
// check if the top bid is valid
|
||||
_, err := suite.anteHandler(suite.ctx, iterator.Tx(), true)
|
||||
return err == nil
|
||||
auctionInfoBz, err := auctionInfo.Marshal()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return auctionInfoBz
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) getAuctionBidInfoFromTxBz(txBz []byte) *buildertypes.BidInfo {
|
||||
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return bidInfo
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package v2_test
|
||||
package abci_test
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
@ -481,17 +481,17 @@ func (suite *ABCITestSuite) TestBuildTOB() {
|
||||
proposal := suite.proposalHandler.BuildTOB(suite.ctx, commitInfo, tc.maxBytes)
|
||||
|
||||
// Size of the proposal should be less than or equal to the max bytes
|
||||
suite.Require().LessOrEqual(proposal.Size, tc.maxBytes)
|
||||
suite.Require().LessOrEqual(proposal.TotalTxBytes, tc.maxBytes)
|
||||
|
||||
if winningBid == nil {
|
||||
suite.Require().Len(proposal.Txs, 0)
|
||||
suite.Require().Equal(proposal.Size, int64(0))
|
||||
suite.Require().Equal(proposal.TotalTxBytes, int64(0))
|
||||
} else {
|
||||
// Get info about the winning bid
|
||||
winningBidBz, err := suite.encodingConfig.TxConfig.TxEncoder()(winningBid)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionBidInfo, err := suite.mempool.GetAuctionBidInfo(winningBid)
|
||||
auctionBidInfo, err := suite.tobLane.GetAuctionBidInfo(winningBid)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Verify that the size of the proposal is the size of the winning bid
|
||||
@ -1,40 +1,20 @@
|
||||
package v2
|
||||
package abci
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
abci "github.com/cometbft/cometbft/abci/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
pobabci "github.com/skip-mev/pob/abci"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
// TopOfBlock contains information about how the top of block should be built.
|
||||
type TopOfBlock struct {
|
||||
// Txs contains the transactions that should be included in the top of block.
|
||||
Txs [][]byte
|
||||
|
||||
// Size is the total size of the top of block.
|
||||
Size int64
|
||||
|
||||
// Cache is the cache of transactions that were seen, stored in order to ignore them
|
||||
// when building the rest of the block.
|
||||
Cache map[string]struct{}
|
||||
}
|
||||
|
||||
func NewTopOfBlock() TopOfBlock {
|
||||
return TopOfBlock{
|
||||
Cache: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildTOB inputs all of the vote extensions and outputs a top of block proposal
|
||||
// that includes the highest bidding valid transaction along with all the bundled
|
||||
// transactions.
|
||||
func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.ExtendedCommitInfo, maxBytes int64) TopOfBlock {
|
||||
func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.ExtendedCommitInfo, maxBytes int64) *blockbuster.Proposal {
|
||||
// Get the bid transactions from the vote extensions.
|
||||
sortedBidTxs := h.GetBidsFromVoteExtensions(voteExtensionInfo.Votes)
|
||||
|
||||
@ -43,14 +23,14 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten
|
||||
|
||||
// Attempt to select the highest bid transaction that is valid and whose
|
||||
// bundled transactions are valid.
|
||||
var topOfBlock TopOfBlock
|
||||
topOfBlock := blockbuster.NewProposal(maxBytes)
|
||||
for _, bidTx := range sortedBidTxs {
|
||||
// Cache the context so that we can write it back to the original context
|
||||
// when we know we have a valid top of block bundle.
|
||||
cacheCtx, write := ctx.CacheContext()
|
||||
|
||||
// Attempt to build the top of block using the bid transaction.
|
||||
proposal, err := h.buildTOB(cacheCtx, bidTx)
|
||||
proposal, err := h.buildTOB(cacheCtx, bidTx, maxBytes)
|
||||
if err != nil {
|
||||
h.logger.Info(
|
||||
"vote extension auction failed to verify auction tx",
|
||||
@ -60,27 +40,22 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten
|
||||
continue
|
||||
}
|
||||
|
||||
if proposal.Size <= maxBytes {
|
||||
// At this point, both the bid transaction itself and all the bundled
|
||||
// transactions are valid. So we select the bid transaction along with
|
||||
// all the bundled transactions and apply the state changes to the cache
|
||||
// context.
|
||||
topOfBlock = proposal
|
||||
write()
|
||||
// At this point, both the bid transaction itself and all the bundled
|
||||
// transactions are valid. So we select the bid transaction along with
|
||||
// all the bundled transactions and apply the state changes to the cache
|
||||
// context.
|
||||
topOfBlock = proposal
|
||||
write()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
h.logger.Info(
|
||||
"failed to select auction bid tx; auction tx size is too large",
|
||||
"tx_size", proposal.Size,
|
||||
"max_size", maxBytes,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Remove all of the transactions that were not valid.
|
||||
for tx := range txsToRemove {
|
||||
h.RemoveTx(tx)
|
||||
if err := utils.RemoveTxsFromLane(txsToRemove, h.tobLane); err != nil {
|
||||
h.logger.Error(
|
||||
"failed to remove transactions from lane",
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
|
||||
return topOfBlock
|
||||
@ -88,14 +63,14 @@ func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.Exten
|
||||
|
||||
// VerifyTOB verifies that the set of vote extensions used in prepare proposal deterministically
|
||||
// produce the same top of block proposal.
|
||||
func (h *ProposalHandler) VerifyTOB(ctx sdk.Context, proposalTxs [][]byte) (*pobabci.AuctionInfo, error) {
|
||||
func (h *ProposalHandler) VerifyTOB(ctx sdk.Context, proposalTxs [][]byte) (*AuctionInfo, error) {
|
||||
// Proposal must include at least the auction info.
|
||||
if len(proposalTxs) < NumInjectedTxs {
|
||||
return nil, fmt.Errorf("proposal is too small; expected at least %d slots", NumInjectedTxs)
|
||||
}
|
||||
|
||||
// Extract the auction info from the proposal.
|
||||
auctionInfo := &pobabci.AuctionInfo{}
|
||||
auctionInfo := &AuctionInfo{}
|
||||
if err := auctionInfo.Unmarshal(proposalTxs[AuctionInfoIndex]); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal auction info: %w", err)
|
||||
}
|
||||
@ -141,12 +116,12 @@ func (h *ProposalHandler) GetBidsFromVoteExtensions(voteExtensions []abci.Extend
|
||||
// Sort the auction transactions by their bid amount in descending order.
|
||||
sort.Slice(bidTxs, func(i, j int) bool {
|
||||
// In the case of an error, we want to sort the transaction to the end of the list.
|
||||
bidInfoI, err := h.mempool.GetAuctionBidInfo(bidTxs[i])
|
||||
bidInfoI, err := h.tobLane.GetAuctionBidInfo(bidTxs[i])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
bidInfoJ, err := h.mempool.GetAuctionBidInfo(bidTxs[j])
|
||||
bidInfoJ, err := h.tobLane.GetAuctionBidInfo(bidTxs[j])
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
@ -161,16 +136,29 @@ func (h *ProposalHandler) GetBidsFromVoteExtensions(voteExtensions []abci.Extend
|
||||
// returns the transactions that should be included in the top of block, size
|
||||
// of the auction transaction and bundle, and a cache of all transactions that
|
||||
// should be ignored.
|
||||
func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx) (TopOfBlock, error) {
|
||||
proposal := NewTopOfBlock()
|
||||
func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx, maxBytes int64) (*blockbuster.Proposal, error) {
|
||||
proposal := blockbuster.NewProposal(maxBytes)
|
||||
|
||||
// Ensure that the bid transaction is valid
|
||||
bidTxBz, err := h.PrepareProposalVerifyTx(ctx, bidTx)
|
||||
// cache the bytes of the bid transaction
|
||||
txBz, hash, err := utils.GetTxHashStr(h.txEncoder, bidTx)
|
||||
if err != nil {
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx)
|
||||
proposal.Cache[hash] = struct{}{}
|
||||
proposal.TotalTxBytes = int64(len(txBz))
|
||||
proposal.Txs = append(proposal.Txs, txBz)
|
||||
|
||||
if int64(len(txBz)) > maxBytes {
|
||||
return proposal, fmt.Errorf("bid transaction is too large; got %d, max %d", len(txBz), maxBytes)
|
||||
}
|
||||
|
||||
// Ensure that the bid transaction is valid
|
||||
if err := h.tobLane.VerifyTx(ctx, bidTx); err != nil {
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
bidInfo, err := h.tobLane.GetAuctionBidInfo(bidTx)
|
||||
if err != nil {
|
||||
return proposal, err
|
||||
}
|
||||
@ -181,34 +169,23 @@ func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx) (TopOfBlock, e
|
||||
// Ensure that the bundled transactions are valid
|
||||
for index, rawRefTx := range bidInfo.Transactions {
|
||||
// convert the bundled raw transaction to a sdk.Tx
|
||||
refTx, err := h.mempool.WrapBundleTransaction(rawRefTx)
|
||||
refTx, err := h.tobLane.WrapBundleTransaction(rawRefTx)
|
||||
if err != nil {
|
||||
return TopOfBlock{}, err
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
txBz, err := h.PrepareProposalVerifyTx(ctx, refTx)
|
||||
// convert the sdk.Tx to a hash and bytes
|
||||
txBz, hash, err := utils.GetTxHashStr(h.txEncoder, refTx)
|
||||
if err != nil {
|
||||
return TopOfBlock{}, err
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
hashBz := sha256.Sum256(txBz)
|
||||
hash := hex.EncodeToString(hashBz[:])
|
||||
|
||||
proposal.Cache[hash] = struct{}{}
|
||||
sdkTxBytes[index] = txBz
|
||||
}
|
||||
|
||||
// cache the bytes of the bid transaction
|
||||
hashBz := sha256.Sum256(bidTxBz)
|
||||
hash := hex.EncodeToString(hashBz[:])
|
||||
proposal.Cache[hash] = struct{}{}
|
||||
|
||||
txs := [][]byte{bidTxBz}
|
||||
txs = append(txs, sdkTxBytes...)
|
||||
|
||||
// Set the top of block transactions and size.
|
||||
proposal.Txs = txs
|
||||
proposal.Size = int64(len(bidTxBz))
|
||||
// Add the bundled transactions to the proposal.
|
||||
proposal.Txs = append(proposal.Txs, sdkTxBytes...)
|
||||
|
||||
return proposal, nil
|
||||
}
|
||||
@ -227,7 +204,7 @@ func (h *ProposalHandler) getAuctionTxFromVoteExtension(voteExtension []byte) (s
|
||||
}
|
||||
|
||||
// Verify the auction transaction has bid information.
|
||||
if bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil {
|
||||
if bidInfo, err := h.tobLane.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil {
|
||||
return nil, fmt.Errorf("vote extension does not contain an auction transaction")
|
||||
}
|
||||
|
||||
152
abci/proposals.go
Normal file
152
abci/proposals.go
Normal file
@ -0,0 +1,152 @@
|
||||
package abci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
cometabci "github.com/cometbft/cometbft/abci/types"
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/abci"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
)
|
||||
|
||||
const (
|
||||
// NumInjectedTxs is the minimum number of transactions that were injected into
|
||||
// the proposal but are not actual transactions. In this case, the auction
|
||||
// info is injected into the proposal but should be ignored by the application.ß
|
||||
NumInjectedTxs = 1
|
||||
|
||||
// AuctionInfoIndex is the index of the auction info in the proposal.
|
||||
AuctionInfoIndex = 0
|
||||
)
|
||||
|
||||
type (
|
||||
// TOBLaneProposal is the interface that defines all of the dependencies that
|
||||
// are required to interact with the top of block lane.
|
||||
TOBLaneProposal interface {
|
||||
sdkmempool.Mempool
|
||||
|
||||
// Factory defines the API/functionality which is responsible for determining
|
||||
// if a transaction is a bid transaction and how to extract relevant
|
||||
// information from the transaction (bid, timeout, bidder, etc.).
|
||||
auction.Factory
|
||||
|
||||
// VerifyTx is utilized to verify a bid transaction according to the preferences
|
||||
// of the top of block lane.
|
||||
VerifyTx(ctx sdk.Context, tx sdk.Tx) error
|
||||
|
||||
// ProcessLaneBasic is utilized to verify the rest of the proposal according to
|
||||
// the preferences of the top of block lane. This is used to verify that no
|
||||
ProcessLaneBasic(txs [][]byte) error
|
||||
}
|
||||
|
||||
// ProposalHandler contains the functionality and handlers required to\
|
||||
// process, validate and build blocks.
|
||||
ProposalHandler struct {
|
||||
prepareLanesHandler blockbuster.PrepareLanesHandler
|
||||
processLanesHandler blockbuster.ProcessLanesHandler
|
||||
tobLane TOBLaneProposal
|
||||
logger log.Logger
|
||||
txEncoder sdk.TxEncoder
|
||||
txDecoder sdk.TxDecoder
|
||||
}
|
||||
)
|
||||
|
||||
// NewProposalHandler returns a ProposalHandler that contains the functionality and handlers
|
||||
// required to process, validate and build blocks.
|
||||
func NewProposalHandler(
|
||||
lanes []blockbuster.Lane,
|
||||
tobLane TOBLaneProposal,
|
||||
logger log.Logger,
|
||||
txEncoder sdk.TxEncoder,
|
||||
txDecoder sdk.TxDecoder,
|
||||
) *ProposalHandler {
|
||||
return &ProposalHandler{
|
||||
prepareLanesHandler: abci.ChainPrepareLanes(lanes...),
|
||||
processLanesHandler: abci.ChainProcessLanes(lanes...),
|
||||
tobLane: tobLane,
|
||||
logger: logger,
|
||||
txEncoder: txEncoder,
|
||||
txDecoder: txDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs
|
||||
// top-of-block auctioning and general block proposal construction.
|
||||
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
|
||||
return func(ctx sdk.Context, req cometabci.RequestPrepareProposal) cometabci.ResponsePrepareProposal {
|
||||
// Build the top of block portion of the proposal given the vote extensions
|
||||
// from the previous block.
|
||||
topOfBlock := h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes)
|
||||
|
||||
// If information is unable to be marshaled, we return an empty proposal. This will
|
||||
// cause another proposal to be generated after it is rejected in ProcessProposal.
|
||||
lastCommitInfo, err := req.LocalLastCommit.Marshal()
|
||||
if err != nil {
|
||||
h.logger.Error("failed to marshal last commit info", "err", err)
|
||||
return cometabci.ResponsePrepareProposal{Txs: nil}
|
||||
}
|
||||
|
||||
auctionInfo := &AuctionInfo{
|
||||
ExtendedCommitInfo: lastCommitInfo,
|
||||
MaxTxBytes: req.MaxTxBytes,
|
||||
NumTxs: uint64(len(topOfBlock.Txs)),
|
||||
}
|
||||
|
||||
// Add the auction info and top of block transactions into the proposal.
|
||||
auctionInfoBz, err := auctionInfo.Marshal()
|
||||
if err != nil {
|
||||
h.logger.Error("failed to marshal auction info", "err", err)
|
||||
return cometabci.ResponsePrepareProposal{Txs: nil}
|
||||
}
|
||||
|
||||
topOfBlock.Txs = append([][]byte{auctionInfoBz}, topOfBlock.Txs...)
|
||||
|
||||
// Prepare the proposal by selecting transactions from each lane according to
|
||||
// each lane's selection logic.
|
||||
proposal := h.prepareLanesHandler(ctx, topOfBlock)
|
||||
|
||||
return cometabci.ResponsePrepareProposal{Txs: proposal.Txs}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs
|
||||
// block proposal verification.
|
||||
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
|
||||
return func(ctx sdk.Context, req cometabci.RequestProcessProposal) cometabci.ResponseProcessProposal {
|
||||
proposal := req.Txs
|
||||
|
||||
// Verify that the same top of block transactions can be built from the vote
|
||||
// extensions included in the proposal.
|
||||
auctionInfo, err := h.VerifyTOB(ctx, proposal)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to verify top of block transactions", "err", err)
|
||||
return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
// Do a basic check of the rest of the proposal to make sure no auction transactions
|
||||
// are included.
|
||||
if err := h.tobLane.ProcessLaneBasic(proposal[NumInjectedTxs:]); err != nil {
|
||||
h.logger.Error("failed to process proposal", "err", err)
|
||||
return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
// Verify that the rest of the proposal is valid according to each lane's verification logic.
|
||||
if _, err = h.processLanesHandler(ctx, proposal[auctionInfo.NumTxs:]); err != nil {
|
||||
h.logger.Error("failed to process proposal", "err", err)
|
||||
return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
return cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTx removes a transaction from the application-side mempool.
|
||||
func (h *ProposalHandler) RemoveTx(tx sdk.Tx) {
|
||||
if err := h.tobLane.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
}
|
||||
776
abci/proposals_test.go
Normal file
776
abci/proposals_test.go
Normal file
@ -0,0 +1,776 @@
|
||||
package abci_test
|
||||
|
||||
import (
|
||||
comettypes "github.com/cometbft/cometbft/abci/types"
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/abci"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/base"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/ante"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
func (suite *ABCITestSuite) TestPrepareProposal() {
|
||||
var (
|
||||
// the modified transactions cannot exceed this size
|
||||
maxTxBytes int64 = 1000000000000000000
|
||||
|
||||
// mempool configuration
|
||||
normalTxs []sdk.Tx
|
||||
auctionTxs []sdk.Tx
|
||||
winningBidTx sdk.Tx
|
||||
insertBundledTxs = false
|
||||
|
||||
// auction configuration
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
frontRunningProtection = true
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
expectedNumberProposalTxs int
|
||||
expectedMempoolDistribution map[string]int
|
||||
}{
|
||||
{
|
||||
"single valid tob transaction in the mempool",
|
||||
func() {
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{}
|
||||
auctionTxs = []sdk.Tx{bidTx}
|
||||
winningBidTx = bidTx
|
||||
insertBundledTxs = false
|
||||
},
|
||||
2,
|
||||
map[string]int{
|
||||
base.LaneName: 0,
|
||||
auction.LaneName: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single invalid tob transaction in the mempool",
|
||||
func() {
|
||||
bidder := suite.accounts[0]
|
||||
bid := reserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1))) // bid is less than the reserve fee
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{}
|
||||
auctionTxs = []sdk.Tx{bidTx}
|
||||
winningBidTx = nil
|
||||
insertBundledTxs = false
|
||||
},
|
||||
0,
|
||||
map[string]int{
|
||||
base.LaneName: 0,
|
||||
auction.LaneName: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"normal transactions in the mempool",
|
||||
func() {
|
||||
account := suite.accounts[0]
|
||||
nonce := suite.nonces[account.Address.String()]
|
||||
timeout := uint64(100)
|
||||
numberMsgs := uint64(3)
|
||||
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{normalTx}
|
||||
auctionTxs = []sdk.Tx{}
|
||||
winningBidTx = nil
|
||||
insertBundledTxs = false
|
||||
},
|
||||
1,
|
||||
map[string]int{
|
||||
base.LaneName: 1,
|
||||
auction.LaneName: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"normal transactions and tob transactions in the mempool",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a valid default transaction
|
||||
account := suite.accounts[1]
|
||||
nonce = suite.nonces[account.Address.String()] + 1
|
||||
numberMsgs := uint64(3)
|
||||
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{normalTx}
|
||||
auctionTxs = []sdk.Tx{bidTx}
|
||||
winningBidTx = bidTx
|
||||
insertBundledTxs = false
|
||||
},
|
||||
3,
|
||||
map[string]int{
|
||||
base.LaneName: 1,
|
||||
auction.LaneName: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple tob transactions where the first is invalid",
|
||||
func() {
|
||||
// Create an invalid tob transaction (frontrunning)
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000000000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder, bidder, suite.accounts[1]}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a valid tob transaction
|
||||
bidder = suite.accounts[1]
|
||||
bid = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce = suite.nonces[bidder.Address.String()]
|
||||
timeout = uint64(100)
|
||||
signers = []testutils.Account{bidder}
|
||||
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{}
|
||||
auctionTxs = []sdk.Tx{bidTx, bidTx2}
|
||||
winningBidTx = bidTx2
|
||||
insertBundledTxs = false
|
||||
},
|
||||
2,
|
||||
map[string]int{
|
||||
base.LaneName: 0,
|
||||
auction.LaneName: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple tob transactions where the first is valid",
|
||||
func() {
|
||||
// Create an valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{suite.accounts[2], bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a valid tob transaction
|
||||
bidder = suite.accounts[1]
|
||||
bid = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce = suite.nonces[bidder.Address.String()]
|
||||
timeout = uint64(100)
|
||||
signers = []testutils.Account{bidder}
|
||||
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{}
|
||||
auctionTxs = []sdk.Tx{bidTx, bidTx2}
|
||||
winningBidTx = bidTx
|
||||
insertBundledTxs = false
|
||||
},
|
||||
3,
|
||||
map[string]int{
|
||||
base.LaneName: 0,
|
||||
auction.LaneName: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple tob transactions where the first is valid and bundle is inserted into mempool",
|
||||
func() {
|
||||
frontRunningProtection = false
|
||||
|
||||
// Create an valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{}
|
||||
auctionTxs = []sdk.Tx{bidTx}
|
||||
winningBidTx = bidTx
|
||||
insertBundledTxs = true
|
||||
},
|
||||
6,
|
||||
map[string]int{
|
||||
base.LaneName: 5,
|
||||
auction.LaneName: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"single tob transaction with other normal transactions in the mempool",
|
||||
func() {
|
||||
// Create an valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
account := suite.accounts[5]
|
||||
nonce = suite.nonces[account.Address.String()]
|
||||
timeout = uint64(100)
|
||||
numberMsgs := uint64(3)
|
||||
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
normalTxs = []sdk.Tx{normalTx}
|
||||
auctionTxs = []sdk.Tx{bidTx}
|
||||
winningBidTx = bidTx
|
||||
insertBundledTxs = true
|
||||
},
|
||||
7,
|
||||
map[string]int{
|
||||
base.LaneName: 6,
|
||||
auction.LaneName: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
tc.malleate()
|
||||
|
||||
// Insert all of the normal transactions into the default lane
|
||||
for _, tx := range normalTxs {
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
|
||||
}
|
||||
|
||||
// Insert all of the auction transactions into the TOB lane
|
||||
for _, tx := range auctionTxs {
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
|
||||
}
|
||||
|
||||
// Insert all of the bundled transactions into the TOB lane if desired
|
||||
if insertBundledTxs {
|
||||
for _, tx := range auctionTxs {
|
||||
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
for _, txBz := range bidInfo.Transactions {
|
||||
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a new auction
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: frontRunningProtection,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
|
||||
|
||||
suite.proposalHandler = abci.NewProposalHandler(
|
||||
[]blockbuster.Lane{suite.baseLane},
|
||||
suite.tobLane,
|
||||
suite.logger,
|
||||
suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
)
|
||||
handler := suite.proposalHandler.PrepareProposalHandler()
|
||||
req := suite.createPrepareProposalRequest(maxTxBytes)
|
||||
res := handler(suite.ctx, req)
|
||||
|
||||
// -------------------- Check Invariants -------------------- //
|
||||
// The first slot in the proposal must be the auction info
|
||||
auctionInfo := abci.AuctionInfo{}
|
||||
err := auctionInfo.Unmarshal(res.Txs[abci.AuctionInfoIndex])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Total bytes must be less than or equal to maxTxBytes
|
||||
totalBytes := int64(0)
|
||||
for _, tx := range res.Txs[abci.NumInjectedTxs:] {
|
||||
totalBytes += int64(len(tx))
|
||||
}
|
||||
suite.Require().LessOrEqual(totalBytes, maxTxBytes)
|
||||
|
||||
// 2. the number of transactions in the response must be equal to the number of expected transactions
|
||||
// NOTE: We add 1 to the expected number of transactions because the first transaction in the response
|
||||
// is the auction transaction
|
||||
suite.Require().Equal(tc.expectedNumberProposalTxs+1, len(res.Txs))
|
||||
|
||||
// 3. if there are auction transactions, the first transaction must be the top bid
|
||||
// and the rest of the bundle must be in the response
|
||||
if winningBidTx != nil {
|
||||
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.tobLane.GetAuctionBidInfo(auctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
for index, tx := range bidInfo.Transactions {
|
||||
suite.Require().Equal(tx, res.Txs[index+1+abci.NumInjectedTxs])
|
||||
}
|
||||
} else if len(res.Txs) > 1 {
|
||||
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Nil(bidInfo)
|
||||
|
||||
}
|
||||
|
||||
// 4. All of the transactions must be unique
|
||||
uniqueTxs := make(map[string]bool)
|
||||
for _, tx := range res.Txs {
|
||||
suite.Require().False(uniqueTxs[string(tx)])
|
||||
uniqueTxs[string(tx)] = true
|
||||
}
|
||||
|
||||
// 5. The number of transactions in the mempool must be correct
|
||||
suite.Require().Equal(tc.expectedMempoolDistribution, suite.mempool.GetTxDistribution())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) TestProcessProposal() {
|
||||
var (
|
||||
// auction configuration
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
frontRunningProtection = true
|
||||
|
||||
// mempool configuration
|
||||
proposal [][]byte
|
||||
)
|
||||
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: frontRunningProtection,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
createTxs func()
|
||||
response comettypes.ResponseProcessProposal_ProposalStatus
|
||||
}{
|
||||
{
|
||||
"no transactions in mempool with no vote extension info",
|
||||
func() {
|
||||
proposal = nil
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"no transactions in mempool with empty vote extension info",
|
||||
func() {
|
||||
proposal = [][]byte{}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single normal tx, no vote extension info",
|
||||
func() {
|
||||
account := suite.accounts[0]
|
||||
nonce := suite.nonces[account.Address.String()]
|
||||
timeout := uint64(100)
|
||||
numberMsgs := uint64(3)
|
||||
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
proposal = [][]byte{normalTxBz}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, single auction tx, no vote extension info",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a valid default transaction
|
||||
account := suite.accounts[1]
|
||||
nonce = suite.nonces[account.Address.String()] + 1
|
||||
numberMsgs := uint64(3)
|
||||
normalTx, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
proposal = [][]byte{bidTx, normalTx}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs (no unwrapping)",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a valid default transaction
|
||||
account := suite.accounts[1]
|
||||
nonce = suite.nonces[account.Address.String()] + 1
|
||||
numberMsgs := uint64(3)
|
||||
normalTx, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTx}, 2)
|
||||
|
||||
proposal = [][]byte{
|
||||
auctionInfo,
|
||||
bidTx,
|
||||
normalTx,
|
||||
}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs (with unwrapping)",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs but misplaced in proposal",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{suite.accounts[1], bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = [][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
bidInfo.Transactions[1],
|
||||
bidInfo.Transactions[0],
|
||||
}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, but auction tx is not valid",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder, suite.accounts[1]} // front-running
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs but wrong auction tx is at top of block",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder, bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create another valid tob transaction
|
||||
bidder = suite.accounts[1]
|
||||
bid = sdk.NewCoin("foo", sdk.NewInt(1000000))
|
||||
nonce = suite.nonces[bidder.Address.String()]
|
||||
timeout = uint64(100)
|
||||
signers = []testutils.Account{bidder}
|
||||
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 3)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs and correct auction tx is selected",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder, bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create another valid tob transaction
|
||||
bidder = suite.accounts[1]
|
||||
bid = sdk.NewCoin("foo", sdk.NewInt(1000000))
|
||||
nonce = suite.nonces[bidder.Address.String()]
|
||||
timeout = uint64(100)
|
||||
signers = []testutils.Account{bidder}
|
||||
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz2,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs included in block",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder, bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create another valid tob transaction
|
||||
bidder = suite.accounts[1]
|
||||
bid = sdk.NewCoin("foo", sdk.NewInt(1000000))
|
||||
nonce = suite.nonces[bidder.Address.String()]
|
||||
timeout = uint64(100)
|
||||
signers = []testutils.Account{bidder}
|
||||
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2)
|
||||
bidInfo2 := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz2,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
|
||||
proposal = append(proposal, bidTxBz)
|
||||
proposal = append(proposal, bidInfo2.Transactions...)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, but rest of the mempool is invalid",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
|
||||
proposal = append(proposal, []byte("invalid tx"))
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs with ref txs + normal transactions",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{bidder}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2)
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
|
||||
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[1], nonce, 3, timeout)
|
||||
suite.Require().NoError(err)
|
||||
proposal = append(proposal, normalTxBz)
|
||||
|
||||
normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[2], nonce, 3, timeout)
|
||||
suite.Require().NoError(err)
|
||||
proposal = append(proposal, normalTxBz)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"front-running protection disabled",
|
||||
func() {
|
||||
// Create a valid tob transaction
|
||||
bidder := suite.accounts[0]
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
nonce := suite.nonces[bidder.Address.String()]
|
||||
timeout := uint64(100)
|
||||
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
|
||||
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, uint64(len(signers)+1))
|
||||
|
||||
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
|
||||
|
||||
proposal = append(
|
||||
[][]byte{
|
||||
auctionInfo,
|
||||
bidTxBz,
|
||||
},
|
||||
bidInfo.Transactions...,
|
||||
)
|
||||
|
||||
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[5], nonce, 3, timeout)
|
||||
suite.Require().NoError(err)
|
||||
proposal = append(proposal, normalTxBz)
|
||||
|
||||
normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[6], nonce, 3, timeout)
|
||||
suite.Require().NoError(err)
|
||||
proposal = append(proposal, normalTxBz)
|
||||
|
||||
// disable frontrunning protection
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: false,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
// suite.SetupTest() // reset
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
|
||||
|
||||
// reset the proposal handler with the new mempool
|
||||
suite.proposalHandler = abci.NewProposalHandler(
|
||||
[]blockbuster.Lane{suite.baseLane},
|
||||
suite.tobLane, log.NewNopLogger(),
|
||||
suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
)
|
||||
|
||||
tc.createTxs()
|
||||
|
||||
handler := suite.proposalHandler.ProcessProposalHandler()
|
||||
res := handler(suite.ctx, comettypes.RequestProcessProposal{
|
||||
Txs: proposal,
|
||||
})
|
||||
|
||||
// Check if the response is valid
|
||||
suite.Require().Equal(tc.response, res.Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ alpha/RC tag is released. These types are simply used to prototype and develop
|
||||
against.
|
||||
*/
|
||||
//nolint
|
||||
package v2
|
||||
package abci
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
@ -1,336 +0,0 @@
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
comettypes "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"
|
||||
v2 "github.com/skip-mev/pob/abci/v2"
|
||||
"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 *v2.ProposalHandler
|
||||
voteExtensionHandler *v2.VoteExtensionHandler
|
||||
config mempool.AuctionFactory
|
||||
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.WithBlockHeight(1)
|
||||
|
||||
// Mempool set up
|
||||
suite.config = mempool.NewDefaultAuctionFactory(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.TxEncoder(), suite.mempool)
|
||||
|
||||
// Accounts set up
|
||||
suite.accounts = testutils.RandomAccounts(suite.random, 10)
|
||||
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 = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())
|
||||
suite.voteExtensionHandler = v2.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler)
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
signer := tx.GetMsgs()[0].GetSigners()[0]
|
||||
suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balances)
|
||||
|
||||
next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
ctx, err := suite.builderDecorator.AnteHandle(ctx, tx, false, next)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
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() [][]byte {
|
||||
txs := make([][]byte, 0)
|
||||
seenTxs := make(map[string]bool)
|
||||
|
||||
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) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal {
|
||||
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
|
||||
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
|
||||
tx := auctionIterator.Tx()
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: txBz,
|
||||
})
|
||||
}
|
||||
|
||||
return comettypes.RequestPrepareProposal{
|
||||
MaxTxBytes: maxBytes,
|
||||
LocalLastCommit: comettypes.ExtendedCommitInfo{
|
||||
Votes: voteExtensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxBzs(txs [][]byte) []byte {
|
||||
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
|
||||
for _, txBz := range txs {
|
||||
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: txBz,
|
||||
})
|
||||
}
|
||||
|
||||
commitInfo := comettypes.ExtendedCommitInfo{
|
||||
Votes: voteExtensions,
|
||||
}
|
||||
|
||||
commitInfoBz, err := commitInfo.Marshal()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return commitInfoBz
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createAuctionInfoFromTxBzs(txs [][]byte, numTxs uint64) []byte {
|
||||
auctionInfo := abci.AuctionInfo{
|
||||
ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs),
|
||||
NumTxs: numTxs,
|
||||
MaxTxBytes: int64(len(txs[0])),
|
||||
}
|
||||
|
||||
auctionInfoBz, err := auctionInfo.Marshal()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return auctionInfoBz
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) getAllAuctionTxs() ([]sdk.Tx, [][]byte) {
|
||||
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
txs := make([]sdk.Tx, 0)
|
||||
txBzs := make([][]byte, 0)
|
||||
|
||||
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
|
||||
txs = append(txs, auctionIterator.Tx())
|
||||
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionIterator.Tx())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
txBzs = append(txBzs, bz)
|
||||
}
|
||||
|
||||
return txs, txBzs
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxs(txs []sdk.Tx) comettypes.ExtendedCommitInfo {
|
||||
voteExtensions := make([][]byte, 0)
|
||||
for _, tx := range txs {
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
voteExtensions = append(voteExtensions, bz)
|
||||
}
|
||||
|
||||
return suite.createExtendedCommitInfo(voteExtensions)
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedVoteInfo(voteExtensions [][]byte) []comettypes.ExtendedVoteInfo {
|
||||
commitInfo := make([]comettypes.ExtendedVoteInfo, 0)
|
||||
for _, voteExtension := range voteExtensions {
|
||||
info := comettypes.ExtendedVoteInfo{
|
||||
VoteExtension: voteExtension,
|
||||
}
|
||||
|
||||
commitInfo = append(commitInfo, info)
|
||||
}
|
||||
|
||||
return commitInfo
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) createExtendedCommitInfo(voteExtensions [][]byte) comettypes.ExtendedCommitInfo {
|
||||
commitInfo := comettypes.ExtendedCommitInfo{
|
||||
Votes: suite.createExtendedVoteInfo(voteExtensions),
|
||||
}
|
||||
|
||||
return commitInfo
|
||||
}
|
||||
@ -1,237 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
abci "github.com/cometbft/cometbft/abci/types"
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
pobabci "github.com/skip-mev/pob/abci"
|
||||
mempool "github.com/skip-mev/pob/mempool"
|
||||
)
|
||||
|
||||
const (
|
||||
// NumInjectedTxs is the minimum number of transactions that were injected into
|
||||
// the proposal but are not actual transactions. In this case, the auction
|
||||
// info is injected into the proposal but should be ignored by the application.ß
|
||||
NumInjectedTxs = 1
|
||||
|
||||
// AuctionInfoIndex is the index of the auction info in the proposal.
|
||||
AuctionInfoIndex = 0
|
||||
)
|
||||
|
||||
type (
|
||||
// ProposalMempool contains the methods required by the ProposalHandler
|
||||
// to interact with the local mempool.
|
||||
ProposalMempool interface {
|
||||
sdkmempool.Mempool
|
||||
|
||||
// The AuctionFactory interface is utilized to retrieve, validate, and wrap bid
|
||||
// information into the block proposal.
|
||||
mempool.AuctionFactory
|
||||
|
||||
// AuctionBidSelect returns an iterator that iterates over the top bid
|
||||
// transactions in the mempool.
|
||||
AuctionBidSelect(ctx context.Context) sdkmempool.Iterator
|
||||
}
|
||||
|
||||
// ProposalHandler contains the functionality and handlers required to\
|
||||
// process, validate and build blocks.
|
||||
ProposalHandler struct {
|
||||
mempool ProposalMempool
|
||||
logger log.Logger
|
||||
anteHandler sdk.AnteHandler
|
||||
txEncoder sdk.TxEncoder
|
||||
txDecoder sdk.TxDecoder
|
||||
}
|
||||
)
|
||||
|
||||
// NewProposalHandler returns a ProposalHandler that contains the functionality and handlers
|
||||
// required to process, validate and build blocks.
|
||||
func NewProposalHandler(
|
||||
mp ProposalMempool,
|
||||
logger log.Logger,
|
||||
anteHandler sdk.AnteHandler,
|
||||
txEncoder sdk.TxEncoder,
|
||||
txDecoder sdk.TxDecoder,
|
||||
) *ProposalHandler {
|
||||
return &ProposalHandler{
|
||||
mempool: mp,
|
||||
logger: logger,
|
||||
anteHandler: anteHandler,
|
||||
txEncoder: txEncoder,
|
||||
txDecoder: txDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs
|
||||
// top-of-block auctioning and general block proposal construction.
|
||||
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
|
||||
// Proposal includes all of the transactions that will be included in the
|
||||
// block along with the vote extensions from the previous block included at
|
||||
// the beginning of the proposal. Vote extensions must be included in the
|
||||
// first slot of the proposal because they are inaccessible in ProcessProposal.
|
||||
proposal := make([][]byte, 0)
|
||||
|
||||
// Build the top of block portion of the proposal given the vote extensions
|
||||
// from the previous block.
|
||||
topOfBlock := h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes)
|
||||
|
||||
// If information is unable to be marshaled, we return an empty proposal. This will
|
||||
// cause another proposal to be generated after it is rejected in ProcessProposal.
|
||||
lastCommitInfo, err := req.LocalLastCommit.Marshal()
|
||||
if err != nil {
|
||||
return abci.ResponsePrepareProposal{Txs: proposal}
|
||||
}
|
||||
|
||||
auctionInfo := pobabci.AuctionInfo{
|
||||
ExtendedCommitInfo: lastCommitInfo,
|
||||
MaxTxBytes: req.MaxTxBytes,
|
||||
NumTxs: uint64(len(topOfBlock.Txs)),
|
||||
}
|
||||
|
||||
// Add the auction info and top of block transactions into the proposal.
|
||||
auctionInfoBz, err := auctionInfo.Marshal()
|
||||
if err != nil {
|
||||
return abci.ResponsePrepareProposal{Txs: proposal}
|
||||
}
|
||||
|
||||
proposal = append(proposal, auctionInfoBz)
|
||||
proposal = append(proposal, topOfBlock.Txs...)
|
||||
|
||||
// Select remaining transactions for the block proposal until we've reached
|
||||
// size capacity.
|
||||
totalTxBytes := topOfBlock.Size
|
||||
txsToRemove := make(map[sdk.Tx]struct{}, 0)
|
||||
for iterator := h.mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
|
||||
memTx := iterator.Tx()
|
||||
|
||||
// If the transaction has already been seen in the top of block, skip it.
|
||||
txBz, err := h.txEncoder(memTx)
|
||||
if err != nil {
|
||||
txsToRemove[memTx] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
hashBz := sha256.Sum256(txBz)
|
||||
hash := hex.EncodeToString(hashBz[:])
|
||||
if _, ok := topOfBlock.Cache[hash]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify that the transaction is valid.
|
||||
txBz, err = h.PrepareProposalVerifyTx(ctx, memTx)
|
||||
if err != nil {
|
||||
txsToRemove[memTx] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
txSize := int64(len(txBz))
|
||||
if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes {
|
||||
proposal = append(proposal, txBz)
|
||||
} else {
|
||||
// We've reached capacity per req.MaxTxBytes so we cannot select any
|
||||
// more transactions.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all invalid transactions from the mempool.
|
||||
for tx := range txsToRemove {
|
||||
h.RemoveTx(tx)
|
||||
}
|
||||
|
||||
return abci.ResponsePrepareProposal{Txs: proposal}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs
|
||||
// block proposal verification.
|
||||
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal {
|
||||
proposal := req.Txs
|
||||
|
||||
// Verify that the same top of block transactions can be built from the vote
|
||||
// extensions included in the proposal.
|
||||
auctionInfo, err := h.VerifyTOB(ctx, proposal)
|
||||
if err != nil {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
// Track the transactions that need to be removed from the mempool.
|
||||
txsToRemove := make(map[sdk.Tx]struct{}, 0)
|
||||
invalidProposal := false
|
||||
|
||||
// Verify that the remaining transactions in the proposal are valid.
|
||||
for _, txBz := range proposal[auctionInfo.NumTxs+NumInjectedTxs:] {
|
||||
tx, err := h.ProcessProposalVerifyTx(ctx, txBz)
|
||||
if tx == nil || err != nil {
|
||||
invalidProposal = true
|
||||
if tx != nil {
|
||||
txsToRemove[tx] = struct{}{}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// The only auction transactions that should be included in the block proposal
|
||||
// must be at the top of the block.
|
||||
if bidInfo, err := h.mempool.GetAuctionBidInfo(tx); err != nil || bidInfo != nil {
|
||||
invalidProposal = true
|
||||
}
|
||||
}
|
||||
// Remove all invalid transactions from the mempool.
|
||||
for tx := range txsToRemove {
|
||||
h.RemoveTx(tx)
|
||||
}
|
||||
|
||||
if invalidProposal {
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareProposalVerifyTx encodes a transaction and verifies it.
|
||||
func (h *ProposalHandler) PrepareProposalVerifyTx(ctx sdk.Context, tx sdk.Tx) ([]byte, error) {
|
||||
txBz, err := h.txEncoder(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBz, h.verifyTx(ctx, tx)
|
||||
}
|
||||
|
||||
// ProcessProposalVerifyTx decodes a transaction and verifies it.
|
||||
func (h *ProposalHandler) ProcessProposalVerifyTx(ctx sdk.Context, txBz []byte) (sdk.Tx, error) {
|
||||
tx, err := h.txDecoder(txBz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx, h.verifyTx(ctx, tx)
|
||||
}
|
||||
|
||||
// RemoveTx removes a transaction from the application-side mempool.
|
||||
func (h *ProposalHandler) RemoveTx(tx sdk.Tx) {
|
||||
if err := h.mempool.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyTx verifies a transaction against the application's state.
|
||||
func (h *ProposalHandler) verifyTx(ctx sdk.Context, tx sdk.Tx) error {
|
||||
if h.anteHandler != nil {
|
||||
_, err := h.anteHandler(ctx, tx, false)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,766 +0,0 @@
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
comettypes "github.com/cometbft/cometbft/abci/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/abci"
|
||||
v2 "github.com/skip-mev/pob/abci/v2"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/ante"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
func (suite *ABCITestSuite) TestPrepareProposal() {
|
||||
var (
|
||||
// the modified transactions cannot exceed this size
|
||||
maxTxBytes int64 = 1000000000000000000
|
||||
|
||||
// mempool configuration
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 100
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
expectedTopAuctionTx sdk.Tx
|
||||
|
||||
// auction configuration
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
frontRunningProtection = true
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
malleate func()
|
||||
expectedNumberProposalTxs int
|
||||
expectedNumberTxsInMempool int
|
||||
expectedNumberTxsInAuctionMempool int
|
||||
}{
|
||||
{
|
||||
"single bundle in the mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
5,
|
||||
3,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, no ref txs in mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
5,
|
||||
0,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, not valid",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = nil
|
||||
},
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"single bundle in the mempool, not valid with ref txs in mempool",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000)) // this will fail the ante handler
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = nil
|
||||
},
|
||||
4,
|
||||
3,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"multiple bundles in the mempool, no normal txs + no ref txs in mempool",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(10000000))
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
5,
|
||||
0,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"multiple bundles in the mempool, normal txs + ref txs in mempool",
|
||||
func() {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
32,
|
||||
30,
|
||||
10,
|
||||
},
|
||||
{
|
||||
"normal txs only",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"many normal txs only",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
101,
|
||||
100,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"single normal tx, single auction tx",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"single normal tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
6,
|
||||
4,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"single normal tx, single failing auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000)) // this will fail the ante handler
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000000000))
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = nil
|
||||
},
|
||||
5,
|
||||
4,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"many normal tx, single auction tx with no ref txs",
|
||||
func() {
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000000))
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = nil
|
||||
},
|
||||
102,
|
||||
100,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"many normal tx, single auction tx with ref txs",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 100
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
},
|
||||
402,
|
||||
400,
|
||||
100,
|
||||
},
|
||||
{
|
||||
"many normal tx, many auction tx with ref txs but top bid is invalid",
|
||||
func() {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 100
|
||||
numBundledTxs = 1
|
||||
insertRefTxs = true
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
|
||||
// create a new bid that is greater than the current top bid
|
||||
bid := sdk.NewCoin("foo", sdk.NewInt(200000000000000000))
|
||||
bidTx, err := testutils.CreateAuctionTxWithSigners(
|
||||
suite.encodingConfig.TxConfig,
|
||||
suite.accounts[0],
|
||||
bid,
|
||||
0,
|
||||
0,
|
||||
[]testutils.Account{suite.accounts[0], suite.accounts[1]},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// add the new bid to the mempool
|
||||
err = suite.mempool.Insert(suite.ctx, bidTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Require().Equal(suite.mempool.CountAuctionTx(), 101)
|
||||
},
|
||||
202,
|
||||
200,
|
||||
100,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
tc.malleate()
|
||||
|
||||
// Create a new auction.
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: frontRunningProtection,
|
||||
MinBidIncrement: suite.minBidIncrement,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
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())
|
||||
|
||||
// Create a prepare proposal request based on the current state of the mempool.
|
||||
handler := suite.proposalHandler.PrepareProposalHandler()
|
||||
req := suite.createPrepareProposalRequest(maxTxBytes)
|
||||
res := handler(suite.ctx, req)
|
||||
|
||||
// -------------------- Check Invariants -------------------- //
|
||||
// The first slot in the proposal must be the auction info
|
||||
auctionInfo := abci.AuctionInfo{}
|
||||
err := auctionInfo.Unmarshal(res.Txs[v2.AuctionInfoIndex])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Total bytes must be less than or equal to maxTxBytes
|
||||
totalBytes := int64(0)
|
||||
for _, tx := range res.Txs[v2.NumInjectedTxs:] {
|
||||
totalBytes += int64(len(tx))
|
||||
}
|
||||
suite.Require().LessOrEqual(totalBytes, maxTxBytes)
|
||||
|
||||
// The number of transactions in the response must be equal to the number of expected transactions
|
||||
suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs))
|
||||
|
||||
// If there are auction transactions, the first transaction must be the top bid
|
||||
// and the rest of the bundle must be in the response
|
||||
if expectedTopAuctionTx != nil {
|
||||
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
for index, tx := range bidInfo.Transactions {
|
||||
suite.Require().Equal(tx, res.Txs[v2.NumInjectedTxs+index+1])
|
||||
}
|
||||
}
|
||||
|
||||
// 5. All of the transactions must be unique
|
||||
uniqueTxs := make(map[string]bool)
|
||||
for _, tx := range res.Txs[v2.NumInjectedTxs:] {
|
||||
suite.Require().False(uniqueTxs[string(tx)])
|
||||
uniqueTxs[string(tx)] = true
|
||||
}
|
||||
|
||||
// 6. The number of transactions in the mempool must be correct
|
||||
suite.Require().Equal(tc.expectedNumberTxsInMempool, suite.mempool.CountTx())
|
||||
suite.Require().Equal(tc.expectedNumberTxsInAuctionMempool, suite.mempool.CountAuctionTx())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ABCITestSuite) TestProcessProposal() {
|
||||
var (
|
||||
// mempool set up
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 3
|
||||
insertRefTxs = false
|
||||
|
||||
// auction set up
|
||||
maxBundleSize uint32 = 10
|
||||
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
createTxs func() [][]byte
|
||||
response comettypes.ResponseProcessProposal_ProposalStatus
|
||||
}{
|
||||
{
|
||||
"single normal tx, no vote extension info",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
txs := suite.exportMempool()
|
||||
|
||||
return txs
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, no vote extension info",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
return suite.exportMempool()
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, single auction tx, no vote extension info",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
return suite.exportMempool()
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs (no unwrapping)",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 1
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(topAuctionTx)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
}, suite.exportMempool()...)
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs (with unwrapping)",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(topAuctionTx)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
}, bidInfo.Transactions...)
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"single auction tx but no inclusion of ref txs",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 4
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(topAuctionTx)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
|
||||
|
||||
return [][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, but auction tx is not valid",
|
||||
func() [][]byte {
|
||||
tx, err := testutils.CreateAuctionTxWithSigners(
|
||||
suite.encodingConfig.TxConfig,
|
||||
suite.accounts[0],
|
||||
sdk.NewCoin("foo", sdk.NewInt(100)),
|
||||
1,
|
||||
0, // invalid timeout
|
||||
[]testutils.Account{},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
|
||||
|
||||
return [][]byte{
|
||||
auctionInfoBz,
|
||||
txBz,
|
||||
}
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with ref txs, but auction tx is not valid",
|
||||
func() [][]byte {
|
||||
tx, err := testutils.CreateAuctionTxWithSigners(
|
||||
suite.encodingConfig.TxConfig,
|
||||
suite.accounts[0],
|
||||
sdk.NewCoin("foo", sdk.NewInt(100)),
|
||||
1,
|
||||
1,
|
||||
[]testutils.Account{suite.accounts[1], suite.accounts[1], suite.accounts[0]},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 4)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(tx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return append([][]byte{
|
||||
auctionInfoBz,
|
||||
txBz,
|
||||
}, bidInfo.Transactions...)
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs but wrong auction tx is at top of block",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 2
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
_, auctionTxBzs := suite.getAllAuctionTxs()
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
|
||||
|
||||
proposal := [][]byte{
|
||||
auctionInfo,
|
||||
auctionTxBzs[1],
|
||||
}
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs included in block",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 2
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
_, auctionTxBzs := suite.getAllAuctionTxs()
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
|
||||
|
||||
proposal := [][]byte{
|
||||
auctionInfo,
|
||||
auctionTxBzs[0],
|
||||
auctionTxBzs[1],
|
||||
}
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx, but rest of the mempool is invalid",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 0
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(topAuctionTx)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
|
||||
|
||||
proposal := [][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
[]byte("invalid tx"),
|
||||
}
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"single auction tx with filled mempool, but rest of the mempool is invalid",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 1
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(topAuctionTx)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
}, suite.exportMempool()...)
|
||||
|
||||
proposal = append(proposal, []byte("invalid tx"))
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs with filled mempool",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
_, auctionTxBzs := suite.getAllAuctionTxs()
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
auctionTxBzs[0],
|
||||
}, suite.exportMempool()...)
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"multiple auction txs with ref txs + filled mempool",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 10
|
||||
numBundledTxs = 10
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
auctionTxs, auctionTxBzs := suite.getAllAuctionTxs()
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 11)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTxs[0])
|
||||
suite.Require().NoError(err)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
auctionTxBzs[0],
|
||||
}, bidInfo.Transactions...)
|
||||
|
||||
proposal = append(proposal, suite.exportMempool()...)
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_ACCEPT,
|
||||
},
|
||||
{
|
||||
"auction tx with front-running",
|
||||
func() [][]byte {
|
||||
numNormalTxs = 100
|
||||
numAuctionTxs = 0
|
||||
numBundledTxs = 0
|
||||
insertRefTxs = false
|
||||
|
||||
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
|
||||
|
||||
topAuctionTx, err := testutils.CreateAuctionTxWithSigners(
|
||||
suite.encodingConfig.TxConfig,
|
||||
suite.accounts[0],
|
||||
sdk.NewCoin("foo", sdk.NewInt(1000000)),
|
||||
0,
|
||||
1,
|
||||
[]testutils.Account{suite.accounts[0], suite.accounts[1]}, // front-running
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 3)
|
||||
|
||||
proposal := append([][]byte{
|
||||
auctionInfo,
|
||||
txBz,
|
||||
}, bidInfo.Transactions...)
|
||||
|
||||
proposal = append(proposal, suite.exportMempool()...)
|
||||
|
||||
return proposal
|
||||
},
|
||||
comettypes.ResponseProcessProposal_REJECT,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create a new auction
|
||||
params := buildertypes.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
FrontRunningProtection: true,
|
||||
MinBidIncrement: suite.minBidIncrement,
|
||||
}
|
||||
suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
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())
|
||||
|
||||
handler := suite.proposalHandler.ProcessProposalHandler()
|
||||
res := handler(suite.ctx, comettypes.RequestProcessProposal{
|
||||
Txs: tc.createTxs(),
|
||||
})
|
||||
|
||||
// Check if the response is valid
|
||||
suite.Require().Equal(tc.response, res.Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,34 @@
|
||||
package v2
|
||||
package abci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
)
|
||||
|
||||
type (
|
||||
// VoteExtensionMempool contains the methods required by the VoteExtensionHandler
|
||||
// to interact with the local mempool.
|
||||
VoteExtensionMempool interface {
|
||||
Remove(tx sdk.Tx) error
|
||||
AuctionBidSelect(ctx context.Context) sdkmempool.Iterator
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
|
||||
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
|
||||
// TOBLaneVE contains the methods required by the VoteExtensionHandler
|
||||
// to interact with the local mempool i.e. the top of block lane.
|
||||
TOBLaneVE interface {
|
||||
sdkmempool.Mempool
|
||||
|
||||
// Factory defines the API/functionality which is responsible for determining
|
||||
// if a transaction is a bid transaction and how to extract relevant
|
||||
// information from the transaction (bid, timeout, bidder, etc.).
|
||||
auction.Factory
|
||||
|
||||
// VerifyTx is utilized to verify a bid transaction according to the preferences
|
||||
// of the top of block lane.
|
||||
VerifyTx(ctx sdk.Context, tx sdk.Tx) error
|
||||
}
|
||||
|
||||
// VoteExtensionHandler contains the functionality and handlers required to
|
||||
// process, validate and build vote extensions.
|
||||
VoteExtensionHandler struct {
|
||||
mempool VoteExtensionMempool
|
||||
tobLane TOBLaneVE
|
||||
|
||||
// txDecoder is used to decode the top bidding auction transaction
|
||||
txDecoder sdk.TxDecoder
|
||||
@ -32,9 +36,6 @@ type (
|
||||
// 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
|
||||
@ -46,14 +47,11 @@ type (
|
||||
|
||||
// NewVoteExtensionHandler returns an VoteExtensionHandler that contains the functionality and handlers
|
||||
// required to inject, process, and validate vote extensions.
|
||||
func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder,
|
||||
txEncoder sdk.TxEncoder, ah sdk.AnteHandler,
|
||||
) *VoteExtensionHandler {
|
||||
func NewVoteExtensionHandler(lane TOBLaneVE, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder) *VoteExtensionHandler {
|
||||
return &VoteExtensionHandler{
|
||||
mempool: mp,
|
||||
tobLane: lane,
|
||||
txDecoder: txDecoder,
|
||||
txEncoder: txEncoder,
|
||||
anteHandler: ah,
|
||||
cache: make(map[string]error),
|
||||
currentHeight: 0,
|
||||
}
|
||||
@ -65,15 +63,17 @@ func NewVoteExtensionHandler(mp VoteExtensionMempool, txDecoder sdk.TxDecoder,
|
||||
func (h *VoteExtensionHandler) ExtendVoteHandler() ExtendVoteHandler {
|
||||
return func(ctx sdk.Context, req *RequestExtendVote) (*ResponseExtendVote, error) {
|
||||
// Iterate through auction bids until we find a valid one
|
||||
auctionIterator := h.mempool.AuctionBidSelect(ctx)
|
||||
auctionIterator := h.tobLane.Select(ctx, nil)
|
||||
|
||||
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 {
|
||||
// Validate the auction transaction against a cache state
|
||||
cacheCtx, _ := ctx.CacheContext()
|
||||
|
||||
if err := h.tobLane.VerifyTx(cacheCtx, bidTx); err == nil {
|
||||
return &ResponseExtendVote{VoteExtension: bidBz}, nil
|
||||
}
|
||||
}
|
||||
@ -116,7 +116,7 @@ func (h *VoteExtensionHandler) VerifyVoteExtensionHandler() VerifyVoteExtensionH
|
||||
}
|
||||
|
||||
// Verify the auction transaction and cache the result
|
||||
if err = h.verifyAuctionTx(ctx, bidTx); err != nil {
|
||||
if err = h.tobLane.VerifyTx(ctx, bidTx); err != nil {
|
||||
h.cache[hash] = err
|
||||
return &ResponseVerifyVoteExtension{Status: ResponseVerifyVoteExtension_REJECT}, err
|
||||
}
|
||||
@ -136,40 +136,3 @@ func (h *VoteExtensionHandler) resetCache(blockHeight int64) {
|
||||
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
|
||||
bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bidInfo == nil {
|
||||
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
|
||||
}
|
||||
|
||||
// Verify all bundled transactions
|
||||
for _, tx := range bidInfo.Transactions {
|
||||
wrappedTx, err := h.mempool.WrapBundleTransaction(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := h.anteHandler(cache, wrappedTx, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
package v2_test
|
||||
package abci_test
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
v2 "github.com/skip-mev/pob/abci/v2"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/abci"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
@ -13,12 +12,8 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
MaxBundleSize: 5,
|
||||
ReserveFee: 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
|
||||
@ -26,21 +21,20 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
{
|
||||
"empty mempool",
|
||||
func() []byte {
|
||||
suite.createFilledMempool(0, 0, 0, false)
|
||||
return []byte{}
|
||||
},
|
||||
},
|
||||
{
|
||||
"filled mempool with no auction transactions",
|
||||
func() []byte {
|
||||
suite.createFilledMempool(100, 0, 0, false)
|
||||
suite.fillBaseLane(10)
|
||||
return []byte{}
|
||||
},
|
||||
},
|
||||
{
|
||||
"mempool with invalid auction transaction (too many bundled transactions)",
|
||||
func() []byte {
|
||||
suite.createFilledMempool(0, 1, int(params.MaxBundleSize)+1, true)
|
||||
suite.fillTOBLane(3, int(params.MaxBundleSize)+1)
|
||||
return []byte{}
|
||||
},
|
||||
},
|
||||
@ -55,9 +49,7 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
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)
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx))
|
||||
|
||||
// this should return nothing since the top bid is not valid
|
||||
return []byte{}
|
||||
@ -66,14 +58,12 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
{
|
||||
"mempool contains only invalid auction bids (bid is too low)",
|
||||
func() []byte {
|
||||
params.ReserveFee = suite.auctionBidAmount
|
||||
params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10000000000000000))
|
||||
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)
|
||||
suite.fillTOBLane(4, 1)
|
||||
|
||||
return []byte{}
|
||||
},
|
||||
@ -88,10 +78,7 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
|
||||
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)
|
||||
suite.Require().NoError(suite.tobLane.Insert(suite.ctx, bidTx))
|
||||
|
||||
// this should return nothing since the top bid is not valid
|
||||
return []byte{}
|
||||
@ -100,26 +87,26 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
{
|
||||
"top bid is invalid but next best is valid",
|
||||
func() []byte {
|
||||
params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(100))
|
||||
err := suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
suite.Require().NoError(err)
|
||||
params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10))
|
||||
|
||||
bidder := suite.accounts[0]
|
||||
bid := suite.auctionBidAmount.Add(suite.minBidIncrement)
|
||||
bid := params.ReserveFee.Add(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.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx))
|
||||
|
||||
suite.createFilledMempool(100, 100, 2, true)
|
||||
|
||||
topBidTx := suite.mempool.GetTopAuctionTx(suite.ctx)
|
||||
|
||||
err = suite.mempool.Insert(suite.ctx, bidTx)
|
||||
bidder = suite.accounts[1]
|
||||
bid = params.ReserveFee
|
||||
signers = []testutils.Account{bidder}
|
||||
timeout = 100
|
||||
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx2))
|
||||
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(topBidTx)
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx2)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return bz
|
||||
@ -129,10 +116,14 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
expectedVE := tc.getExpectedVE()
|
||||
|
||||
err := suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Reset the handler with the new mempool
|
||||
suite.voteExtensionHandler = v2.NewVoteExtensionHandler(suite.mempool, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.anteHandler)
|
||||
suite.voteExtensionHandler = abci.NewVoteExtensionHandler(suite.tobLane, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder())
|
||||
|
||||
handler := suite.voteExtensionHandler.ExtendVoteHandler()
|
||||
resp, err := handler(suite.ctx, nil)
|
||||
@ -148,7 +139,6 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
MaxBundleSize: 5,
|
||||
ReserveFee: 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)
|
||||
@ -156,13 +146,13 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
req func() *v2.RequestVerifyVoteExtension
|
||||
req func() *abci.RequestVerifyVoteExtension
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
"invalid vote extension bytes",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: []byte("invalid vote extension"),
|
||||
}
|
||||
},
|
||||
@ -170,8 +160,8 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"empty vote extension bytes",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: []byte{},
|
||||
}
|
||||
},
|
||||
@ -179,8 +169,8 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"nil vote extension bytes",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: nil,
|
||||
}
|
||||
},
|
||||
@ -188,14 +178,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"invalid extension with bid tx with bad timeout",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
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 &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -203,14 +193,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"invalid vote extension with bid tx with bad bid",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
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 &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -218,14 +208,14 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"valid vote extension",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
bidder := suite.accounts[0]
|
||||
bid := params.ReserveFee
|
||||
signers := []testutils.Account{bidder}
|
||||
timeout := 10
|
||||
|
||||
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -233,7 +223,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"invalid vote extension with front running bid tx",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
bidder := suite.accounts[0]
|
||||
bid := params.ReserveFee
|
||||
timeout := 10
|
||||
@ -242,7 +232,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
signers := []testutils.Account{bidder, bundlee}
|
||||
|
||||
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -250,7 +240,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"invalid vote extension with too many bundle txs",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
// disable front running protection
|
||||
params.FrontRunningProtection = false
|
||||
err := suite.builderKeeper.SetParams(suite.ctx, params)
|
||||
@ -262,7 +252,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
timeout := 10
|
||||
|
||||
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -270,7 +260,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"invalid vote extension with a failing bundle tx",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
bidder := suite.accounts[0]
|
||||
bid := params.ReserveFee
|
||||
|
||||
@ -286,7 +276,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -294,7 +284,7 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
},
|
||||
{
|
||||
"valid vote extension + no comparison to local mempool",
|
||||
func() *v2.RequestVerifyVoteExtension {
|
||||
func() *abci.RequestVerifyVoteExtension {
|
||||
bidder := suite.accounts[0]
|
||||
bid := params.ReserveFee
|
||||
signers := []testutils.Account{bidder}
|
||||
@ -303,17 +293,17 @@ func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
|
||||
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)
|
||||
bid = bid.Add(params.ReserveFee)
|
||||
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)
|
||||
tx := suite.tobLane.GetTopAuctionTx(suite.ctx)
|
||||
suite.Require().NotNil(tx)
|
||||
|
||||
return &v2.RequestVerifyVoteExtension{
|
||||
return &abci.RequestVerifyVoteExtension{
|
||||
VoteExtension: bz,
|
||||
}
|
||||
},
|
||||
@ -6,6 +6,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/terminator"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -33,10 +34,22 @@ func NewProposalHandler(logger log.Logger, mempool blockbuster.Mempool) *Proposa
|
||||
// the default lane will not have a boundary on the number of bytes that can be included in the proposal and
|
||||
// will include all valid transactions in the proposal (up to MaxTxBytes).
|
||||
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
|
||||
return func(ctx sdk.Context, req abci.RequestPrepareProposal) (resp abci.ResponsePrepareProposal) {
|
||||
// In the case where there is a panic, we recover here and return an empty proposal.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.logger.Error("failed to prepare proposal", "err", err)
|
||||
resp = abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}
|
||||
}
|
||||
}()
|
||||
|
||||
proposal := h.prepareLanesHandler(ctx, blockbuster.NewProposal(req.MaxTxBytes))
|
||||
|
||||
return abci.ResponsePrepareProposal{Txs: proposal.Txs}
|
||||
resp = abci.ResponsePrepareProposal{
|
||||
Txs: proposal.Txs,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +58,16 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
|
||||
// If a lane's portion of the proposal is invalid, we reject the proposal. After a lane's portion
|
||||
// of the proposal is verified, we pass the remaining transactions to the next lane in the chain.
|
||||
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
|
||||
return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal {
|
||||
return func(ctx sdk.Context, req abci.RequestProcessProposal) (resp abci.ResponseProcessProposal) {
|
||||
// In the case where any of the lanes panic, we recover here and return a reject status.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.logger.Error("failed to process proposal", "err", err)
|
||||
resp = abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
}
|
||||
}()
|
||||
|
||||
// Verify the proposal using the verification logic from each lane.
|
||||
if _, err := h.processLanesHandler(ctx, req.Txs); err != nil {
|
||||
h.logger.Error("failed to validate the proposal", "err", err)
|
||||
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
|
||||
@ -58,6 +80,9 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
|
||||
// ChainPrepareLanes chains together the proposal preparation logic from each lane
|
||||
// into a single function. The first lane in the chain is the first lane to be prepared and
|
||||
// the last lane in the chain is the last lane to be prepared.
|
||||
//
|
||||
// In the case where any of the lanes fail to prepare the partial proposal, the lane that failed
|
||||
// will be skipped and the next lane in the chain will be called to prepare the proposal.
|
||||
func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandler {
|
||||
if len(chain) == 0 {
|
||||
return nil
|
||||
@ -68,8 +93,62 @@ func ChainPrepareLanes(chain ...blockbuster.Lane) blockbuster.PrepareLanesHandle
|
||||
chain = append(chain, terminator.Terminator{})
|
||||
}
|
||||
|
||||
return func(ctx sdk.Context, proposal *blockbuster.Proposal) *blockbuster.Proposal {
|
||||
return chain[0].PrepareLane(ctx, proposal, ChainPrepareLanes(chain[1:]...))
|
||||
return func(ctx sdk.Context, partialProposal *blockbuster.Proposal) (finalProposal *blockbuster.Proposal) {
|
||||
lane := chain[0]
|
||||
lane.Logger().Info("preparing lane", "lane", lane.Name())
|
||||
|
||||
// Cache the context in the case where any of the lanes fail to prepare the proposal.
|
||||
cacheCtx, write := ctx.CacheContext()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
lane.Logger().Error("failed to prepare lane", "lane", lane.Name(), "err", err)
|
||||
|
||||
lanesRemaining := len(chain)
|
||||
switch {
|
||||
case lanesRemaining <= 2:
|
||||
// If there are only two lanes remaining, then the first lane in the chain
|
||||
// is the lane that failed to prepare the partial proposal and the second lane in the
|
||||
// chain is the terminator lane. We return the proposal as is.
|
||||
finalProposal = partialProposal
|
||||
default:
|
||||
// If there are more than two lanes remaining, then the first lane in the chain
|
||||
// is the lane that failed to prepare the proposal but the second lane in the
|
||||
// chain is not the terminator lane so there could potentially be more transactions
|
||||
// added to the proposal
|
||||
maxTxBytesForLane := utils.GetMaxTxBytesForLane(
|
||||
partialProposal,
|
||||
chain[1].GetMaxBlockSpace(),
|
||||
)
|
||||
|
||||
finalProposal = chain[1].PrepareLane(
|
||||
ctx,
|
||||
partialProposal,
|
||||
maxTxBytesForLane,
|
||||
ChainPrepareLanes(chain[2:]...),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Write the cache to the context since we know that the lane successfully prepared
|
||||
// the partial proposal.
|
||||
write()
|
||||
|
||||
lane.Logger().Info("prepared lane", "lane", lane.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the maximum number of bytes that can be included in the proposal for this lane.
|
||||
maxTxBytesForLane := utils.GetMaxTxBytesForLane(
|
||||
partialProposal,
|
||||
lane.GetMaxBlockSpace(),
|
||||
)
|
||||
|
||||
return lane.PrepareLane(
|
||||
cacheCtx,
|
||||
partialProposal,
|
||||
maxTxBytesForLane,
|
||||
ChainPrepareLanes(chain[1:]...),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +166,16 @@ func ChainProcessLanes(chain ...blockbuster.Lane) blockbuster.ProcessLanesHandle
|
||||
}
|
||||
|
||||
return func(ctx sdk.Context, proposalTxs [][]byte) (sdk.Context, error) {
|
||||
// Short circuit if there are no transactions to process.
|
||||
if len(proposalTxs) == 0 {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
chain[0].Logger().Info("processing lane", "lane", chain[0].Name())
|
||||
if err := chain[0].ProcessLaneBasic(proposalTxs); err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
return chain[0].ProcessLane(ctx, proposalTxs, ChainProcessLanes(chain[1:]...))
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
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"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -26,9 +26,9 @@ type (
|
||||
// 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
|
||||
// TOBLane is utilized to retrieve the bid info of a transaction and to
|
||||
// insert a bid transaction into the application-side mempool.
|
||||
tobLane TOBLane
|
||||
|
||||
// anteHandler is utilized to verify the bid transaction against the latest
|
||||
// committed state.
|
||||
@ -42,11 +42,11 @@ type (
|
||||
// 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 {
|
||||
// TOBLane is the interface that defines all of the dependencies that
|
||||
// are required to interact with the top of block lane.
|
||||
TOBLane interface {
|
||||
// GetAuctionBidInfo is utilized to retrieve the bid info of a transaction.
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error)
|
||||
|
||||
// Insert is utilized to insert a transaction into the application-side mempool.
|
||||
Insert(ctx context.Context, tx sdk.Tx) error
|
||||
@ -78,11 +78,17 @@ type (
|
||||
)
|
||||
|
||||
// NewCheckTxHandler is a constructor for CheckTxHandler.
|
||||
func NewCheckTxHandler(baseApp BaseApp, txDecoder sdk.TxDecoder, mempool CheckTxMempool, anteHandler sdk.AnteHandler, chainID string) *CheckTxHandler {
|
||||
func NewCheckTxHandler(
|
||||
baseApp BaseApp,
|
||||
txDecoder sdk.TxDecoder,
|
||||
tobLane TOBLane,
|
||||
anteHandler sdk.AnteHandler,
|
||||
chainID string,
|
||||
) *CheckTxHandler {
|
||||
return &CheckTxHandler{
|
||||
baseApp: baseApp,
|
||||
txDecoder: txDecoder,
|
||||
mempool: mempool,
|
||||
tobLane: tobLane,
|
||||
anteHandler: anteHandler,
|
||||
chainID: chainID,
|
||||
}
|
||||
@ -108,7 +114,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx {
|
||||
}
|
||||
|
||||
// Attempt to get the bid info of the transaction.
|
||||
bidInfo, err := handler.mempool.GetAuctionBidInfo(tx)
|
||||
bidInfo, err := handler.tobLane.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to get auction bid info: %w", err), 0, 0, nil, false)
|
||||
}
|
||||
@ -130,7 +136,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := handler.tobLane.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)
|
||||
}
|
||||
|
||||
@ -143,7 +149,7 @@ func (handler *CheckTxHandler) CheckTx() CheckTx {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *types.BidInfo) (sdk.GasInfo, error) {
|
||||
// Verify the bid transaction.
|
||||
ctx, err := handler.anteHandler(ctx, bidTx, false)
|
||||
if err != nil {
|
||||
@ -158,13 +164,13 @@ func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidI
|
||||
|
||||
// Verify all of the bundled transactions.
|
||||
for _, tx := range bidInfo.Transactions {
|
||||
bundledTx, err := handler.mempool.WrapBundleTransaction(tx)
|
||||
bundledTx, err := handler.tobLane.WrapBundleTransaction(tx)
|
||||
if err != nil {
|
||||
return gasInfo, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err)
|
||||
}
|
||||
|
||||
// bid txs cannot be included in bundled txs
|
||||
bidInfo, _ := handler.mempool.GetAuctionBidInfo(bundledTx)
|
||||
bidInfo, _ := handler.tobLane.GetAuctionBidInfo(bundledTx)
|
||||
if bidInfo != nil {
|
||||
return gasInfo, fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx")
|
||||
}
|
||||
@ -3,6 +3,7 @@ package blockbuster
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
@ -15,8 +16,8 @@ type (
|
||||
// Txs is the list of transactions in the proposal.
|
||||
Txs [][]byte
|
||||
|
||||
// SelectedTxs is a cache of the selected transactions in the proposal.
|
||||
SelectedTxs map[string]struct{}
|
||||
// Cache is a cache of the selected transactions in the proposal.
|
||||
Cache map[string]struct{}
|
||||
|
||||
// TotalTxBytes is the total number of bytes currently included in the proposal.
|
||||
TotalTxBytes int64
|
||||
@ -65,14 +66,29 @@ type (
|
||||
// Contains returns true if the mempool contains the given transaction.
|
||||
Contains(tx sdk.Tx) (bool, error)
|
||||
|
||||
// PrepareLane which builds a portion of the block. Inputs include the max
|
||||
// number of bytes that can be included in the block and the selected transactions
|
||||
// thus from from previous lane(s) as mapping from their HEX-encoded hash to
|
||||
// the raw transaction.
|
||||
PrepareLane(ctx sdk.Context, proposal *Proposal, next PrepareLanesHandler) *Proposal
|
||||
// PrepareLane builds a portion of the block. It inputs the maxTxBytes that can be
|
||||
// included in the proposal for the given lane, the partial proposal, and a function
|
||||
// to call the next lane in the chain. The next lane in the chain will be called with
|
||||
// the updated proposal and context.
|
||||
PrepareLane(ctx sdk.Context, proposal *Proposal, maxTxBytes int64, next PrepareLanesHandler) *Proposal
|
||||
|
||||
// ProcessLane verifies this lane's portion of a proposed block.
|
||||
// ProcessLaneBasic validates that transactions belonging to this lane are not misplaced
|
||||
// in the block proposal.
|
||||
ProcessLaneBasic(txs [][]byte) error
|
||||
|
||||
// ProcessLane verifies this lane's portion of a proposed block. It inputs the transactions
|
||||
// that may belong to this lane and a function to call the next lane in the chain. The next
|
||||
// lane in the chain will be called with the updated context and filtered down transactions.
|
||||
ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next ProcessLanesHandler) (sdk.Context, error)
|
||||
|
||||
// SetAnteHandler sets the lane's antehandler.
|
||||
SetAnteHandler(antehander sdk.AnteHandler)
|
||||
|
||||
// Logger returns the lane's logger.
|
||||
Logger() log.Logger
|
||||
|
||||
// GetMaxBlockSpace returns the max block space for the lane as a relative percentage.
|
||||
GetMaxBlockSpace() sdk.Dec
|
||||
}
|
||||
)
|
||||
|
||||
@ -87,11 +103,33 @@ func NewBaseLaneConfig(logger log.Logger, txEncoder sdk.TxEncoder, txDecoder sdk
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBasic validates the lane configuration.
|
||||
func (c *BaseLaneConfig) ValidateBasic() error {
|
||||
if c.Logger == nil {
|
||||
return fmt.Errorf("logger cannot be nil")
|
||||
}
|
||||
|
||||
if c.TxEncoder == nil {
|
||||
return fmt.Errorf("tx encoder cannot be nil")
|
||||
}
|
||||
|
||||
if c.TxDecoder == nil {
|
||||
return fmt.Errorf("tx decoder cannot be nil")
|
||||
}
|
||||
|
||||
if c.MaxBlockSpace.IsNil() || c.MaxBlockSpace.IsNegative() || c.MaxBlockSpace.GT(sdk.OneDec()) {
|
||||
return fmt.Errorf("max block space must be set to a value between 0 and 1")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewProposal returns a new empty proposal.
|
||||
func NewProposal(maxTxBytes int64) *Proposal {
|
||||
return &Proposal{
|
||||
Txs: make([][]byte, 0),
|
||||
SelectedTxs: make(map[string]struct{}),
|
||||
MaxTxBytes: maxTxBytes,
|
||||
Txs: make([][]byte, 0),
|
||||
Cache: make(map[string]struct{}),
|
||||
MaxTxBytes: maxTxBytes,
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +142,7 @@ func (p *Proposal) UpdateProposal(txs [][]byte, totalSize int64) *Proposal {
|
||||
txHash := sha256.Sum256(tx)
|
||||
txHashStr := hex.EncodeToString(txHash[:])
|
||||
|
||||
p.SelectedTxs[txHashStr] = struct{}{}
|
||||
p.Cache[txHashStr] = struct{}{}
|
||||
}
|
||||
|
||||
return p
|
||||
|
||||
@ -2,17 +2,22 @@ package auction
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
// PrepareLane will attempt to select the highest bid transaction that is valid
|
||||
// and whose bundled transactions are valid and include them in the proposal. It
|
||||
// will return an empty partial proposal if no valid bids are found.
|
||||
func (l *TOBLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposal, next blockbuster.PrepareLanesHandler) *blockbuster.Proposal {
|
||||
func (l *TOBLane) PrepareLane(
|
||||
ctx sdk.Context,
|
||||
proposal *blockbuster.Proposal,
|
||||
maxTxBytes int64,
|
||||
next blockbuster.PrepareLanesHandler,
|
||||
) *blockbuster.Proposal {
|
||||
// Define all of the info we need to select transactions for the partial proposal.
|
||||
var (
|
||||
totalSize int64
|
||||
@ -20,10 +25,6 @@ func (l *TOBLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposal, n
|
||||
txsToRemove = make(map[sdk.Tx]struct{}, 0)
|
||||
)
|
||||
|
||||
// Calculate the max tx bytes for the lane and track the total size of the
|
||||
// transactions we have selected so far.
|
||||
maxTxBytes := blockbuster.GetMaxTxBytesForLane(proposal, l.cfg.MaxBlockSpace)
|
||||
|
||||
// Attempt to select the highest bid transaction that is valid and whose
|
||||
// bundled transactions are valid.
|
||||
bidTxIterator := l.Select(ctx, nil)
|
||||
@ -32,19 +33,14 @@ selectBidTxLoop:
|
||||
cacheCtx, write := ctx.CacheContext()
|
||||
tmpBidTx := bidTxIterator.Tx()
|
||||
|
||||
// if the transaction is already in the (partial) block proposal, we skip it.
|
||||
txHash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, tmpBidTx)
|
||||
bidTxBz, txHash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tmpBidTx)
|
||||
if err != nil {
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue
|
||||
}
|
||||
if _, ok := proposal.SelectedTxs[txHash]; ok {
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
bidTxBz, err := l.cfg.TxEncoder(tmpBidTx)
|
||||
if err != nil {
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
// if the transaction is already in the (partial) block proposal, we skip it.
|
||||
if _, ok := proposal.Cache[txHash]; ok {
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
@ -75,19 +71,14 @@ selectBidTxLoop:
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
sdkTxBz, err := l.cfg.TxEncoder(sdkTx)
|
||||
sdkTxBz, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, sdkTx)
|
||||
if err != nil {
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
// if the transaction is already in the (partial) block proposal, we skip it.
|
||||
hash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, sdkTx)
|
||||
if err != nil {
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
if _, ok := proposal.SelectedTxs[hash]; ok {
|
||||
if _, ok := proposal.Cache[hash]; ok {
|
||||
continue selectBidTxLoop
|
||||
}
|
||||
|
||||
@ -112,7 +103,7 @@ selectBidTxLoop:
|
||||
}
|
||||
|
||||
txsToRemove[tmpBidTx] = struct{}{}
|
||||
l.cfg.Logger.Info(
|
||||
l.Cfg.Logger.Info(
|
||||
"failed to select auction bid tx; tx size is too large",
|
||||
"tx_size", bidTxSize,
|
||||
"max_size", proposal.MaxTxBytes,
|
||||
@ -120,8 +111,8 @@ selectBidTxLoop:
|
||||
}
|
||||
|
||||
// Remove all transactions that were invalid during the creation of the partial proposal.
|
||||
if err := blockbuster.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil {
|
||||
l.cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err)
|
||||
if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil {
|
||||
l.Cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err)
|
||||
return proposal
|
||||
}
|
||||
|
||||
@ -132,68 +123,99 @@ selectBidTxLoop:
|
||||
}
|
||||
|
||||
// ProcessLane will ensure that block proposals that include transactions from
|
||||
// the top-of-block auction lane are valid. It will return an error if the
|
||||
// block proposal is invalid. The block proposal is invalid if it does not
|
||||
// respect the ordering of transactions in the bid transaction or if the bid/bundled
|
||||
// transactions are invalid.
|
||||
// the top-of-block auction lane are valid.
|
||||
func (l *TOBLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next blockbuster.ProcessLanesHandler) (sdk.Context, error) {
|
||||
// Track the index of the first transaction that does not belong to this lane.
|
||||
endIndex := 0
|
||||
tx, err := l.Cfg.TxDecoder(proposalTxs[0])
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
for index, txBz := range proposalTxs {
|
||||
tx, err := l.cfg.TxDecoder(txBz)
|
||||
if !l.Match(tx) {
|
||||
return next(ctx, proposalTxs)
|
||||
}
|
||||
|
||||
bidInfo, err := l.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if err := l.VerifyTx(ctx, tx); err != nil {
|
||||
return ctx, fmt.Errorf("invalid bid tx: %w", err)
|
||||
}
|
||||
|
||||
return next(ctx, proposalTxs[len(bidInfo.Transactions)+1:])
|
||||
}
|
||||
|
||||
// ProcessLaneBasic ensures that if a bid transaction is present in a proposal,
|
||||
// - it is the first transaction in the partial proposal
|
||||
// - all of the bundled transactions are included after the bid transaction in the order
|
||||
// they were included in the bid transaction.
|
||||
// - there are no other bid transactions in the proposal
|
||||
func (l *TOBLane) ProcessLaneBasic(txs [][]byte) error {
|
||||
tx, err := l.Cfg.TxDecoder(txs[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
// If there is a bid transaction, it must be the first transaction in the block proposal.
|
||||
if !l.Match(tx) {
|
||||
for _, txBz := range txs[1:] {
|
||||
tx, err := l.Cfg.TxDecoder(txBz)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if l.Match(tx) {
|
||||
return fmt.Errorf("misplaced bid transactions in lane %s", l.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bidInfo, err := l.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if len(txs) < len(bidInfo.Transactions)+1 {
|
||||
return fmt.Errorf("invalid number of transactions in lane %s; expected at least %d, got %d", l.Name(), len(bidInfo.Transactions)+1, len(txs))
|
||||
}
|
||||
|
||||
// Ensure that the order of transactions in the bundle is preserved.
|
||||
for i, bundleTxBz := range txs[1 : len(bidInfo.Transactions)+1] {
|
||||
tx, err := l.WrapBundleTransaction(bundleTxBz)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
return fmt.Errorf("failed to decode bundled tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if l.Match(tx) {
|
||||
// If the transaction is an auction bid, then we need to ensure that it is
|
||||
// the first transaction in the block proposal and that the order of
|
||||
// transactions in the block proposal follows the order of transactions in
|
||||
// the bid.
|
||||
if index != 0 {
|
||||
return ctx, fmt.Errorf("block proposal did not place auction bid transaction at the top of the lane: %d", index)
|
||||
}
|
||||
return fmt.Errorf("multiple bid transactions in lane %s", l.Name())
|
||||
}
|
||||
|
||||
bidInfo, err := l.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to get auction bid info for tx at index %w", err)
|
||||
}
|
||||
txBz, err := l.Cfg.TxEncoder(tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if bidInfo != nil {
|
||||
if len(proposalTxs) < len(bidInfo.Transactions)+1 {
|
||||
return ctx, errors.New("block proposal does not contain enough transactions to match the bundled transactions in the auction bid")
|
||||
}
|
||||
|
||||
for i, refTxRaw := range bidInfo.Transactions {
|
||||
// Wrap and then encode the bundled transaction to ensure that the underlying
|
||||
// reference transaction can be processed as an sdk.Tx.
|
||||
wrappedTx, err := l.WrapBundleTransaction(refTxRaw)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
refTxBz, err := l.cfg.TxEncoder(wrappedTx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(refTxBz, proposalTxs[i+1]) {
|
||||
return ctx, errors.New("block proposal does not match the bundled transactions in the auction bid")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the bid transaction.
|
||||
if err = l.VerifyTx(ctx, tx); err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
endIndex = len(bidInfo.Transactions) + 1
|
||||
}
|
||||
if !bytes.Equal(txBz, bidInfo.Transactions[i]) {
|
||||
return fmt.Errorf("invalid order of transactions in lane %s", l.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx, proposalTxs[endIndex:])
|
||||
// Ensure that there are no more bid transactions in the block proposal.
|
||||
for _, txBz := range txs[len(bidInfo.Transactions)+1:] {
|
||||
tx, err := l.Cfg.TxDecoder(txBz)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if l.Match(tx) {
|
||||
return fmt.Errorf("multiple bid transactions in lane %s", l.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTx will verify that the bid transaction and all of its bundled
|
||||
@ -235,8 +257,8 @@ func (l *TOBLane) VerifyTx(ctx sdk.Context, bidTx sdk.Tx) error {
|
||||
// verifyTx will execute the ante handler on the transaction and return the
|
||||
// resulting context and error.
|
||||
func (l *TOBLane) verifyTx(ctx sdk.Context, tx sdk.Tx) (sdk.Context, error) {
|
||||
if l.cfg.AnteHandler != nil {
|
||||
newCtx, err := l.cfg.AnteHandler(ctx, tx, false)
|
||||
if l.Cfg.AnteHandler != nil {
|
||||
newCtx, err := l.Cfg.AnteHandler(ctx, tx, false)
|
||||
return newCtx, err
|
||||
}
|
||||
|
||||
|
||||
47
blockbuster/lanes/auction/auction_test.go
Normal file
47
blockbuster/lanes/auction/auction_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package auction_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
encCfg testutils.EncodingConfig
|
||||
config auction.Factory
|
||||
mempool auction.Mempool
|
||||
ctx sdk.Context
|
||||
random *rand.Rand
|
||||
accounts []testutils.Account
|
||||
nonces map[string]uint64
|
||||
}
|
||||
|
||||
func TestMempoolTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) SetupTest() {
|
||||
// Mempool setup
|
||||
suite.encCfg = testutils.CreateTestEncodingConfig()
|
||||
suite.config = auction.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder())
|
||||
suite.mempool = auction.NewMempool(suite.encCfg.TxConfig.TxEncoder(), 0, suite.config)
|
||||
suite.ctx = sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
|
||||
|
||||
// Init accounts
|
||||
suite.random = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
suite.accounts = testutils.RandomAccounts(suite.random, 10)
|
||||
|
||||
suite.nonces = make(map[string]uint64)
|
||||
for _, acc := range suite.accounts {
|
||||
suite.nonces[acc.Address.String()] = 0
|
||||
}
|
||||
}
|
||||
@ -5,18 +5,10 @@ import (
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// BidInfo defines the information about a bid to the auction house.
|
||||
BidInfo struct {
|
||||
Bidder sdk.AccAddress
|
||||
Bid sdk.Coin
|
||||
Transactions [][]byte
|
||||
Timeout uint64
|
||||
Signers []map[string]struct{}
|
||||
}
|
||||
|
||||
// Factory defines the interface for processing auction transactions. It is
|
||||
// a wrapper around all of the functionality that each application chain must implement
|
||||
// in order for auction processing to work.
|
||||
@ -28,7 +20,7 @@ type (
|
||||
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
|
||||
|
||||
// GetAuctionBidInfo defines a function that returns the bid info from an auction transaction.
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, error)
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error)
|
||||
}
|
||||
|
||||
// DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions.
|
||||
@ -65,7 +57,7 @@ func (config *DefaultAuctionFactory) WrapBundleTransaction(tx []byte) (sdk.Tx, e
|
||||
// GetAuctionBidInfo defines a default function that returns the auction bid info from
|
||||
// an auction transaction. In the default case, the auction bid info is stored in the
|
||||
// MsgAuctionBid message.
|
||||
func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, error) {
|
||||
func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error) {
|
||||
msg, err := GetMsgAuctionBidFromTx(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -90,7 +82,7 @@ func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*BidInfo, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BidInfo{
|
||||
return &types.BidInfo{
|
||||
Bid: msg.Bid,
|
||||
Bidder: bidder,
|
||||
Transactions: msg.Transactions,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package mempool_test
|
||||
package auction_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@ -1,17 +1,20 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/base"
|
||||
)
|
||||
|
||||
const (
|
||||
// LaneName defines the name of the top-of-block auction lane.
|
||||
LaneName = "tob"
|
||||
LaneName = "top-of-block"
|
||||
)
|
||||
|
||||
var _ blockbuster.Lane = (*TOBLane)(nil)
|
||||
var (
|
||||
_ blockbuster.Lane = (*TOBLane)(nil)
|
||||
_ Factory = (*TOBLane)(nil)
|
||||
)
|
||||
|
||||
// TOBLane defines a top-of-block auction lane. The top of block auction lane
|
||||
// hosts transactions that want to bid for inclusion at the top of the next block.
|
||||
@ -24,7 +27,7 @@ type TOBLane struct {
|
||||
Mempool
|
||||
|
||||
// LaneConfig defines the base lane configuration.
|
||||
cfg blockbuster.BaseLaneConfig
|
||||
*base.DefaultLane
|
||||
|
||||
// Factory defines the API/functionality which is responsible for determining
|
||||
// if a transaction is a bid transaction and how to extract relevant
|
||||
@ -34,18 +37,18 @@ type TOBLane struct {
|
||||
|
||||
// NewTOBLane returns a new TOB lane.
|
||||
func NewTOBLane(
|
||||
logger log.Logger,
|
||||
txDecoder sdk.TxDecoder,
|
||||
txEncoder sdk.TxEncoder,
|
||||
cfg blockbuster.BaseLaneConfig,
|
||||
maxTx int,
|
||||
anteHandler sdk.AnteHandler,
|
||||
af Factory,
|
||||
maxBlockSpace sdk.Dec,
|
||||
) *TOBLane {
|
||||
if err := cfg.ValidateBasic(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TOBLane{
|
||||
Mempool: NewMempool(txEncoder, maxTx, af),
|
||||
cfg: blockbuster.NewBaseLaneConfig(logger, txEncoder, txDecoder, anteHandler, maxBlockSpace),
|
||||
Factory: af,
|
||||
Mempool: NewMempool(cfg.TxEncoder, maxTx, af),
|
||||
DefaultLane: base.NewDefaultLane(cfg),
|
||||
Factory: af,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
var _ Mempool = (*TOBMempool)(nil)
|
||||
@ -48,8 +48,8 @@ type (
|
||||
|
||||
// TxPriority returns a TxPriority over auction bid transactions only. It
|
||||
// is to be used in the auction index only.
|
||||
func TxPriority(config Factory) mempool.TxPriority[string] {
|
||||
return mempool.TxPriority[string]{
|
||||
func TxPriority(config Factory) blockbuster.TxPriority[string] {
|
||||
return blockbuster.TxPriority[string]{
|
||||
GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string {
|
||||
bidInfo, err := config.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
@ -92,8 +92,8 @@ func TxPriority(config Factory) mempool.TxPriority[string] {
|
||||
// NewMempool returns a new auction mempool.
|
||||
func NewMempool(txEncoder sdk.TxEncoder, maxTx int, config Factory) *TOBMempool {
|
||||
return &TOBMempool{
|
||||
index: mempool.NewPriorityMempool(
|
||||
mempool.PriorityNonceMempoolConfig[string]{
|
||||
index: blockbuster.NewPriorityMempool(
|
||||
blockbuster.PriorityNonceMempoolConfig[string]{
|
||||
TxPriority: TxPriority(config),
|
||||
MaxTx: maxTx,
|
||||
},
|
||||
@ -120,7 +120,7 @@ func (am *TOBMempool) Insert(ctx context.Context, tx sdk.Tx) error {
|
||||
return fmt.Errorf("failed to insert tx into auction index: %w", err)
|
||||
}
|
||||
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -167,7 +167,7 @@ func (am *TOBMempool) CountTx() int {
|
||||
|
||||
// Contains returns true if the transaction is contained in the mempool.
|
||||
func (am *TOBMempool) Contains(tx sdk.Tx) (bool, error) {
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get tx hash string: %w", err)
|
||||
}
|
||||
@ -181,7 +181,7 @@ func (am *TOBMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get tx hash string: %w", err))
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package mempool_test
|
||||
package auction_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
pobcodec "github.com/skip-mev/pob/codec"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -16,7 +16,7 @@ func TestGetMsgAuctionBidFromTx_Valid(t *testing.T) {
|
||||
txBuilder := encCfg.TxConfig.NewTxBuilder()
|
||||
txBuilder.SetMsgs(&buildertypes.MsgAuctionBid{})
|
||||
|
||||
msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, msg)
|
||||
}
|
||||
@ -31,7 +31,7 @@ func TestGetMsgAuctionBidFromTx_MultiMsgBid(t *testing.T) {
|
||||
&banktypes.MsgSend{},
|
||||
)
|
||||
|
||||
msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, msg)
|
||||
}
|
||||
@ -42,7 +42,7 @@ func TestGetMsgAuctionBidFromTx_NoBid(t *testing.T) {
|
||||
txBuilder := encCfg.TxConfig.NewTxBuilder()
|
||||
txBuilder.SetMsgs(&banktypes.MsgSend{})
|
||||
|
||||
msg, err := mempool.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
msg, err := auction.GetMsgAuctionBidFromTx(txBuilder.GetTx())
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, msg)
|
||||
}
|
||||
@ -5,37 +5,36 @@ import (
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
// PrepareLane will prepare a partial proposal for the base lane.
|
||||
func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposal, next blockbuster.PrepareLanesHandler) *blockbuster.Proposal {
|
||||
func (l *DefaultLane) PrepareLane(
|
||||
ctx sdk.Context,
|
||||
proposal *blockbuster.Proposal,
|
||||
maxTxBytes int64,
|
||||
next blockbuster.PrepareLanesHandler,
|
||||
) *blockbuster.Proposal {
|
||||
// Define all of the info we need to select transactions for the partial proposal.
|
||||
txs := make([][]byte, 0)
|
||||
txsToRemove := make(map[sdk.Tx]struct{}, 0)
|
||||
totalSize := int64(0)
|
||||
|
||||
// Calculate the max tx bytes for the lane and track the total size of the
|
||||
// transactions we have selected so far.
|
||||
maxTxBytes := blockbuster.GetMaxTxBytesForLane(proposal, l.cfg.MaxBlockSpace)
|
||||
var (
|
||||
totalSize int64
|
||||
txs [][]byte
|
||||
txsToRemove = make(map[sdk.Tx]struct{}, 0)
|
||||
)
|
||||
|
||||
// Select all transactions in the mempool that are valid and not already in the
|
||||
// partial proposal.
|
||||
for iterator := l.Mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
|
||||
tx := iterator.Tx()
|
||||
|
||||
txBytes, err := l.cfg.TxEncoder(tx)
|
||||
txBytes, hash, err := utils.GetTxHashStr(l.Cfg.TxEncoder, tx)
|
||||
if err != nil {
|
||||
txsToRemove[tx] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
// if the transaction is already in the (partial) block proposal, we skip it.
|
||||
hash, err := blockbuster.GetTxHashStr(l.cfg.TxEncoder, tx)
|
||||
if err != nil {
|
||||
txsToRemove[tx] = struct{}{}
|
||||
continue
|
||||
}
|
||||
if _, ok := proposal.SelectedTxs[hash]; ok {
|
||||
if _, ok := proposal.Cache[hash]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -56,8 +55,8 @@ func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposa
|
||||
}
|
||||
|
||||
// Remove all transactions that were invalid during the creation of the partial proposal.
|
||||
if err := blockbuster.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil {
|
||||
l.cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err)
|
||||
if err := utils.RemoveTxsFromLane(txsToRemove, l.Mempool); err != nil {
|
||||
l.Cfg.Logger.Error("failed to remove txs from mempool", "lane", l.Name(), "err", err)
|
||||
return proposal
|
||||
}
|
||||
|
||||
@ -69,7 +68,7 @@ func (l *DefaultLane) PrepareLane(ctx sdk.Context, proposal *blockbuster.Proposa
|
||||
// ProcessLane verifies the default lane's portion of a block proposal.
|
||||
func (l *DefaultLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next blockbuster.ProcessLanesHandler) (sdk.Context, error) {
|
||||
for index, tx := range proposalTxs {
|
||||
tx, err := l.cfg.TxDecoder(tx)
|
||||
tx, err := l.Cfg.TxDecoder(tx)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
@ -87,10 +86,37 @@ func (l *DefaultLane) ProcessLane(ctx sdk.Context, proposalTxs [][]byte, next bl
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// ProcessLaneBasic does basic validation on the block proposal to ensure that
|
||||
// transactions that belong to this lane are not misplaced in the block proposal.
|
||||
func (l *DefaultLane) ProcessLaneBasic(txs [][]byte) error {
|
||||
seenOtherLaneTx := false
|
||||
lastSeenIndex := 0
|
||||
|
||||
for _, txBz := range txs {
|
||||
tx, err := l.Cfg.TxDecoder(txBz)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode tx in lane %s: %w", l.Name(), err)
|
||||
}
|
||||
|
||||
if l.Match(tx) {
|
||||
if seenOtherLaneTx {
|
||||
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
|
||||
}
|
||||
|
||||
lastSeenIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
seenOtherLaneTx = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTx does basic verification of the transaction using the ante handler.
|
||||
func (l *DefaultLane) VerifyTx(ctx sdk.Context, tx sdk.Tx) error {
|
||||
if l.cfg.AnteHandler != nil {
|
||||
_, err := l.cfg.AnteHandler(ctx, tx, false)
|
||||
if l.Cfg.AnteHandler != nil {
|
||||
_, err := l.Cfg.AnteHandler(ctx, tx, false)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -20,13 +20,18 @@ type DefaultLane struct {
|
||||
Mempool
|
||||
|
||||
// LaneConfig defines the base lane configuration.
|
||||
cfg blockbuster.BaseLaneConfig
|
||||
Cfg blockbuster.BaseLaneConfig
|
||||
}
|
||||
|
||||
func NewDefaultLane(logger log.Logger, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, anteHandler sdk.AnteHandler, maxBlockSpace sdk.Dec) *DefaultLane {
|
||||
// NewDefaultLane returns a new default lane.
|
||||
func NewDefaultLane(cfg blockbuster.BaseLaneConfig) *DefaultLane {
|
||||
if err := cfg.ValidateBasic(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &DefaultLane{
|
||||
Mempool: NewDefaultMempool(txEncoder),
|
||||
cfg: blockbuster.NewBaseLaneConfig(logger, txEncoder, txDecoder, anteHandler, maxBlockSpace),
|
||||
Mempool: NewDefaultMempool(cfg.TxEncoder),
|
||||
Cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,3 +46,18 @@ func (l *DefaultLane) Match(sdk.Tx) bool {
|
||||
func (l *DefaultLane) Name() string {
|
||||
return LaneName
|
||||
}
|
||||
|
||||
// Logger returns the lane's logger.
|
||||
func (l *DefaultLane) Logger() log.Logger {
|
||||
return l.Cfg.Logger
|
||||
}
|
||||
|
||||
// SetAnteHandler sets the lane's antehandler.
|
||||
func (l *DefaultLane) SetAnteHandler(anteHandler sdk.AnteHandler) {
|
||||
l.Cfg.AnteHandler = anteHandler
|
||||
}
|
||||
|
||||
// GetMaxBlockSpace returns the maximum block space for the lane as a relative percentage.
|
||||
func (l *DefaultLane) GetMaxBlockSpace() sdk.Dec {
|
||||
return l.Cfg.MaxBlockSpace
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster/utils"
|
||||
)
|
||||
|
||||
var _ sdkmempool.Mempool = (*DefaultMempool)(nil)
|
||||
@ -42,8 +42,8 @@ type (
|
||||
|
||||
func NewDefaultMempool(txEncoder sdk.TxEncoder) *DefaultMempool {
|
||||
return &DefaultMempool{
|
||||
index: mempool.NewPriorityMempool(
|
||||
mempool.DefaultPriorityNonceMempoolConfig(),
|
||||
index: blockbuster.NewPriorityMempool(
|
||||
blockbuster.DefaultPriorityNonceMempoolConfig(),
|
||||
),
|
||||
txEncoder: txEncoder,
|
||||
txIndex: make(map[string]struct{}),
|
||||
@ -56,7 +56,7 @@ func (am *DefaultMempool) Insert(ctx context.Context, tx sdk.Tx) error {
|
||||
return fmt.Errorf("failed to insert tx into auction index: %w", err)
|
||||
}
|
||||
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -82,7 +82,7 @@ func (am *DefaultMempool) CountTx() int {
|
||||
|
||||
// Contains returns true if the transaction is contained in the mempool.
|
||||
func (am *DefaultMempool) Contains(tx sdk.Tx) (bool, error) {
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get tx hash string: %w", err)
|
||||
}
|
||||
@ -97,7 +97,7 @@ func (am *DefaultMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
|
||||
txHashStr, err := blockbuster.GetTxHashStr(am.txEncoder, tx)
|
||||
_, txHashStr, err := utils.GetTxHashStr(am.txEncoder, tx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get tx hash string: %w", err))
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
@ -34,7 +35,7 @@ type Terminator struct{}
|
||||
var _ blockbuster.Lane = (*Terminator)(nil)
|
||||
|
||||
// PrepareLane is a no-op
|
||||
func (t Terminator) PrepareLane(_ sdk.Context, proposal *blockbuster.Proposal, _ blockbuster.PrepareLanesHandler) *blockbuster.Proposal {
|
||||
func (t Terminator) PrepareLane(_ sdk.Context, proposal *blockbuster.Proposal, _ int64, _ blockbuster.PrepareLanesHandler) *blockbuster.Proposal {
|
||||
return proposal
|
||||
}
|
||||
|
||||
@ -82,3 +83,21 @@ func (t Terminator) Remove(sdk.Tx) error {
|
||||
func (t Terminator) Select(context.Context, [][]byte) sdkmempool.Iterator {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateLaneBasic is a no-op
|
||||
func (t Terminator) ProcessLaneBasic([][]byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLaneConfig is a no-op
|
||||
func (t Terminator) SetAnteHandler(sdk.AnteHandler) {}
|
||||
|
||||
// Logger is a no-op
|
||||
func (t Terminator) Logger() log.Logger {
|
||||
return log.NewNopLogger()
|
||||
}
|
||||
|
||||
// GetMaxBlockSpace is a no-op
|
||||
func (t Terminator) GetMaxBlockSpace() sdk.Dec {
|
||||
return sdk.ZeroDec()
|
||||
}
|
||||
|
||||
@ -19,6 +19,9 @@ type (
|
||||
|
||||
// Contains returns true if the transaction is contained in the mempool.
|
||||
Contains(tx sdk.Tx) (bool, error)
|
||||
|
||||
// GetTxDistribution returns the number of transactions in each lane.
|
||||
GetTxDistribution() map[string]int
|
||||
}
|
||||
|
||||
// Mempool defines the Blockbuster mempool implement. It contains a registry
|
||||
@ -34,24 +37,27 @@ func NewMempool(lanes ...Lane) *BBMempool {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider using a tx cache in Mempool and returning the length of that
|
||||
// cache instead of relying on lane count tracking.
|
||||
// CountTx returns the total number of transactions in the mempool.
|
||||
func (m *BBMempool) CountTx() int {
|
||||
var total int
|
||||
for _, lane := range m.registry {
|
||||
// TODO: If a global lane exists, we assume that lane has all transactions
|
||||
// and we return the total.
|
||||
//
|
||||
// if lane.Name() == LaneNameGlobal {
|
||||
// return lane.CountTx()
|
||||
// }
|
||||
|
||||
total += lane.CountTx()
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// GetTxDistribution returns the number of transactions in each lane.
|
||||
func (m *BBMempool) GetTxDistribution() map[string]int {
|
||||
counts := make(map[string]int, len(m.registry))
|
||||
|
||||
for _, lane := range m.registry {
|
||||
counts[lane.Name()] = lane.CountTx()
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
|
||||
// Insert inserts a transaction into every lane that it matches. Insertion will
|
||||
// be attempted on all lanes, even if an error is encountered.
|
||||
func (m *BBMempool) Insert(ctx context.Context, tx sdk.Tx) error {
|
||||
@ -74,8 +80,8 @@ func (m *BBMempool) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a transaction from every lane that it matches. Removal will be
|
||||
// attempted on all lanes, even if an error is encountered.
|
||||
// Remove removes a transaction from the mempool. It removes the transaction
|
||||
// from the first lane that it matches.
|
||||
func (m *BBMempool) Remove(tx sdk.Tx) error {
|
||||
for _, lane := range m.registry {
|
||||
if lane.Match(tx) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package mempool
|
||||
package blockbuster
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -1,4 +1,4 @@
|
||||
package blockbuster
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@ -7,19 +7,21 @@ import (
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
)
|
||||
|
||||
// GetTxHashStr returns the hex-encoded hash of the transaction.
|
||||
func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) (string, error) {
|
||||
// GetTxHashStr returns the hex-encoded hash of the transaction alongside the
|
||||
// transaction bytes.
|
||||
func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) ([]byte, string, error) {
|
||||
txBz, err := txEncoder(tx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode transaction: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
|
||||
txHash := sha256.Sum256(txBz)
|
||||
txHashStr := hex.EncodeToString(txHash[:])
|
||||
|
||||
return txHashStr, nil
|
||||
return txBz, txHashStr, nil
|
||||
}
|
||||
|
||||
// RemoveTxsFromLane removes the transactions from the given lane's mempool.
|
||||
@ -35,7 +37,7 @@ func RemoveTxsFromLane(txs map[sdk.Tx]struct{}, mempool sdkmempool.Mempool) erro
|
||||
|
||||
// GetMaxTxBytesForLane returns the maximum number of bytes that can be included in the proposal
|
||||
// for the given lane.
|
||||
func GetMaxTxBytesForLane(proposal *Proposal, ratio sdk.Dec) int64 {
|
||||
func GetMaxTxBytesForLane(proposal *blockbuster.Proposal, ratio sdk.Dec) int64 {
|
||||
// In the case where the ratio is zero, we return the max tx bytes remaining. Note, the only
|
||||
// lane that should have a ratio of zero is the default lane. This means the default lane
|
||||
// will have no limit on the number of transactions it can include in a block and is only
|
||||
@ -1,128 +0,0 @@
|
||||
package mempool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
)
|
||||
|
||||
type (
|
||||
// AuctionBidInfo defines the information about a bid to the auction house.
|
||||
AuctionBidInfo struct {
|
||||
Bidder sdk.AccAddress
|
||||
Bid sdk.Coin
|
||||
Transactions [][]byte
|
||||
Timeout uint64
|
||||
Signers []map[string]struct{}
|
||||
}
|
||||
|
||||
// AuctionFactory defines the interface for processing auction transactions. It is
|
||||
// a wrapper around all of the functionality that each application chain must implement
|
||||
// in order for auction processing to work.
|
||||
AuctionFactory interface {
|
||||
// WrapBundleTransaction defines a function that wraps a bundle transaction into a sdk.Tx. Since
|
||||
// this is a potentially expensive operation, we allow each application chain to define how
|
||||
// they want to wrap the transaction such that it is only called when necessary (i.e. when the
|
||||
// transaction is being considered in the proposal handlers).
|
||||
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
|
||||
|
||||
// GetAuctionBidInfo defines a function that returns the bid info from an auction transaction.
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*AuctionBidInfo, error)
|
||||
}
|
||||
|
||||
// DefaultAuctionFactory defines a default implmentation for the auction factory interface for processing auction transactions.
|
||||
DefaultAuctionFactory struct {
|
||||
txDecoder sdk.TxDecoder
|
||||
}
|
||||
|
||||
// TxWithTimeoutHeight is used to extract timeouts from sdk.Tx transactions. In the case where,
|
||||
// timeouts are explicitly set on the sdk.Tx, we can use this interface to extract the timeout.
|
||||
TxWithTimeoutHeight interface {
|
||||
sdk.Tx
|
||||
|
||||
GetTimeoutHeight() uint64
|
||||
}
|
||||
)
|
||||
|
||||
var _ AuctionFactory = (*DefaultAuctionFactory)(nil)
|
||||
|
||||
// NewDefaultAuctionFactory returns a default auction factory interface implementation.
|
||||
func NewDefaultAuctionFactory(txDecoder sdk.TxDecoder) AuctionFactory {
|
||||
return &DefaultAuctionFactory{
|
||||
txDecoder: txDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
// WrapBundleTransaction defines a default function that wraps a transaction
|
||||
// that is included in the bundle into a sdk.Tx. In the default case, the transaction
|
||||
// that is included in the bundle will be the raw bytes of an sdk.Tx so we can just
|
||||
// decode it.
|
||||
func (config *DefaultAuctionFactory) WrapBundleTransaction(tx []byte) (sdk.Tx, error) {
|
||||
return config.txDecoder(tx)
|
||||
}
|
||||
|
||||
// GetAuctionBidInfo defines a default function that returns the auction bid info from
|
||||
// an auction transaction. In the default case, the auction bid info is stored in the
|
||||
// MsgAuctionBid message.
|
||||
func (config *DefaultAuctionFactory) GetAuctionBidInfo(tx sdk.Tx) (*AuctionBidInfo, error) {
|
||||
msg, err := GetMsgAuctionBidFromTx(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
bidder, err := sdk.AccAddressFromBech32(msg.Bidder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid bidder address (%s): %w", msg.Bidder, err)
|
||||
}
|
||||
|
||||
timeoutTx, ok := tx.(TxWithTimeoutHeight)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot extract timeout; transaction does not implement TxWithTimeoutHeight")
|
||||
}
|
||||
|
||||
signers, err := config.getBundleSigners(msg.Transactions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuctionBidInfo{
|
||||
Bid: msg.Bid,
|
||||
Bidder: bidder,
|
||||
Transactions: msg.Transactions,
|
||||
Timeout: timeoutTx.GetTimeoutHeight(),
|
||||
Signers: signers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBundleSigners defines a default function that returns the signers of all transactions in
|
||||
// a bundle. In the default case, each bundle transaction will be an sdk.Tx and the
|
||||
// signers are the signers of each sdk.Msg in the transaction.
|
||||
func (config *DefaultAuctionFactory) getBundleSigners(bundle [][]byte) ([]map[string]struct{}, error) {
|
||||
signers := make([]map[string]struct{}, 0)
|
||||
|
||||
for _, tx := range bundle {
|
||||
sdkTx, err := config.txDecoder(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigTx, ok := sdkTx.(signing.SigVerifiableTx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("transaction is not valid")
|
||||
}
|
||||
|
||||
txSigners := make(map[string]struct{})
|
||||
for _, signer := range sigTx.GetSigners() {
|
||||
txSigners[signer.String()] = struct{}{}
|
||||
}
|
||||
|
||||
signers = append(signers, txSigners)
|
||||
}
|
||||
|
||||
return signers, nil
|
||||
}
|
||||
@ -1,240 +0,0 @@
|
||||
package mempool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
)
|
||||
|
||||
var _ Mempool = (*AuctionMempool)(nil)
|
||||
|
||||
type (
|
||||
// Mempool defines the interface for a POB mempool.
|
||||
Mempool interface {
|
||||
// Inherit the methods of the SDK's Mempool interface.
|
||||
sdkmempool.Mempool
|
||||
|
||||
// GetTopAuctionTx returns the top auction bid transaction in the mempool.
|
||||
GetTopAuctionTx(ctx context.Context) sdk.Tx
|
||||
|
||||
// CountAuctionTx returns the number of auction bid transactions in the mempool.
|
||||
CountAuctionTx() int
|
||||
|
||||
// AuctionBidSelect returns an iterator over the auction bid transactions in the mempool.
|
||||
AuctionBidSelect(ctx context.Context) sdkmempool.Iterator
|
||||
|
||||
// Contains returns true if the mempool contains the given transaction.
|
||||
Contains(tx sdk.Tx) (bool, error)
|
||||
|
||||
// AuctionFactory implements the functionality required to process auction transactions.
|
||||
AuctionFactory
|
||||
}
|
||||
|
||||
// AuctionMempool defines an auction mempool. It can be seen as an extension of
|
||||
// an SDK PriorityNonceMempool, i.e. a mempool that supports <sender, nonce>
|
||||
// two-dimensional priority ordering, with the additional support of prioritizing
|
||||
// and indexing auction bids.
|
||||
AuctionMempool struct {
|
||||
// globalIndex defines the index of all transactions in the mempool. It uses
|
||||
// the SDK's builtin PriorityNonceMempool. Once a bid is selected for top-of-block,
|
||||
// all subsequent transactions in the mempool will be selected from this index.
|
||||
globalIndex sdkmempool.Mempool
|
||||
|
||||
// auctionIndex defines an index of auction bids.
|
||||
auctionIndex sdkmempool.Mempool
|
||||
|
||||
// txDecoder defines the sdk.Tx decoder that allows us to decode transactions
|
||||
// and construct sdk.Txs from the bundled transactions.
|
||||
txDecoder sdk.TxDecoder
|
||||
|
||||
// txEncoder defines the sdk.Tx encoder that allows us to encode transactions
|
||||
// to bytes.
|
||||
txEncoder sdk.TxEncoder
|
||||
|
||||
// txIndex is a map of all transactions in the mempool. It is used
|
||||
// to quickly check if a transaction is already in the mempool.
|
||||
txIndex map[string]struct{}
|
||||
|
||||
// AuctionFactory implements the functionality required to process auction transactions.
|
||||
AuctionFactory
|
||||
}
|
||||
)
|
||||
|
||||
// AuctionTxPriority returns a TxPriority over auction bid transactions only. It
|
||||
// is to be used in the auction index only.
|
||||
func AuctionTxPriority(config AuctionFactory) TxPriority[string] {
|
||||
return TxPriority[string]{
|
||||
GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string {
|
||||
bidInfo, err := config.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return bidInfo.Bid.String()
|
||||
},
|
||||
Compare: func(a, b string) int {
|
||||
aCoins, _ := sdk.ParseCoinsNormalized(a)
|
||||
bCoins, _ := sdk.ParseCoinsNormalized(b)
|
||||
|
||||
switch {
|
||||
case aCoins == nil && bCoins == nil:
|
||||
return 0
|
||||
|
||||
case aCoins == nil:
|
||||
return -1
|
||||
|
||||
case bCoins == nil:
|
||||
return 1
|
||||
|
||||
default:
|
||||
switch {
|
||||
case aCoins.IsAllGT(bCoins):
|
||||
return 1
|
||||
|
||||
case aCoins.IsAllLT(bCoins):
|
||||
return -1
|
||||
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
MinValue: "",
|
||||
}
|
||||
}
|
||||
|
||||
func NewAuctionMempool(txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, maxTx int, config AuctionFactory) *AuctionMempool {
|
||||
return &AuctionMempool{
|
||||
globalIndex: NewPriorityMempool(
|
||||
PriorityNonceMempoolConfig[int64]{
|
||||
TxPriority: NewDefaultTxPriority(),
|
||||
MaxTx: maxTx,
|
||||
},
|
||||
),
|
||||
auctionIndex: NewPriorityMempool(
|
||||
PriorityNonceMempoolConfig[string]{
|
||||
TxPriority: AuctionTxPriority(config),
|
||||
MaxTx: maxTx,
|
||||
},
|
||||
),
|
||||
txDecoder: txDecoder,
|
||||
txEncoder: txEncoder,
|
||||
txIndex: make(map[string]struct{}),
|
||||
AuctionFactory: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts a transaction into the mempool based on the transaction type (normal or auction).
|
||||
func (am *AuctionMempool) Insert(ctx context.Context, tx sdk.Tx) error {
|
||||
bidInfo, err := am.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert the transactions into the appropriate index.
|
||||
if bidInfo == nil {
|
||||
if err := am.globalIndex.Insert(ctx, tx); err != nil {
|
||||
return fmt.Errorf("failed to insert tx into global index: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := am.auctionIndex.Insert(ctx, tx); err != nil {
|
||||
return fmt.Errorf("failed to insert tx into auction index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
txHashStr, err := am.getTxHashStr(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
am.txIndex[txHashStr] = struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a transaction from the mempool based on the transaction type (normal or auction).
|
||||
func (am *AuctionMempool) Remove(tx sdk.Tx) error {
|
||||
bidInfo, err := am.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the transactions from the appropriate index.
|
||||
if bidInfo == nil {
|
||||
am.removeTx(am.globalIndex, tx)
|
||||
} else {
|
||||
am.removeTx(am.auctionIndex, tx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTopAuctionTx returns the highest bidding transaction in the auction mempool.
|
||||
func (am *AuctionMempool) GetTopAuctionTx(ctx context.Context) sdk.Tx {
|
||||
iterator := am.auctionIndex.Select(ctx, nil)
|
||||
if iterator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return iterator.Tx()
|
||||
}
|
||||
|
||||
// AuctionBidSelect returns an iterator over auction bids transactions only.
|
||||
func (am *AuctionMempool) AuctionBidSelect(ctx context.Context) sdkmempool.Iterator {
|
||||
return am.auctionIndex.Select(ctx, nil)
|
||||
}
|
||||
|
||||
func (am *AuctionMempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator {
|
||||
return am.globalIndex.Select(ctx, txs)
|
||||
}
|
||||
|
||||
func (am *AuctionMempool) CountAuctionTx() int {
|
||||
return am.auctionIndex.CountTx()
|
||||
}
|
||||
|
||||
func (am *AuctionMempool) CountTx() int {
|
||||
return am.globalIndex.CountTx()
|
||||
}
|
||||
|
||||
// Contains returns true if the transaction is contained in the mempool.
|
||||
func (am *AuctionMempool) Contains(tx sdk.Tx) (bool, error) {
|
||||
txHashStr, err := am.getTxHashStr(tx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get tx hash string: %w", err)
|
||||
}
|
||||
|
||||
_, ok := am.txIndex[txHashStr]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (am *AuctionMempool) removeTx(mp sdkmempool.Mempool, tx sdk.Tx) {
|
||||
err := mp.Remove(tx)
|
||||
if err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) {
|
||||
panic(fmt.Errorf("failed to remove invalid transaction from the mempool: %w", err))
|
||||
}
|
||||
|
||||
txHashStr, err := am.getTxHashStr(tx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get tx hash string: %w", err))
|
||||
}
|
||||
|
||||
delete(am.txIndex, txHashStr)
|
||||
}
|
||||
|
||||
// getTxHashStr returns the transaction hash string for a given transaction.
|
||||
func (am *AuctionMempool) getTxHashStr(tx sdk.Tx) (string, error) {
|
||||
txBz, err := am.txEncoder(tx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
|
||||
txHash := sha256.Sum256(txBz)
|
||||
txHashStr := hex.EncodeToString(txHash[:])
|
||||
|
||||
return txHashStr, nil
|
||||
}
|
||||
@ -1,240 +0,0 @@
|
||||
package mempool_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
encCfg testutils.EncodingConfig
|
||||
config mempool.AuctionFactory
|
||||
mempool *mempool.AuctionMempool
|
||||
ctx sdk.Context
|
||||
random *rand.Rand
|
||||
accounts []testutils.Account
|
||||
nonces map[string]uint64
|
||||
}
|
||||
|
||||
func TestMempoolTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) SetupTest() {
|
||||
// Mempool setup
|
||||
suite.encCfg = testutils.CreateTestEncodingConfig()
|
||||
suite.config = mempool.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder())
|
||||
suite.mempool = mempool.NewAuctionMempool(suite.encCfg.TxConfig.TxDecoder(), suite.encCfg.TxConfig.TxEncoder(), 0, suite.config)
|
||||
suite.ctx = sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
|
||||
|
||||
// Init accounts
|
||||
suite.random = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
suite.accounts = testutils.RandomAccounts(suite.random, 10)
|
||||
|
||||
suite.nonces = make(map[string]uint64)
|
||||
for _, acc := range suite.accounts {
|
||||
suite.nonces[acc.Address.String()] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFilledMempool creates a pre-filled mempool with numNormalTxs normal transactions, numAuctionTxs auction transactions, and numBundledTxs bundled
|
||||
// transactions per auction transaction. If insertRefTxs is true, it will also insert a the referenced transactions into the mempool. This returns
|
||||
// the total number of transactions inserted into the mempool.
|
||||
func (suite *IntegrationTestSuite) CreateFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs int, insertRefTxs bool) int {
|
||||
// Insert a bunch of normal transactions into the global mempool
|
||||
for i := 0; i < numNormalTxs; i++ {
|
||||
// create a few random msgs
|
||||
|
||||
// randomly select an account to create the tx
|
||||
randomIndex := suite.random.Intn(len(suite.accounts))
|
||||
acc := suite.accounts[randomIndex]
|
||||
nonce := suite.nonces[acc.Address.String()]
|
||||
randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3)
|
||||
randomTx, err := testutils.CreateTx(suite.encCfg.TxConfig, acc, nonce, 100, 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))
|
||||
contains, err := suite.mempool.Contains(randomTx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().True(contains)
|
||||
}
|
||||
|
||||
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
|
||||
acc := testutils.RandomAccounts(suite.random, 1)[0]
|
||||
|
||||
// create a new auction bid msg with numBundledTxs bundled transactions
|
||||
priority := suite.random.Int63n(100) + 1
|
||||
bid := sdk.NewInt64Coin("foo", priority)
|
||||
nonce := suite.nonces[acc.Address.String()]
|
||||
bidMsg, err := testutils.CreateMsgAuctionBid(suite.encCfg.TxConfig, acc, bid, 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.encCfg.TxConfig, acc, nonce, 1000, []sdk.Msg{bidMsg})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// insert the auction tx into the global mempool
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), auctionTx))
|
||||
contains, err := suite.mempool.Contains(auctionTx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().True(contains)
|
||||
suite.nonces[acc.Address.String()]++
|
||||
|
||||
if insertRefTxs {
|
||||
for _, refRawTx := range bidMsg.GetTransactions() {
|
||||
refTx, err := suite.encCfg.TxConfig.TxDecoder()(refRawTx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), refTx))
|
||||
contains, err = suite.mempool.Contains(refTx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().True(contains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalNumTxs int
|
||||
suite.Require().Equal(numAuctionTxs, suite.mempool.CountAuctionTx())
|
||||
if insertRefTxs {
|
||||
totalNumTxs = numNormalTxs + numAuctionTxs*(numBundledTxs)
|
||||
suite.Require().Equal(totalNumTxs, suite.mempool.CountTx())
|
||||
} else {
|
||||
suite.Require().Equal(totalNumTxs, suite.mempool.CountTx())
|
||||
}
|
||||
|
||||
return totalNumTxs
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) TestAuctionMempoolRemove() {
|
||||
numberTotalTxs := 100
|
||||
numberAuctionTxs := 10
|
||||
numberBundledTxs := 5
|
||||
insertRefTxs := true
|
||||
numMempoolTxs := suite.CreateFilledMempool(numberTotalTxs, numberAuctionTxs, numberBundledTxs, insertRefTxs)
|
||||
|
||||
// Select the top bid tx from the auction mempool and do sanity checks
|
||||
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
suite.Require().NotNil(auctionIterator)
|
||||
tx := auctionIterator.Tx()
|
||||
suite.Require().Len(tx.GetMsgs(), 1)
|
||||
suite.Require().NoError(suite.mempool.Remove(tx))
|
||||
|
||||
// Ensure that the auction tx was removed from the auction mempool only
|
||||
suite.Require().Equal(numberAuctionTxs-1, suite.mempool.CountAuctionTx())
|
||||
suite.Require().Equal(numMempoolTxs, suite.mempool.CountTx())
|
||||
contains, err := suite.mempool.Contains(tx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().False(contains)
|
||||
|
||||
// Attempt to remove again and ensure that the tx is not found
|
||||
suite.Require().NoError(suite.mempool.Remove(tx))
|
||||
suite.Require().Equal(numberAuctionTxs-1, suite.mempool.CountAuctionTx())
|
||||
suite.Require().Equal(numMempoolTxs, suite.mempool.CountTx())
|
||||
|
||||
// Bundled txs should be in the global mempool
|
||||
auctionMsg, err := mempool.GetMsgAuctionBidFromTx(tx)
|
||||
suite.Require().NoError(err)
|
||||
for _, refTx := range auctionMsg.GetTransactions() {
|
||||
tx, err := suite.encCfg.TxConfig.TxDecoder()(refTx)
|
||||
suite.Require().NoError(err)
|
||||
contains, err = suite.mempool.Contains(tx)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().True(contains)
|
||||
}
|
||||
|
||||
// Attempt to remove a global tx
|
||||
iterator := suite.mempool.Select(context.Background(), nil)
|
||||
tx = iterator.Tx()
|
||||
size := suite.mempool.CountTx()
|
||||
suite.mempool.Remove(tx)
|
||||
suite.Require().Equal(size-1, suite.mempool.CountTx())
|
||||
|
||||
// Remove the rest of the global transactions
|
||||
iterator = suite.mempool.Select(context.Background(), nil)
|
||||
suite.Require().NotNil(iterator)
|
||||
for iterator != nil {
|
||||
tx = iterator.Tx()
|
||||
suite.Require().NoError(suite.mempool.Remove(tx))
|
||||
iterator = suite.mempool.Select(context.Background(), nil)
|
||||
}
|
||||
suite.Require().Equal(0, suite.mempool.CountTx())
|
||||
|
||||
// Remove the rest of the auction transactions
|
||||
auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
for auctionIterator != nil {
|
||||
tx = auctionIterator.Tx()
|
||||
suite.Require().NoError(suite.mempool.Remove(tx))
|
||||
auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
}
|
||||
suite.Require().Equal(0, suite.mempool.CountAuctionTx())
|
||||
|
||||
// Ensure that the mempool is empty
|
||||
iterator = suite.mempool.Select(context.Background(), nil)
|
||||
suite.Require().Nil(iterator)
|
||||
auctionIterator = suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
suite.Require().Nil(auctionIterator)
|
||||
suite.Require().Equal(0, suite.mempool.CountTx())
|
||||
suite.Require().Equal(0, suite.mempool.CountAuctionTx())
|
||||
}
|
||||
|
||||
func (suite *IntegrationTestSuite) TestAuctionMempoolSelect() {
|
||||
numberTotalTxs := 100
|
||||
numberAuctionTxs := 10
|
||||
numberBundledTxs := 5
|
||||
insertRefTxs := true
|
||||
totalTxs := suite.CreateFilledMempool(numberTotalTxs, numberAuctionTxs, numberBundledTxs, insertRefTxs)
|
||||
|
||||
// iterate through the entire auction mempool and ensure the bids are in order
|
||||
var highestBid sdk.Coin
|
||||
var prevBid sdk.Coin
|
||||
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
|
||||
numberTxsSeen := 0
|
||||
for auctionIterator != nil {
|
||||
tx := auctionIterator.Tx()
|
||||
suite.Require().Len(tx.GetMsgs(), 1)
|
||||
|
||||
msgAuctionBid := tx.GetMsgs()[0].(*buildertypes.MsgAuctionBid)
|
||||
if highestBid.IsNil() {
|
||||
highestBid = msgAuctionBid.Bid
|
||||
prevBid = msgAuctionBid.Bid
|
||||
} else {
|
||||
suite.Require().True(msgAuctionBid.Bid.IsLTE(highestBid))
|
||||
suite.Require().True(msgAuctionBid.Bid.IsLTE(prevBid))
|
||||
prevBid = msgAuctionBid.Bid
|
||||
}
|
||||
|
||||
suite.Require().Len(msgAuctionBid.GetTransactions(), numberBundledTxs)
|
||||
|
||||
auctionIterator = auctionIterator.Next()
|
||||
numberTxsSeen++
|
||||
}
|
||||
|
||||
suite.Require().Equal(numberAuctionTxs, numberTxsSeen)
|
||||
|
||||
iterator := suite.mempool.Select(context.Background(), nil)
|
||||
numberTxsSeen = 0
|
||||
for iterator != nil {
|
||||
iterator = iterator.Next()
|
||||
numberTxsSeen++
|
||||
}
|
||||
suite.Require().Equal(totalTxs, numberTxsSeen)
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package mempool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
// GetMsgAuctionBidFromTx attempts to retrieve a MsgAuctionBid from an sdk.Tx if
|
||||
// one exists. If a MsgAuctionBid does exist and other messages are also present,
|
||||
// an error is returned. If no MsgAuctionBid is present, <nil, nil> is returned.
|
||||
func GetMsgAuctionBidFromTx(tx sdk.Tx) (*buildertypes.MsgAuctionBid, error) {
|
||||
auctionBidMsgs := make([]*buildertypes.MsgAuctionBid, 0)
|
||||
for _, msg := range tx.GetMsgs() {
|
||||
t, ok := msg.(*buildertypes.MsgAuctionBid)
|
||||
if ok {
|
||||
auctionBidMsgs = append(auctionBidMsgs, t)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(auctionBidMsgs) == 0:
|
||||
// a normal transaction without a MsgAuctionBid message
|
||||
return nil, nil
|
||||
|
||||
case len(auctionBidMsgs) == 1 && len(tx.GetMsgs()) == 1:
|
||||
// a single MsgAuctionBid message transaction
|
||||
return auctionBidMsgs[0], nil
|
||||
|
||||
default:
|
||||
// a transaction with at at least one MsgAuctionBid message
|
||||
return nil, errors.New("invalid MsgAuctionBid transaction")
|
||||
}
|
||||
}
|
||||
@ -3,14 +3,15 @@ package app
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/ante"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
builderante "github.com/skip-mev/pob/x/builder/ante"
|
||||
builderkeeper "github.com/skip-mev/pob/x/builder/keeper"
|
||||
)
|
||||
|
||||
type POBHandlerOptions struct {
|
||||
BaseOptions ante.HandlerOptions
|
||||
Mempool mempool.Mempool
|
||||
Mempool blockbuster.Mempool
|
||||
TOBLane builderante.TOBLane
|
||||
TxDecoder sdk.TxDecoder
|
||||
TxEncoder sdk.TxEncoder
|
||||
BuilderKeeper builderkeeper.Keeper
|
||||
@ -48,7 +49,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.TxEncoder, options.Mempool),
|
||||
builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxEncoder, options.TOBLane, options.Mempool),
|
||||
}
|
||||
|
||||
return sdk.ChainAnteDecorators(anteDecorators...)
|
||||
|
||||
@ -67,8 +67,10 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/x/upgrade"
|
||||
upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client"
|
||||
upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper"
|
||||
"github.com/skip-mev/pob/abci"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/abci"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/base"
|
||||
buildermodule "github.com/skip-mev/pob/x/builder"
|
||||
builderkeeper "github.com/skip-mev/pob/x/builder/keeper"
|
||||
)
|
||||
@ -261,8 +263,38 @@ func New(
|
||||
|
||||
app.App = appBuilder.Build(logger, db, traceStore, baseAppOptions...)
|
||||
|
||||
// ---------------------------------------------------------------------------- //
|
||||
// ------------------------- Begin Custom Code -------------------------------- //
|
||||
// ---------------------------------------------------------------------------- //
|
||||
|
||||
// Set POB's mempool into the app.
|
||||
mempool := mempool.NewAuctionMempool(app.txConfig.TxDecoder(), app.txConfig.TxEncoder(), 0, mempool.NewDefaultAuctionFactory(app.txConfig.TxDecoder()))
|
||||
config := blockbuster.BaseLaneConfig{
|
||||
Logger: app.Logger(),
|
||||
TxEncoder: app.txConfig.TxEncoder(),
|
||||
TxDecoder: app.txConfig.TxDecoder(),
|
||||
MaxBlockSpace: sdk.ZeroDec(),
|
||||
}
|
||||
|
||||
// Create the lanes.
|
||||
//
|
||||
// NOTE: The lanes are ordered by priority. The first lane is the highest priority
|
||||
// lane and the last lane is the lowest priority lane.
|
||||
|
||||
// Top of block lane allows transactions to bid for inclusion at the top of the next block.
|
||||
tobLane := auction.NewTOBLane(
|
||||
config,
|
||||
0,
|
||||
auction.NewDefaultAuctionFactory(app.txConfig.TxDecoder()),
|
||||
)
|
||||
|
||||
// Default lane accepts all other transactions.
|
||||
defaultLane := base.NewDefaultLane(config)
|
||||
lanes := []blockbuster.Lane{
|
||||
tobLane,
|
||||
defaultLane,
|
||||
}
|
||||
|
||||
mempool := blockbuster.NewMempool(lanes...)
|
||||
app.App.SetMempool(mempool)
|
||||
|
||||
// Create a global ante handler that will be called on each transaction when
|
||||
@ -277,19 +309,22 @@ func New(
|
||||
options := POBHandlerOptions{
|
||||
BaseOptions: handlerOptions,
|
||||
BuilderKeeper: app.BuilderKeeper,
|
||||
Mempool: mempool,
|
||||
TxDecoder: app.txConfig.TxDecoder(),
|
||||
TxEncoder: app.txConfig.TxEncoder(),
|
||||
TOBLane: tobLane,
|
||||
Mempool: mempool,
|
||||
}
|
||||
anteHandler := NewPOBAnteHandler(options)
|
||||
|
||||
// Set the lane config on the lanes.
|
||||
for _, lane := range lanes {
|
||||
lane.SetAnteHandler(anteHandler)
|
||||
}
|
||||
|
||||
// Set the proposal handlers on the BaseApp along with the custom antehandler.
|
||||
proposalHandlers := abci.NewProposalHandler(
|
||||
app.Logger(),
|
||||
mempool,
|
||||
app.App.Logger(),
|
||||
anteHandler,
|
||||
options.TxEncoder,
|
||||
options.TxDecoder,
|
||||
)
|
||||
app.App.SetPrepareProposal(proposalHandlers.PrepareProposalHandler())
|
||||
app.App.SetProcessProposal(proposalHandlers.ProcessProposalHandler())
|
||||
@ -299,12 +334,16 @@ func New(
|
||||
checkTxHandler := abci.NewCheckTxHandler(
|
||||
app.App,
|
||||
app.txConfig.TxDecoder(),
|
||||
mempool,
|
||||
tobLane,
|
||||
anteHandler,
|
||||
ChainID,
|
||||
)
|
||||
app.SetCheckTx(checkTxHandler.CheckTx())
|
||||
|
||||
// ---------------------------------------------------------------------------- //
|
||||
// ------------------------- End Custom Code ---------------------------------- //
|
||||
// ---------------------------------------------------------------------------- //
|
||||
|
||||
// load state streaming if enabled
|
||||
if _, _, err := streaming.LoadStreamingServices(app.App.BaseApp, appOpts, app.appCodec, logger, app.kvStoreKeys()); err != nil {
|
||||
logger.Error("failed to load state streaming", "err", err)
|
||||
|
||||
@ -7,32 +7,39 @@ import (
|
||||
|
||||
"cosmossdk.io/errors"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/x/builder/keeper"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
var _ sdk.AnteDecorator = BuilderDecorator{}
|
||||
|
||||
type (
|
||||
// TOBLane is an interface that defines the methods required to interact with the top of block
|
||||
// lane.
|
||||
TOBLane interface {
|
||||
GetAuctionBidInfo(tx sdk.Tx) (*types.BidInfo, error)
|
||||
GetTopAuctionTx(ctx context.Context) sdk.Tx
|
||||
}
|
||||
|
||||
// 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
|
||||
txEncoder sdk.TxEncoder
|
||||
lane TOBLane
|
||||
mempool Mempool
|
||||
}
|
||||
)
|
||||
|
||||
func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator {
|
||||
func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, lane TOBLane, mempool Mempool) BuilderDecorator {
|
||||
return BuilderDecorator{
|
||||
builderKeeper: ak,
|
||||
txEncoder: txEncoder,
|
||||
lane: lane,
|
||||
mempool: mempool,
|
||||
}
|
||||
}
|
||||
@ -52,7 +59,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
|
||||
}
|
||||
}
|
||||
|
||||
bidInfo, err := bd.mempool.GetAuctionBidInfo(tx)
|
||||
bidInfo, err := bd.lane.GetAuctionBidInfo(tx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
@ -70,7 +77,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
|
||||
// poor liveness guarantees.
|
||||
topBid := sdk.Coin{}
|
||||
if ctx.IsCheckTx() || ctx.IsReCheckTx() {
|
||||
if topBidTx := bd.mempool.GetTopAuctionTx(ctx); topBidTx != nil {
|
||||
if topBidTx := bd.lane.GetTopAuctionTx(ctx); topBidTx != nil {
|
||||
topBidBz, err := bd.txEncoder(topBidTx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
@ -83,7 +90,7 @@ func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
|
||||
|
||||
// Compare the bytes to see if the current transaction is the highest bidding transaction.
|
||||
if !bytes.Equal(topBidBz, currentTxBz) {
|
||||
topBidInfo, err := bd.mempool.GetAuctionBidInfo(topBidTx)
|
||||
topBidInfo, err := bd.lane.GetAuctionBidInfo(topBidTx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/blockbuster"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/auction"
|
||||
"github.com/skip-mev/pob/blockbuster/lanes/base"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/ante"
|
||||
"github.com/skip-mev/pob/x/builder/keeper"
|
||||
@ -21,7 +23,6 @@ type AnteTestSuite struct {
|
||||
suite.Suite
|
||||
ctx sdk.Context
|
||||
|
||||
// mempool setup
|
||||
encodingConfig testutils.EncodingConfig
|
||||
random *rand.Rand
|
||||
|
||||
@ -34,6 +35,15 @@ type AnteTestSuite struct {
|
||||
builderDecorator ante.BuilderDecorator
|
||||
key *storetypes.KVStoreKey
|
||||
authorityAccount sdk.AccAddress
|
||||
|
||||
// mempool and lane set up
|
||||
mempool blockbuster.Mempool
|
||||
tobLane *auction.TOBLane
|
||||
baseLane *base.DefaultLane
|
||||
lanes []blockbuster.Lane
|
||||
|
||||
// Account set up
|
||||
balance sdk.Coins
|
||||
}
|
||||
|
||||
func TestAnteTestSuite(t *testing.T) {
|
||||
@ -67,17 +77,40 @@ func (suite *AnteTestSuite) SetupTest() {
|
||||
)
|
||||
err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Lanes configuration
|
||||
//
|
||||
// TOB lane set up
|
||||
config := blockbuster.BaseLaneConfig{
|
||||
Logger: suite.ctx.Logger(),
|
||||
TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(),
|
||||
TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(),
|
||||
AnteHandler: suite.anteHandler,
|
||||
MaxBlockSpace: sdk.ZeroDec(),
|
||||
}
|
||||
suite.tobLane = auction.NewTOBLane(
|
||||
config,
|
||||
0, // No bound on the number of transactions in the lane
|
||||
auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()),
|
||||
)
|
||||
|
||||
// Base lane set up
|
||||
suite.baseLane = base.NewDefaultLane(config)
|
||||
|
||||
// Mempool set up
|
||||
suite.lanes = []blockbuster.Lane{suite.tobLane, suite.baseLane}
|
||||
suite.mempool = blockbuster.NewMempool(suite.lanes...)
|
||||
}
|
||||
|
||||
func (suite *AnteTestSuite) executeAnteHandler(tx sdk.Tx, balance sdk.Coins) (sdk.Context, error) {
|
||||
func (suite *AnteTestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
signer := tx.GetMsgs()[0].GetSigners()[0]
|
||||
suite.bankKeeper.EXPECT().GetAllBalances(suite.ctx, signer).AnyTimes().Return(balance)
|
||||
suite.bankKeeper.EXPECT().GetAllBalances(ctx, signer).AnyTimes().Return(suite.balance)
|
||||
|
||||
next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
next := func(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
return suite.builderDecorator.AnteHandle(suite.ctx, tx, false, next)
|
||||
return suite.builderDecorator.AnteHandle(ctx, tx, false, next)
|
||||
}
|
||||
|
||||
func (suite *AnteTestSuite) TestAnteHandler() {
|
||||
@ -232,15 +265,19 @@ func (suite *AnteTestSuite) TestAnteHandler() {
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Insert the top bid into the mempool
|
||||
config := mempool.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder())
|
||||
mempool := mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, config)
|
||||
if insertTopBid {
|
||||
topAuctionTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, topBidder, topBid, 0, timeout, []testutils.Account{})
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(0, mempool.CountTx())
|
||||
suite.Require().Equal(0, mempool.CountAuctionTx())
|
||||
suite.Require().NoError(mempool.Insert(suite.ctx, topAuctionTx))
|
||||
suite.Require().Equal(1, mempool.CountAuctionTx())
|
||||
|
||||
distribution := suite.mempool.GetTxDistribution()
|
||||
suite.Require().Equal(0, distribution[auction.LaneName])
|
||||
suite.Require().Equal(0, distribution[base.LaneName])
|
||||
|
||||
suite.Require().NoError(suite.mempool.Insert(suite.ctx, topAuctionTx))
|
||||
|
||||
distribution = suite.mempool.GetTxDistribution()
|
||||
suite.Require().Equal(1, distribution[auction.LaneName])
|
||||
suite.Require().Equal(0, distribution[base.LaneName])
|
||||
}
|
||||
|
||||
// Create the actual auction tx and insert into the mempool
|
||||
@ -248,8 +285,9 @@ func (suite *AnteTestSuite) TestAnteHandler() {
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Execute the ante handler
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), mempool)
|
||||
_, err = suite.executeAnteHandler(auctionTx, balance)
|
||||
suite.balance = balance
|
||||
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
|
||||
_, err = suite.anteHandler(suite.ctx, auctionTx, false)
|
||||
if tc.pass {
|
||||
suite.Require().NoError(err)
|
||||
} else {
|
||||
|
||||
@ -4,11 +4,11 @@ import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
// ValidateBidInfo validates that the bid can be included in the auction.
|
||||
func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *mempool.AuctionBidInfo) error {
|
||||
func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *types.BidInfo) error {
|
||||
// Validate the bundle size.
|
||||
maxBundleSize, err := k.GetMaxBundleSize(ctx)
|
||||
if err != nil {
|
||||
|
||||
@ -5,10 +5,9 @@ import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/keeper"
|
||||
buildertypes "github.com/skip-mev/pob/x/builder/types"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestValidateBidInfo() {
|
||||
@ -162,7 +161,7 @@ func (suite *KeeperTestSuite) TestValidateBidInfo() {
|
||||
suite.stakingKeeper,
|
||||
suite.authorityAccount.String(),
|
||||
)
|
||||
params := buildertypes.Params{
|
||||
params := types.Params{
|
||||
MaxBundleSize: maxBundleSize,
|
||||
ReserveFee: reserveFee,
|
||||
EscrowAccountAddress: escrowAddress.String(),
|
||||
@ -191,7 +190,7 @@ func (suite *KeeperTestSuite) TestValidateBidInfo() {
|
||||
signers[index] = txSigners
|
||||
}
|
||||
|
||||
bidInfo := &mempool.AuctionBidInfo{
|
||||
bidInfo := &types.BidInfo{
|
||||
Bidder: bidder.Address,
|
||||
Bid: bid,
|
||||
Transactions: bundle,
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/skip-mev/pob/mempool"
|
||||
testutils "github.com/skip-mev/pob/testutils"
|
||||
"github.com/skip-mev/pob/x/builder/keeper"
|
||||
"github.com/skip-mev/pob/x/builder/types"
|
||||
@ -28,8 +27,6 @@ type KeeperTestSuite struct {
|
||||
msgServer types.MsgServer
|
||||
key *storetypes.KVStoreKey
|
||||
authorityAccount sdk.AccAddress
|
||||
|
||||
mempool *mempool.AuctionMempool
|
||||
}
|
||||
|
||||
func TestKeeperTestSuite(t *testing.T) {
|
||||
@ -64,7 +61,5 @@ func (suite *KeeperTestSuite) SetupTest() {
|
||||
err := suite.builderKeeper.SetParams(suite.ctx, types.DefaultParams())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
config := mempool.NewDefaultAuctionFactory(suite.encCfg.TxConfig.TxDecoder())
|
||||
suite.mempool = mempool.NewAuctionMempool(suite.encCfg.TxConfig.TxDecoder(), suite.encCfg.TxConfig.TxEncoder(), 0, config)
|
||||
suite.msgServer = keeper.NewMsgServerImpl(suite.builderKeeper)
|
||||
}
|
||||
|
||||
12
x/builder/types/bid_info.go
Normal file
12
x/builder/types/bid_info.go
Normal file
@ -0,0 +1,12 @@
|
||||
package types
|
||||
|
||||
import sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
// BidInfo defines the information about a bid to the auction house.
|
||||
type BidInfo struct {
|
||||
Bidder sdk.AccAddress
|
||||
Bid sdk.Coin
|
||||
Transactions [][]byte
|
||||
Timeout uint64
|
||||
Signers []map[string]struct{}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user