fix: CheckTx Handler (#141)

This commit is contained in:
David Terpay 2023-05-23 18:09:37 -04:00 committed by GitHub
parent af94805b48
commit 648a2f88d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1362 additions and 503 deletions

View File

@ -90,8 +90,12 @@ $ go install github.com/skip-mev/pob
```go
type App struct {
BuilderKeeper builderkeeper.Keeper
...
// BuilderKeeper is the keeper that handles processing auction transactions
BuilderKeeper builderkeeper.Keeper
// Custom checkTx handler
checkTxHandler abci.CheckTx
}
```
@ -133,7 +137,7 @@ $ go install github.com/skip-mev/pob
```
d. Searchers bid to have their bundles executed at the top of the block
using `MsgAuctionBid` messages. While the builder `Keeper` is capable of
using `MsgAuctionBid` messages (by default). While the builder `Keeper` is capable of
tracking valid bids, it is unable to correctly sequence the auction
transactions alongside the normal transactions without having access to the
applications mempool. As such, we have to instantiate POBs custom
@ -141,8 +145,17 @@ $ go install github.com/skip-mev/pob
mempool - into the application. Note, this should be done after `BaseApp` is
instantiated.
d.1. Application developers can choose to implement their own `AuctionFactory` implementation
or use the default implementation provided by POB. The `AuctionFactory` is responsible
for determining what is an auction bid transaction and how to extract the bid information
from the transaction. The default implementation provided by POB is `DefaultAuctionFactory`
which uses the `MsgAuctionBid` message to determine if a transaction is an auction bid
transaction and extracts the bid information from the message.
```go
mempool := mempool.NewAuctionMempool(txConfig.TxDecoder(), GetMaxMempoolSize())
config := mempool.NewDefaultAuctionFactory(txDecoder)
mempool := mempool.NewAuctionMempool(txDecoder, txEncoder, maxTx, config)
bApp.SetMempool(mempool)
```
@ -153,18 +166,68 @@ $ go install github.com/skip-mev/pob
will verify the contents of the block proposal by all validators. The
combination of the `AuctionMempool`, `PrepareProposal` and `ProcessProposal`
handlers allows the application to verifiably build valid blocks with
top-of-block block space reserved for auctions.
top-of-block block space reserved for auctions. Additionally, we override the
`BaseApp`'s `CheckTx` handler with our own custom `CheckTx` handler that will
be responsible for checking the validity of transactions. We override the
`CheckTx` handler so that we can verify auction transactions before they are
inserted into the mempool. With the POB `CheckTx`, we can verify the auction
transaction and all of the bundled transactions before inserting the auction
transaction into the mempool. This is important because we otherwise there may be
discrepencies between the auction transaction and the bundled transactions
are validated in `CheckTx` and `PrepareProposal` such that the auction can be
griefed. All other transactions will be executed with base app's `CheckTx`.
```go
// Create the entire chain of AnteDecorators for the application.
anteDecorators := []sdk.AnteDecorator{
auction.NewAuctionDecorator(
app.BuilderKeeper,
txConfig.TxEncoder(),
mempool,
),
...,
}
// Create the antehandler that will be used to check transactions throughout the lifecycle
// of the application.
anteHandler := sdk.ChainAnteDecorators(anteDecorators...)
app.SetAnteHandler(anteHandler)
// Create the proposal handler that will be used to build and validate blocks.
handler := proposalhandler.NewProposalHandler(
mempool,
bApp.Logger(),
bApp,
anteHandler,
txConfig.TxEncoder(),
txConfig.TxDecoder(),
)
bApp.SetPrepareProposal(handler.PrepareProposalHandler())
bApp.SetProcessProposal(handler.ProcessProposalHandler())
app.SetPrepareProposal(handler.PrepareProposalHandler())
app.SetProcessProposal(handler.ProcessProposalHandler())
// Set the custom CheckTx handler on BaseApp.
checkTxHandler := pobabci.CheckTxHandler(
app.App,
app.TxDecoder,
mempool,
anteHandler,
chainID,
)
app.SetCheckTx(checkTxHandler)
...
// CheckTx will check the transaction with the provided checkTxHandler. We override the default
// handler so that we can verify bid transactions before they are inserted into the mempool.
// With the POB CheckTx, we can verify the bid transaction and all of the bundled transactions
// before inserting the bid transaction into the mempool.
func (app *TestApp) CheckTx(req cometabci.RequestCheckTx) cometabci.ResponseCheckTx {
return app.checkTxHandler(req)
}
// SetCheckTx sets the checkTxHandler for the app.
func (app *TestApp) SetCheckTx(handler abci.CheckTx) {
app.checkTxHandler = handler
}
```
f. Finally, update the app's `InitGenesis` order and ante-handler chain.
@ -174,16 +237,6 @@ $ go install github.com/skip-mev/pob
buildertypes.ModuleName,
...,
}
anteDecorators := []sdk.AnteDecorator{
auction.NewAuctionDecorator(
app.BuilderKeeper,
txConfig.TxDecoder(),
txConfig.TxEncoder(),
mempool,
),
...,
}
```
## Params

View File

@ -97,7 +97,7 @@ func (suite *ABCITestSuite) SetupTest() {
)
err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams())
suite.Require().NoError(err)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// Accounts set up
suite.accounts = testutils.RandomAccounts(suite.random, 1)
@ -487,7 +487,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
MinBidIncrement: suite.minBidIncrement,
}
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
handler := suite.proposalHandler.PrepareProposalHandler()
res := handler(suite.ctx, abcitypes.RequestPrepareProposal{
@ -733,7 +733,7 @@ func (suite *ABCITestSuite) TestProcessProposal() {
MinBidIncrement: suite.minBidIncrement,
}
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid())
txs := suite.exportMempool(exportRefTxs)

214
abci/check_tx.go Normal file
View File

@ -0,0 +1,214 @@
package abci
import (
"context"
"fmt"
cometabci "github.com/cometbft/cometbft/abci/types"
log "github.com/cometbft/cometbft/libs/log"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/skip-mev/pob/mempool"
)
type (
// CheckTxHandler is a wrapper around baseapp's CheckTx method that allows us to
// verify bid transactions against the latest committed state. All other transactions
// are executed normally using base app's CheckTx. This defines all of the
// dependencies that are required to verify a bid transaction.
CheckTxHandler struct {
// baseApp is utilized to retrieve the latest committed state and to call
// baseapp's CheckTx method.
baseApp BaseApp
// txDecoder is utilized to decode transactions to determine if they are
// bid transactions.
txDecoder sdk.TxDecoder
// mempool is utilized to retrieve the bid info of a transaction and to
// insert a transaction into the application-side mempool.
mempool CheckTxMempool
// anteHandler is utilized to verify the bid transaction against the latest
// committed state.
anteHandler sdk.AnteHandler
// chainID is the chain ID of the blockchain.
chainID string
}
// CheckTx is baseapp's CheckTx method that checks the validity of a
// transaction.
CheckTx func(cometabci.RequestCheckTx) cometabci.ResponseCheckTx
// CheckTxMempool is the interface that defines all of the dependencies that
// are required to interact with the application-side mempool.
CheckTxMempool interface {
// GetAuctionBidInfo is utilized to retrieve the bid info of a transaction.
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
// Insert is utilized to insert a transaction into the application-side mempool.
Insert(ctx context.Context, tx sdk.Tx) error
// WrapBundleTransaction is utilized to wrap a transaction included in a bid transaction
// into an sdk.Tx.
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
}
// BaseApp is an interface that allows us to call baseapp's CheckTx method
// as well as retrieve the latest committed state.
BaseApp interface {
// CommitMultiStore is utilized to retrieve the latest committed state.
CommitMultiStore() sdk.CommitMultiStore
// CheckTx is baseapp's CheckTx method that checks the validity of a
// transaction.
CheckTx(cometabci.RequestCheckTx) cometabci.ResponseCheckTx
// Logger is utilized to log errors.
Logger() log.Logger
// LastBlockHeight is utilized to retrieve the latest block height.
LastBlockHeight() int64
// GetConsensusParams is utilized to retrieve the consensus params.
GetConsensusParams(ctx sdk.Context) *tmproto.ConsensusParams
}
)
// NewCheckTxHandler is a constructor for CheckTxHandler.
func NewCheckTxHandler(baseApp BaseApp, txDecoder sdk.TxDecoder, mempool CheckTxMempool, anteHandler sdk.AnteHandler, chainID string) *CheckTxHandler {
return &CheckTxHandler{
baseApp: baseApp,
txDecoder: txDecoder,
mempool: mempool,
anteHandler: anteHandler,
chainID: chainID,
}
}
// CheckTxHandler is a wrapper around baseapp's CheckTx method that allows us to
// verify bid transactions against the latest committed state. All other transactions
// are executed normally. We must verify each bid tx and all of its bundled transactions
// before we can insert it into the mempool against the latest commit state because
// otherwise the auction can be griefed. No state changes are applied to the state
// during this process.
func (handler *CheckTxHandler) CheckTx() CheckTx {
return func(req cometabci.RequestCheckTx) (resp cometabci.ResponseCheckTx) {
defer func() {
if err := recover(); err != nil {
resp = sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("panic in check tx handler: %s", err), 0, 0, nil, false)
}
}()
tx, err := handler.txDecoder(req.Tx)
if err != nil {
return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to decode tx: %w", err), 0, 0, nil, false)
}
// Attempt to get the bid info of the transaction.
bidInfo, err := handler.mempool.GetAuctionBidInfo(tx)
if err != nil {
return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("failed to get auction bid info: %w", err), 0, 0, nil, false)
}
// If this is not a bid transaction, we just execute it normally.
if bidInfo == nil {
return handler.baseApp.CheckTx(req)
}
// We attempt to get the latest committed state in order to verify transactions
// as if they were to be executed at the top of the block. After verification, this
// context will be discarded and will not apply any state changes.
ctx := handler.GetContextForBidTx(req)
// Verify the bid transaction.
gasInfo, err := handler.ValidateBidTx(ctx, tx, bidInfo)
if err != nil {
return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("invalid bid tx: %w", err), gasInfo.GasWanted, gasInfo.GasUsed, nil, false)
}
// If the bid transaction is valid, we know we can insert it into the mempool for consideration in the next block.
if err := handler.mempool.Insert(ctx, tx); err != nil {
return sdkerrors.ResponseCheckTxWithEvents(fmt.Errorf("invalid bid tx; failed to insert bid transaction into mempool: %w", err), gasInfo.GasWanted, gasInfo.GasUsed, nil, false)
}
return cometabci.ResponseCheckTx{
Code: cometabci.CodeTypeOK,
GasWanted: int64(gasInfo.GasWanted),
GasUsed: int64(gasInfo.GasUsed),
}
}
}
// ValidateBidTx is utilized to verify the bid transaction against the latest committed state.
func (handler *CheckTxHandler) ValidateBidTx(ctx sdk.Context, bidTx sdk.Tx, bidInfo *mempool.AuctionBidInfo) (sdk.GasInfo, error) {
// Verify the bid transaction.
ctx, err := handler.anteHandler(ctx, bidTx, false)
if err != nil {
return sdk.GasInfo{}, fmt.Errorf("invalid bid tx; failed to execute ante handler: %w", err)
}
// Store the gas info and priority of the bid transaction before applying changes with other transactions.
gasInfo := sdk.GasInfo{
GasWanted: ctx.GasMeter().Limit(),
GasUsed: ctx.GasMeter().GasConsumed(),
}
// Verify all of the bundled transactions.
for _, tx := range bidInfo.Transactions {
bundledTx, err := handler.mempool.WrapBundleTransaction(tx)
if err != nil {
return gasInfo, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err)
}
bidInfo, err := handler.mempool.GetAuctionBidInfo(bundledTx)
if err != nil {
return gasInfo, fmt.Errorf("invalid bid tx; failed to get auction bid info: %w", err)
}
// Bid txs cannot be included in bundled txs.
if bidInfo != nil {
return gasInfo, fmt.Errorf("invalid bid tx; bundled tx cannot be a bid tx")
}
if ctx, err = handler.anteHandler(ctx, bundledTx, false); err != nil {
return gasInfo, fmt.Errorf("invalid bid tx; failed to execute bundled transaction: %w", err)
}
}
return gasInfo, nil
}
// GetContextForBidTx is returns the latest committed state and sets the context given
// the checkTx request.
func (handler *CheckTxHandler) GetContextForBidTx(req cometabci.RequestCheckTx) sdk.Context {
// Retrieve the commit multi-store which is used to retrieve the latest committed state.
ms := handler.baseApp.CommitMultiStore().CacheMultiStore()
// Create a new context based off of the latest committed state.
header := tmproto.Header{
Height: handler.baseApp.LastBlockHeight(),
ChainID: handler.chainID, // TODO: Replace with actual chain ID. This is currently not exposed by the app.
}
ctx, _ := sdk.NewContext(ms, header, true, handler.baseApp.Logger()).CacheContext()
// Set the context to the correct checking mode.
switch req.Type {
case cometabci.CheckTxType_New:
ctx = ctx.WithIsCheckTx(true)
case cometabci.CheckTxType_Recheck:
ctx = ctx.WithIsReCheckTx(true)
default:
panic("unknown check tx type")
}
// Set the remaining important context values.
ctx = ctx.
WithTxBytes(req.Tx).
WithEventManager(sdk.NewEventManager()).
WithConsensusParams(handler.baseApp.GetConsensusParams(ctx))
return ctx
}

View File

@ -95,7 +95,7 @@ func (suite *ABCITestSuite) SetupTest() {
)
err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams())
suite.Require().NoError(err)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// Accounts set up
suite.accounts = testutils.RandomAccounts(suite.random, 10)

View File

@ -296,7 +296,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
MinBidIncrement: suite.minBidIncrement,
}
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// Reset the proposal handler with the new mempool.
suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())
@ -749,7 +749,7 @@ func (suite *ABCITestSuite) TestProcessProposal() {
MinBidIncrement: suite.minBidIncrement,
}
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// reset the proposal handler with the new mempool
suite.proposalHandler = v2.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())

View File

@ -48,7 +48,7 @@ func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler {
ante.NewSigGasConsumeDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SigGasConsumer),
ante.NewSigVerificationDecorator(options.BaseOptions.AccountKeeper, options.BaseOptions.SignModeHandler),
ante.NewIncrementSequenceDecorator(options.BaseOptions.AccountKeeper),
builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxDecoder, options.TxEncoder, options.Mempool),
builderante.NewBuilderDecorator(options.BuilderKeeper, options.TxEncoder, options.Mempool),
}
return sdk.ChainAnteDecorators(anteDecorators...)

View File

@ -9,6 +9,7 @@ import (
"cosmossdk.io/depinject"
dbm "github.com/cometbft/cometbft-db"
cometabci "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/libs/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
@ -72,6 +73,10 @@ import (
builderkeeper "github.com/skip-mev/pob/x/builder/keeper"
)
const (
ChainID = "chain-id-0"
)
var (
BondDenom = sdk.DefaultBondDenom
@ -142,6 +147,9 @@ type TestApp struct {
GroupKeeper groupkeeper.Keeper
ConsensusParamsKeeper consensuskeeper.Keeper
BuilderKeeper builderkeeper.Keeper
// custom checkTx handler
checkTxHandler abci.CheckTx
}
func init() {
@ -275,7 +283,7 @@ func New(
}
anteHandler := NewPOBAnteHandler(options)
// Set the proposal handlers on the BaseApp.
// Set the proposal handlers on the BaseApp along with the custom antehandler.
proposalHandlers := abci.NewProposalHandler(
mempool,
app.App.Logger(),
@ -285,6 +293,17 @@ func New(
)
app.App.SetPrepareProposal(proposalHandlers.PrepareProposalHandler())
app.App.SetProcessProposal(proposalHandlers.ProcessProposalHandler())
app.App.SetAnteHandler(anteHandler)
// Set the custom CheckTx handler on BaseApp.
checkTxHandler := abci.NewCheckTxHandler(
app.App,
app.txConfig.TxDecoder(),
mempool,
anteHandler,
ChainID,
)
app.SetCheckTx(checkTxHandler.CheckTx())
// load state streaming if enabled
if _, _, err := streaming.LoadStreamingServices(app.App.BaseApp, appOpts, app.appCodec, logger, app.kvStoreKeys()); err != nil {
@ -320,6 +339,19 @@ func New(
return app
}
// CheckTx will check the transaction with the provided checkTxHandler. We override the default
// handler so that we can verify bid transactions before they are inserted into the mempool.
// With the POB CheckTx, we can verify the bid transaction and all of the bundled transactions
// before inserting the bid transaction into the mempool.
func (app *TestApp) CheckTx(req cometabci.RequestCheckTx) cometabci.ResponseCheckTx {
return app.checkTxHandler(req)
}
// SetCheckTx sets the checkTxHandler for the app.
func (app *TestApp) SetCheckTx(handler abci.CheckTx) {
app.checkTxHandler = handler
}
// Name returns the name of the App
func (app *TestApp) Name() string { return app.BaseApp.Name() }

View File

@ -6,7 +6,6 @@ import (
dbm "github.com/cometbft/cometbft-db"
"github.com/cometbft/cometbft/libs/log"
cometrand "github.com/cometbft/cometbft/libs/rand"
"github.com/cosmos/cosmos-sdk/codec"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/skip-mev/pob/tests/app"
@ -47,7 +46,7 @@ func newChain() (*chain, error) {
}
return &chain{
id: "chain-" + cometrand.NewRand().Str(6),
id: app.ChainID,
dataDir: tmpDir,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ package e2e
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"time"
@ -16,68 +15,9 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ory/dockertest/v3/docker"
"github.com/skip-mev/pob/tests/app"
buildertypes "github.com/skip-mev/pob/x/builder/types"
)
// execAuctionBidTx executes an auction bid transaction on the given validator given the provided
// bid, timeout, and bundle. This function returns the transaction hash. It does not wait for the
// transaction to be committed.
func (s *IntegrationTestSuite) execAuctionBidTx(valIdx int, bid sdk.Coin, timeout int64, bundle []string) string {
address, err := s.chain.validators[valIdx].keyInfo.GetAddress()
s.Require().NoError(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{
Context: ctx,
AttachStdout: true,
AttachStderr: true,
Container: s.valResources[valIdx].Container.ID,
User: "root",
Cmd: []string{
"testappd",
"tx",
"builder",
"auction-bid",
address.String(), // bidder
bid.String(), // bid
strings.Join(bundle, ","), // bundle
fmt.Sprintf("--%s=%d", flags.FlagTimeoutHeight, timeout), // timeout
fmt.Sprintf("--%s=%s", flags.FlagFrom, s.chain.validators[valIdx].keyInfo.Name),
fmt.Sprintf("--%s=%s", flags.FlagChainID, s.chain.id),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoin(app.BondDenom, sdk.NewInt(1000000000)).String()),
"--keyring-backend=test",
"--broadcast-mode=sync",
"-y",
},
})
s.Require().NoError(err)
var (
outBuf bytes.Buffer
errBuf bytes.Buffer
)
err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{
Context: ctx,
Detach: false,
OutputStream: &outBuf,
ErrorStream: &errBuf,
})
s.Require().NoErrorf(err, "stdout: %s, stderr: %s", outBuf.String(), errBuf.String())
output := outBuf.String()
resp := strings.Split(output, ":")
txHash := strings.TrimSpace(resp[len(resp)-1])
s.T().Logf(
"broadcasted bid tx %s with bid %s timeout %d and %d bundled txs",
txHash, bid, timeout, len(bundle),
)
return txHash
}
// execMsgSendTx executes a send transaction on the given validator given the provided
// recipient and amount. This function returns the transaction hash. It does not wait for the
// transaction to be committed.
@ -136,12 +76,22 @@ func (s *IntegrationTestSuite) execMsgSendTx(valIdx int, to sdk.AccAddress, amou
return txHash
}
// createAuctionBidTx creates a transaction that bids on an auction given the provided bidder, bid, and transactions.
func (s *IntegrationTestSuite) createAuctionBidTx(account TestAccount, bid sdk.Coin, transactions [][]byte, sequenceOffset, height uint64) []byte {
msgs := []sdk.Msg{
&buildertypes.MsgAuctionBid{
Bidder: account.Address.String(),
Bid: bid,
Transactions: transactions,
},
}
return s.createTx(account, msgs, sequenceOffset, height)
}
// createMsgSendTx creates a send transaction given the provided signer, recipient, amount, sequence number offset, and block height timeout.
// This function is primarily used to create bundles of transactions.
func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress string, amount sdk.Coins, sequenceOffset, height int) string {
txConfig := encodingConfig.TxConfig
txBuilder := txConfig.NewTxBuilder()
func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress string, amount sdk.Coins, sequenceOffset, height uint64) []byte {
msgs := []sdk.Msg{
&banktypes.MsgSend{
FromAddress: account.Address.String(),
@ -150,15 +100,23 @@ func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress st
},
}
return s.createTx(account, msgs, sequenceOffset, height)
}
// createTx creates a transaction given the provided messages, sequence number offset, and block height timeout.
func (s *IntegrationTestSuite) createTx(account TestAccount, msgs []sdk.Msg, sequenceOffset, height uint64) []byte {
txConfig := encodingConfig.TxConfig
txBuilder := txConfig.NewTxBuilder()
// Get account info of the sender to set the account number and sequence number
baseAccount := s.queryAccount(account.Address)
sequenceNumber := baseAccount.Sequence + uint64(sequenceOffset)
sequenceNumber := baseAccount.Sequence + sequenceOffset
// Set the messages, fees, and timeout.
txBuilder.SetMsgs(msgs...)
txBuilder.SetGasLimit(5000000)
txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(75000))))
txBuilder.SetTimeoutHeight(uint64(height))
txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(150000))))
txBuilder.SetTimeoutHeight(height)
sigV2 := signing.SignatureV2{
PubKey: account.PrivateKey.PubKey(),
@ -191,8 +149,5 @@ func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress st
bz, err := txConfig.TxEncoder()(txBuilder.GetTx())
s.Require().NoError(err)
// Hex encode the transaction
hash := hex.EncodeToString(bz)
return hash
return bz
}

View File

@ -116,15 +116,29 @@ func (s *IntegrationTestSuite) waitForABlock() {
}
// bundleToTxHashes converts a bundle to a slice of transaction hashes.
func (s *IntegrationTestSuite) bundleToTxHashes(bundle []string) []string {
hashes := make([]string, len(bundle))
func (s *IntegrationTestSuite) bundleToTxHashes(bidTx []byte, bundle [][]byte) []string {
hashes := make([]string, len(bundle)+1)
for i, tx := range bundle {
hashBz, err := hex.DecodeString(tx)
s.Require().NoError(err)
// encode the bid transaction into a hash
hashBz := sha256.Sum256(bidTx)
hash := hex.EncodeToString(hashBz[:])
hashes[0] = hash
shaBz := sha256.Sum256(hashBz)
hashes[i] = hex.EncodeToString(shaBz[:])
for i, hash := range s.normalTxsToTxHashes(bundle) {
hashes[i+1] = hash
}
return hashes
}
// normalTxsToTxHashes converts a slice of normal transactions to a slice of transaction hashes.
func (s *IntegrationTestSuite) normalTxsToTxHashes(txs [][]byte) []string {
hashes := make([]string, len(txs))
for i, tx := range txs {
hashBz := sha256.Sum256(tx)
hash := hex.EncodeToString(hashBz[:])
hashes[i] = hash
}
return hashes
@ -132,13 +146,13 @@ func (s *IntegrationTestSuite) bundleToTxHashes(bundle []string) []string {
// verifyBlock verifies that the transactions in the block at the given height were seen
// and executed in the order they were submitted i.e. how they are broadcasted in the bundle.
func (s *IntegrationTestSuite) verifyBlock(height int64, bidTx string, bundle []string, expectedExecution map[string]bool) {
func (s *IntegrationTestSuite) verifyBlock(height uint64, bundle []string, expectedExecution map[string]bool) {
s.waitForABlock()
s.T().Logf("Verifying block %d", height)
// Get the block's transactions and display the expected and actual block for debugging.
txs := s.queryBlockTxs(height)
s.displayBlock(txs, bidTx, bundle)
s.displayBlock(txs, bundle)
// Ensure that all transactions executed as expected (i.e. landed or failed to land).
for tx, landed := range expectedExecution {
@ -149,28 +163,32 @@ func (s *IntegrationTestSuite) verifyBlock(height int64, bidTx string, bundle []
// Check that the block contains the expected transactions in the expected order
// iff the bid transaction was expected to execute.
if expectedExecution[bidTx] {
hashBz := sha256.Sum256(txs[0])
hash := hex.EncodeToString(hashBz[:])
s.Require().Equal(strings.ToUpper(bidTx), strings.ToUpper(hash))
if len(bundle) > 0 && expectedExecution[bundle[0]] {
if expectedExecution[bundle[0]] {
hashBz := sha256.Sum256(txs[0])
hash := hex.EncodeToString(hashBz[:])
s.Require().Equal(strings.ToUpper(bundle[0]), strings.ToUpper(hash))
for index, bundleTx := range bundle {
hashBz := sha256.Sum256(txs[index+1])
txHash := hex.EncodeToString(hashBz[:])
for index, bundleTx := range bundle[1:] {
hashBz := sha256.Sum256(txs[index+1])
txHash := hex.EncodeToString(hashBz[:])
s.Require().Equal(strings.ToUpper(bundleTx), strings.ToUpper(txHash))
s.Require().Equal(strings.ToUpper(bundleTx), strings.ToUpper(txHash))
}
}
}
}
// displayExpectedBlock displays the expected and actual blocks.
func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bidTx string, bundle []string) {
expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, bidTx)
for index, bundleTx := range bundle {
expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx)
}
func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bundle []string) {
if len(bundle) != 0 {
expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, bundle[0])
for index, bundleTx := range bundle[1:] {
expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx)
}
s.T().Logf(expectedBlock)
s.T().Logf(expectedBlock)
}
// Display the actual block.
if len(txs) == 0 {
@ -192,15 +210,35 @@ func (s *IntegrationTestSuite) displayBlock(txs [][]byte, bidTx string, bundle [
}
// displayExpectedBundle displays the expected order of the bid and bundled transactions.
func (s *IntegrationTestSuite) displayExpectedBundle(prefix, bidTx string, bundle []string) {
expectedBundle := fmt.Sprintf("%s expected bundle:\n\t(%d, %s)\n", prefix, 0, bidTx)
for index, bundleTx := range s.bundleToTxHashes(bundle) {
func (s *IntegrationTestSuite) displayExpectedBundle(prefix string, bidTx []byte, bundle [][]byte) {
// encode the bid transaction into a hash
hashes := s.bundleToTxHashes(bidTx, bundle)
expectedBundle := fmt.Sprintf("%s expected bundle:\n\t(%d, %s)\n", prefix, 0, hashes[0])
for index, bundleTx := range hashes[1:] {
expectedBundle += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx)
}
s.T().Logf(expectedBundle)
}
// broadcastTx broadcasts a transaction to the network using the given validator.
func (s *IntegrationTestSuite) broadcastTx(tx []byte, valIdx int) {
node := s.valResources[valIdx]
gRPCURI := node.GetHostPort("9090/tcp")
grpcConn, err := grpc.Dial(
gRPCURI,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
client := txtypes.NewServiceClient(grpcConn)
req := &txtypes.BroadcastTxRequest{TxBytes: tx, Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC}
_, err = client.BroadcastTx(context.Background(), req)
s.Require().NoError(err)
}
// queryTx queries a transaction by its hash and returns whether there was an
// error in including the transaction in a block.
func (s *IntegrationTestSuite) queryTxPassed(txHash string) error {
@ -268,21 +306,21 @@ func (s *IntegrationTestSuite) queryAccount(address sdk.AccAddress) *authtypes.B
}
// queryCurrentHeight returns the current block height.
func (s *IntegrationTestSuite) queryCurrentHeight() int64 {
func (s *IntegrationTestSuite) queryCurrentHeight() uint64 {
queryClient := tmclient.NewServiceClient(s.createClientContext())
req := &tmclient.GetLatestBlockRequest{}
resp, err := queryClient.GetLatestBlock(context.Background(), req)
s.Require().NoError(err)
return resp.SdkBlock.Header.Height
return uint64(resp.SdkBlock.Header.Height)
}
// queryBlockTxs returns the txs of the block at the given height.
func (s *IntegrationTestSuite) queryBlockTxs(height int64) [][]byte {
func (s *IntegrationTestSuite) queryBlockTxs(height uint64) [][]byte {
queryClient := tmclient.NewServiceClient(s.createClientContext())
req := &tmclient.GetBlockByHeightRequest{Height: height}
req := &tmclient.GetBlockByHeightRequest{Height: int64(height)}
resp, err := queryClient.GetBlockByHeight(context.Background(), req)
s.Require().NoError(err)

View File

@ -14,24 +14,24 @@ import (
var _ sdk.AnteDecorator = BuilderDecorator{}
type (
// Mempool is an interface that defines the methods required to interact with the application-side mempool.
Mempool interface {
Contains(tx sdk.Tx) (bool, error)
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
GetTopAuctionTx(ctx context.Context) sdk.Tx
}
// BuilderDecorator is an AnteDecorator that validates the auction bid and bundled transactions.
BuilderDecorator struct {
builderKeeper keeper.Keeper
txDecoder sdk.TxDecoder
txEncoder sdk.TxEncoder
mempool Mempool
}
)
func NewBuilderDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator {
func NewBuilderDecorator(ak keeper.Keeper, txEncoder sdk.TxEncoder, mempool Mempool) BuilderDecorator {
return BuilderDecorator{
builderKeeper: ak,
txDecoder: txDecoder,
txEncoder: txEncoder,
mempool: mempool,
}
@ -39,20 +39,20 @@ func NewBuilderDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, txEncoder sd
// AnteHandle validates that the auction bid is valid if one exists. If valid it will deduct the entrance fee from the
// bidder's account.
func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
func (bd BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
// If comet is re-checking a transaction, we only need to check if the transaction is in the application-side mempool.
if ctx.IsReCheckTx() {
contains, err := ad.mempool.Contains(tx)
contains, err := bd.mempool.Contains(tx)
if err != nil {
return ctx, err
}
if !contains {
return ctx, fmt.Errorf("transaction not found in application mempool")
return ctx, fmt.Errorf("transaction not found in application-side mempool")
}
}
bidInfo, err := ad.mempool.GetAuctionBidInfo(tx)
bidInfo, err := bd.mempool.GetAuctionBidInfo(tx)
if err != nil {
return ctx, err
}
@ -60,8 +60,8 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
// Validate the auction bid if one exists.
if bidInfo != nil {
// Auction transactions must have a timeout set to a valid block height.
if int64(bidInfo.Timeout) < ctx.BlockHeight() {
return ctx, fmt.Errorf("timeout height cannot be less than the current block height")
if err := bd.ValidateTimeout(ctx, int64(bidInfo.Timeout)); err != nil {
return ctx, err
}
// We only need to verify the auction bid relative to the local validator's mempool if the mode
@ -70,20 +70,20 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
// poor liveness guarantees.
topBid := sdk.Coin{}
if ctx.IsCheckTx() || ctx.IsReCheckTx() {
if topBidTx := ad.mempool.GetTopAuctionTx(ctx); topBidTx != nil {
topBidBz, err := ad.txEncoder(topBidTx)
if topBidTx := bd.mempool.GetTopAuctionTx(ctx); topBidTx != nil {
topBidBz, err := bd.txEncoder(topBidTx)
if err != nil {
return ctx, err
}
currentTxBz, err := ad.txEncoder(tx)
currentTxBz, err := bd.txEncoder(tx)
if err != nil {
return ctx, err
}
// Compare the bytes to see if the current transaction is the highest bidding transaction.
if !bytes.Equal(topBidBz, currentTxBz) {
topBidInfo, err := ad.mempool.GetAuctionBidInfo(topBidTx)
topBidInfo, err := bd.mempool.GetAuctionBidInfo(topBidTx)
if err != nil {
return ctx, err
}
@ -93,10 +93,31 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
}
}
if err := ad.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo); err != nil {
if err := bd.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo); err != nil {
return ctx, errors.Wrap(err, "failed to validate auction bid")
}
}
return next(ctx, tx, simulate)
}
// ValidateTimeout validates that the timeout is greater than or equal to the expected block height
// the bid transaction will be executed in.
//
// TODO: This will be deprecated in favor of the pre-commit hook once this available on the SDK
// https://github.com/skip-mev/pob/issues/147
func (bd BuilderDecorator) ValidateTimeout(ctx sdk.Context, timeout int64) error {
currentBlockHeight := ctx.BlockHeight()
// If the mode is CheckTx or ReCheckTx, we increment the current block height by one to
// account for the fact that the transaction will be executed in the next block.
if ctx.IsCheckTx() || ctx.IsReCheckTx() {
currentBlockHeight++
}
if timeout < currentBlockHeight {
return fmt.Errorf("timeout height cannot be less than the current block height")
}
return nil
}

View File

@ -248,7 +248,7 @@ func (suite *AnteTestSuite) TestAnteHandler() {
suite.Require().NoError(err)
// Execute the ante handler
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), mempool)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), mempool)
_, err = suite.executeAnteHandler(auctionTx, balance)
if tc.pass {
suite.Require().NoError(err)