fix(Config): Simplifying config interface (#104)

This commit is contained in:
David Terpay 2023-05-04 15:33:18 -04:00 committed by GitHub
parent 07c76b8330
commit d58b36bf3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 345 additions and 564 deletions

View File

@ -32,7 +32,7 @@ type ABCITestSuite struct {
encodingConfig testutils.EncodingConfig
proposalHandler *abci.ProposalHandler
voteExtensionHandler *abci.VoteExtensionHandler
config mempool.Config
config mempool.AuctionFactory
txs map[string]struct{}
// auction bid setup
@ -66,10 +66,10 @@ func (suite *ABCITestSuite) SetupTest() {
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
suite.ctx = testCtx.Ctx.WithBlockHeight(1)
// Mempool set up
suite.config = mempool.NewDefaultConfig(suite.encodingConfig.TxConfig.TxDecoder())
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))

View File

@ -12,6 +12,7 @@ import (
"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 (
@ -19,10 +20,14 @@ type (
// 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
GetBundledTransactions(tx sdk.Tx) ([][]byte, error)
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
IsAuctionTx(tx sdk.Tx) (bool, error)
}
// ProposalHandler contains the functionality and handlers required to\
@ -82,7 +87,7 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
bidTxSize := int64(len(bidTxBz))
if bidTxSize <= req.MaxTxBytes {
bundledTransactions, err := h.mempool.GetBundledTransactions(tmpBidTx)
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.
@ -91,6 +96,7 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
}
// 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
@ -205,23 +211,21 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
isAuctionTx, err := h.mempool.IsAuctionTx(tx)
bidInfo, err := h.mempool.GetAuctionBidInfo(tx)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
if isAuctionTx {
// Only the first transaction can be an auction bid 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 bidInfo != nil {
if index != 0 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
bundledTransactions, err := h.mempool.GetBundledTransactions(tx)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
// The order of transactions in the block proposal must follow the order of transactions in the bid.
bundledTransactions := bidInfo.Transactions
if len(req.Txs) < len(bundledTransactions)+1 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}

View File

@ -8,6 +8,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/skip-mev/pob/mempool"
)
type (
@ -16,8 +17,7 @@ type (
VoteExtensionMempool interface {
Remove(tx sdk.Tx) error
AuctionBidSelect(ctx context.Context) sdkmempool.Iterator
IsAuctionTx(tx sdk.Tx) (bool, error)
GetBundledTransactions(tx sdk.Tx) ([][]byte, error)
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
}
@ -140,12 +140,12 @@ func (h *VoteExtensionHandler) resetCache(blockHeight int64) {
// verifyAuctionTx verifies a transaction against the application's state.
func (h *VoteExtensionHandler) verifyAuctionTx(ctx sdk.Context, bidTx sdk.Tx) error {
// Verify the vote extension is a auction transaction
isAuctionTx, err := h.mempool.IsAuctionTx(bidTx)
bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx)
if err != nil {
return err
}
if !isAuctionTx {
if bidInfo == nil {
return fmt.Errorf("vote extension is not a valid auction transaction")
}
@ -159,13 +159,8 @@ func (h *VoteExtensionHandler) verifyAuctionTx(ctx sdk.Context, bidTx sdk.Tx) er
return err
}
bundledTxs, err := h.mempool.GetBundledTransactions(bidTx)
if err != nil {
return err
}
// Verify all bundled transactions
for _, tx := range bundledTxs {
for _, tx := range bidInfo.Transactions {
wrappedTx, err := h.mempool.WrapBundleTransaction(tx)
if err != nil {
return err

View File

@ -79,31 +79,6 @@ func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
return nil
},
},
{
"mempool contains bid that has bundled txs that are invalid",
func() []byte {
params.ReserveFee = sdk.NewCoin("foo", sdk.NewInt(10))
err := suite.builderKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)
bidder := suite.accounts[0]
bid := params.ReserveFee.Sub(sdk.NewCoin("foo", sdk.NewInt(1)))
msgAuctionBid, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, bidder, bid, 0, 0)
suite.Require().NoError(err)
msgAuctionBid.Transactions = [][]byte{[]byte("invalid tx")}
bidTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, bidder, 0, 10, []sdk.Msg{msgAuctionBid})
suite.Require().NoError(err)
suite.mempool = mempool.NewAuctionMempool(suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), 0, suite.config)
err = suite.mempool.Insert(suite.ctx, bidTx)
suite.Require().NoError(err)
// this should return nothing since the top bid is not valid
return nil
},
},
{
"mempool contains bid that has an invalid timeout",
func() []byte {
@ -126,6 +101,10 @@ 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)
bidder := suite.accounts[0]
bid := suite.auctionBidAmount.Add(suite.minBidIncrement)
signers := []testutils.Account{bidder}

128
mempool/auction_bid.go Normal file
View File

@ -0,0 +1,128 @@
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
}

View File

@ -1,6 +1,8 @@
package mempool_test
import (
"crypto/rand"
sdk "github.com/cosmos/cosmos-sdk/types"
testutils "github.com/skip-mev/pob/testutils"
)
@ -77,9 +79,9 @@ func (suite *IntegrationTestSuite) TestIsAuctionTx() {
suite.Run(tc.name, func() {
tx := tc.createTx()
isAuctionTx, err := suite.config.IsAuctionTx(tx)
bidInfo, err := suite.config.GetAuctionBidInfo(tx)
suite.Require().Equal(tc.isAuctionTx, isAuctionTx)
suite.Require().Equal(tc.isAuctionTx, bidInfo != nil)
if tc.expectedError {
suite.Require().Error(err)
} else {
@ -92,47 +94,70 @@ func (suite *IntegrationTestSuite) TestIsAuctionTx() {
func (suite *IntegrationTestSuite) TestGetTransactionSigners() {
testCases := []struct {
name string
createTx func() []byte
expectedSigners []string
createTx func() sdk.Tx
expectedSigners []map[string]struct{}
expectedError bool
}{
{
"normal sdk tx",
func() []byte {
tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0)
"normal auction tx",
func() sdk.Tx {
tx, err := testutils.CreateAuctionTxWithSigners(
suite.encCfg.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
1,
0,
suite.accounts[0:1],
)
suite.Require().NoError(err)
bz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
return bz
return tx
},
[]string{suite.accounts[0].Address.String()},
[]map[string]struct{}{
{
suite.accounts[0].Address.String(): {},
},
},
false,
},
{
"normal sdk tx with several messages",
func() []byte {
"normal sdk tx",
func() sdk.Tx {
tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 10, 0)
suite.Require().NoError(err)
bz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
return bz
return tx
},
[]string{suite.accounts[0].Address.String()},
nil,
true,
},
{
"multiple signers on tx",
func() []byte {
tx, err := testutils.CreateTxWithSigners(suite.encCfg.TxConfig, 0, 0, suite.accounts[0:3])
"multiple signers on auction tx",
func() sdk.Tx {
tx, err := testutils.CreateAuctionTxWithSigners(
suite.encCfg.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
1,
0,
suite.accounts[0:3],
)
suite.Require().NoError(err)
bz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
return bz
return tx
},
[]string{suite.accounts[0].Address.String(), suite.accounts[1].Address.String(), suite.accounts[2].Address.String()},
[]map[string]struct{}{
{
suite.accounts[0].Address.String(): {},
},
{
suite.accounts[1].Address.String(): {},
},
{
suite.accounts[2].Address.String(): {},
},
},
false,
},
}
@ -140,103 +165,11 @@ func (suite *IntegrationTestSuite) TestGetTransactionSigners() {
suite.Run(tc.name, func() {
tx := tc.createTx()
signers, err := suite.config.GetTransactionSigners(tx)
suite.Require().NoError(err)
suite.Require().Equal(len(tc.expectedSigners), len(signers))
for _, signer := range tc.expectedSigners {
suite.Require().Contains(signers, signer)
}
})
}
}
func (suite *IntegrationTestSuite) TestGetBundleSigners() {
testCases := []struct {
name string
createBundle func() [][]byte
expectedSigners [][]string
}{
{
"single bundle with one signer",
func() [][]byte {
tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0)
suite.Require().NoError(err)
bz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
return [][]byte{bz}
},
[][]string{{suite.accounts[0].Address.String()}},
},
{
"single bundle with multiple signers",
func() [][]byte {
tx, err := testutils.CreateTxWithSigners(suite.encCfg.TxConfig, 0, 0, suite.accounts[0:3])
suite.Require().NoError(err)
bz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
return [][]byte{bz}
},
[][]string{{suite.accounts[0].Address.String(), suite.accounts[1].Address.String(), suite.accounts[2].Address.String()}},
},
{
"multiple bundles with one signer",
func() [][]byte {
tx1, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0)
suite.Require().NoError(err)
bz1, err := suite.encCfg.TxConfig.TxEncoder()(tx1)
suite.Require().NoError(err)
tx2, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[1], 0, 1, 0)
suite.Require().NoError(err)
bz2, err := suite.encCfg.TxConfig.TxEncoder()(tx2)
suite.Require().NoError(err)
return [][]byte{bz1, bz2}
},
[][]string{{suite.accounts[0].Address.String()}, {suite.accounts[1].Address.String()}},
},
{
"multiple bundles with multiple signers",
func() [][]byte {
tx1, err := testutils.CreateTxWithSigners(suite.encCfg.TxConfig, 0, 0, suite.accounts[0:3])
suite.Require().NoError(err)
bz1, err := suite.encCfg.TxConfig.TxEncoder()(tx1)
suite.Require().NoError(err)
tx2, err := testutils.CreateTxWithSigners(suite.encCfg.TxConfig, 0, 0, suite.accounts[3:6])
suite.Require().NoError(err)
bz2, err := suite.encCfg.TxConfig.TxEncoder()(tx2)
suite.Require().NoError(err)
return [][]byte{bz1, bz2}
},
[][]string{{suite.accounts[0].Address.String(), suite.accounts[1].Address.String(), suite.accounts[2].Address.String()}, {suite.accounts[3].Address.String(), suite.accounts[4].Address.String(), suite.accounts[5].Address.String()}},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
bundle := tc.createBundle()
signers, err := suite.config.GetBundleSigners(bundle)
suite.Require().NoError(err)
suite.Require().Equal(len(tc.expectedSigners), len(signers))
for i, bundleSigners := range tc.expectedSigners {
suite.Require().Equal(len(bundleSigners), len(signers[i]))
for _, signer := range bundleSigners {
suite.Require().Contains(signers[i], signer)
}
bidInfo, _ := suite.config.GetAuctionBidInfo(tx)
if tc.expectedError {
suite.Require().Nil(bidInfo)
} else {
suite.Require().Equal(tc.expectedSigners, bidInfo.Signers)
}
})
}
@ -246,6 +179,7 @@ func (suite *IntegrationTestSuite) TestWrapBundleTransaction() {
testCases := []struct {
name string
createBundleTx func() (sdk.Tx, []byte)
expectedError bool
}{
{
"normal sdk tx",
@ -258,6 +192,17 @@ func (suite *IntegrationTestSuite) TestWrapBundleTransaction() {
return tx, bz
},
false,
},
{
"random bytes with expected failure",
func() (sdk.Tx, []byte) {
bz := make([]byte, 100)
rand.Read(bz)
return nil, bz
},
true,
},
}
@ -266,15 +211,19 @@ func (suite *IntegrationTestSuite) TestWrapBundleTransaction() {
tx, bz := tc.createBundleTx()
wrappedTx, err := suite.config.WrapBundleTransaction(bz)
suite.Require().NoError(err)
if tc.expectedError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
txBytes, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
txBytes, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
wrappedTxBytes, err := suite.encCfg.TxConfig.TxEncoder()(wrappedTx)
suite.Require().NoError(err)
wrappedTxBytes, err := suite.encCfg.TxConfig.TxEncoder()(wrappedTx)
suite.Require().NoError(err)
suite.Require().Equal(txBytes, wrappedTxBytes)
suite.Require().Equal(txBytes, wrappedTxBytes)
}
})
}
}
@ -285,6 +234,7 @@ func (suite *IntegrationTestSuite) TestGetBidder() {
createTx func() sdk.Tx
expectedBidder string
expectedError bool
isAuctionTx bool
}{
{
"normal sdk tx",
@ -295,7 +245,8 @@ func (suite *IntegrationTestSuite) TestGetBidder() {
return tx
},
"",
true,
false,
false,
},
{
"valid auction tx",
@ -311,6 +262,7 @@ func (suite *IntegrationTestSuite) TestGetBidder() {
},
suite.accounts[0].Address.String(),
false,
true,
},
{
"invalid auction tx",
@ -329,6 +281,7 @@ func (suite *IntegrationTestSuite) TestGetBidder() {
},
"",
true,
false,
},
}
@ -336,12 +289,15 @@ func (suite *IntegrationTestSuite) TestGetBidder() {
suite.Run(tc.name, func() {
tx := tc.createTx()
bidder, err := suite.config.GetBidder(tx)
bidInfo, err := suite.config.GetAuctionBidInfo(tx)
if tc.expectedError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(tc.expectedBidder, bidder.String())
if tc.isAuctionTx {
suite.Require().Equal(tc.expectedBidder, bidInfo.Bidder.String())
}
}
})
}
@ -353,6 +309,7 @@ func (suite *IntegrationTestSuite) TestGetBid() {
createTx func() sdk.Tx
expectedBid sdk.Coin
expectedError bool
isAuctionTx bool
}{
{
"normal sdk tx",
@ -363,7 +320,8 @@ func (suite *IntegrationTestSuite) TestGetBid() {
return tx
},
sdk.Coin{},
true,
false,
false,
},
{
"valid auction tx",
@ -379,6 +337,7 @@ func (suite *IntegrationTestSuite) TestGetBid() {
},
sdk.NewInt64Coin("foo", 100),
false,
true,
},
{
"invalid auction tx",
@ -397,6 +356,7 @@ func (suite *IntegrationTestSuite) TestGetBid() {
},
sdk.Coin{},
true,
false,
},
}
@ -404,12 +364,15 @@ func (suite *IntegrationTestSuite) TestGetBid() {
suite.Run(tc.name, func() {
tx := tc.createTx()
bid, err := suite.config.GetBid(tx)
bidInfo, err := suite.config.GetAuctionBidInfo(tx)
if tc.expectedError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(tc.expectedBid, bid)
if tc.isAuctionTx {
suite.Require().Equal(tc.expectedBid, bidInfo.Bid)
}
}
})
}
@ -420,6 +383,7 @@ func (suite *IntegrationTestSuite) TestGetBundledTransactions() {
name string
createTx func() (sdk.Tx, [][]byte)
expectedError bool
isAuctionTx bool
}{
{
"normal sdk tx",
@ -429,7 +393,8 @@ func (suite *IntegrationTestSuite) TestGetBundledTransactions() {
return tx, nil
},
true,
false,
false,
},
{
"valid auction tx",
@ -444,6 +409,7 @@ func (suite *IntegrationTestSuite) TestGetBundledTransactions() {
return tx, msgAuctionBid.Transactions
},
false,
true,
},
{
"invalid auction tx",
@ -461,6 +427,7 @@ func (suite *IntegrationTestSuite) TestGetBundledTransactions() {
return tx, nil
},
true,
false,
},
}
@ -468,12 +435,15 @@ func (suite *IntegrationTestSuite) TestGetBundledTransactions() {
suite.Run(tc.name, func() {
tx, expectedBundledTxs := tc.createTx()
bundledTxs, err := suite.config.GetBundledTransactions(tx)
bidInfo, err := suite.config.GetAuctionBidInfo(tx)
if tc.expectedError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(expectedBundledTxs, bundledTxs)
if tc.isAuctionTx {
suite.Require().Equal(expectedBundledTxs, bidInfo.Transactions)
}
}
})
}
@ -484,6 +454,7 @@ func (suite *IntegrationTestSuite) TestGetTimeout() {
name string
createTx func() sdk.Tx
expectedError bool
isAuctionTx bool
expectedTimeout uint64
}{
{
@ -495,6 +466,7 @@ func (suite *IntegrationTestSuite) TestGetTimeout() {
return tx
},
false,
false,
1,
},
{
@ -510,6 +482,7 @@ func (suite *IntegrationTestSuite) TestGetTimeout() {
return tx
},
false,
true,
10,
},
{
@ -527,6 +500,7 @@ func (suite *IntegrationTestSuite) TestGetTimeout() {
suite.Require().NoError(err)
return tx
},
true,
false,
10,
},
@ -536,12 +510,15 @@ func (suite *IntegrationTestSuite) TestGetTimeout() {
suite.Run(tc.name, func() {
tx := tc.createTx()
timeout, err := suite.config.GetTimeout(tx)
bidInfo, err := suite.config.GetAuctionBidInfo(tx)
if tc.expectedError {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(tc.expectedTimeout, timeout)
if tc.isAuctionTx {
suite.Require().Equal(tc.expectedTimeout, bidInfo.Timeout)
}
}
})
}

View File

@ -1,242 +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
}
// Config defines the configuration 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.
Config interface {
// IsAuctionTx defines a function that returns true iff a transaction is an
// auction bid transaction.
IsAuctionTx(tx sdk.Tx) (bool, error)
// GetTransactionSigners defines a function that returns the signers of a
// bundle transaction i.e. transaction that was included in the auction transaction's bundle.
GetTransactionSigners(tx []byte) (map[string]struct{}, error)
// GetBundleSigners defines a function that returns the signers of every transaction in a bundle.
GetBundleSigners(tx [][]byte) ([]map[string]struct{}, error)
// WrapBundleTransaction defines a function that wraps a bundle transaction into a sdk.Tx.
WrapBundleTransaction(tx []byte) (sdk.Tx, error)
// GetBidder defines a function that returns the bidder of an auction transaction transaction.
GetBidder(tx sdk.Tx) (sdk.AccAddress, error)
// GetBid defines a function that returns the bid of an auction transaction.
GetBid(tx sdk.Tx) (sdk.Coin, error)
// GetBundledTransactions defines a function that returns the bundled transactions
// that the user wants to execute at the top of the block given an auction transaction.
GetBundledTransactions(tx sdk.Tx) ([][]byte, error)
// GetTimeout defines a function that returns the timeout of an auction transaction.
GetTimeout(tx sdk.Tx) (uint64, error)
// GetAuctionBidInfo defines a function that returns the bid info from an auction transaction.
GetAuctionBidInfo(tx sdk.Tx) (AuctionBidInfo, error)
}
// DefaultConfig defines a default configuration for processing auction transactions.
DefaultConfig 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 _ Config = (*DefaultConfig)(nil)
// NewDefaultConfig returns a default transaction configuration.
func NewDefaultConfig(txDecoder sdk.TxDecoder) Config {
return &DefaultConfig{
txDecoder: txDecoder,
}
}
// NewDefaultIsAuctionTx defines a default function that returns true iff a transaction
// is an auction bid transaction. In the default case, the transaction must contain a single
// MsgAuctionBid message.
func (config *DefaultConfig) IsAuctionTx(tx sdk.Tx) (bool, error) {
msg, err := GetMsgAuctionBidFromTx(tx)
if err != nil {
return false, err
}
return msg != nil, nil
}
// GetTransactionSigners defines a default function that returns the signers
// of a transaction. 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 *DefaultConfig) GetTransactionSigners(tx []byte) (map[string]struct{}, error) {
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")
}
signers := make(map[string]struct{})
for _, signer := range sigTx.GetSigners() {
signers[signer.String()] = struct{}{}
}
return signers, nil
}
// GetBundleSigners defines a default function that returns the signers of every transaction
// 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 *DefaultConfig) GetBundleSigners(txs [][]byte) ([]map[string]struct{}, error) {
signers := make([]map[string]struct{}, len(txs))
for index, tx := range txs {
txSigners, err := config.GetTransactionSigners(tx)
if err != nil {
return nil, err
}
signers[index] = txSigners
}
return signers, nil
}
// 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 *DefaultConfig) WrapBundleTransaction(tx []byte) (sdk.Tx, error) {
return config.txDecoder(tx)
}
// GetBidder defines a default function that returns the bidder of an auction transaction.
// In the default case, the bidder is the address defined in MsgAuctionBid.
func (config *DefaultConfig) GetBidder(tx sdk.Tx) (sdk.AccAddress, error) {
isAuctionTx, err := config.IsAuctionTx(tx)
if err != nil {
return nil, err
}
if !isAuctionTx {
return nil, fmt.Errorf("transaction is not an auction transaction")
}
msg, err := GetMsgAuctionBidFromTx(tx)
if err != nil {
return nil, err
}
bidder, err := sdk.AccAddressFromBech32(msg.Bidder)
if err != nil {
return nil, fmt.Errorf("invalid bidder address (%s): %w", msg.Bidder, err)
}
return bidder, nil
}
// GetBid defines a default function that returns the bid of an auction transaction.
// In the default case, the bid is the amount defined in MsgAuctionBid.
func (config *DefaultConfig) GetBid(tx sdk.Tx) (sdk.Coin, error) {
isAuctionTx, err := config.IsAuctionTx(tx)
if err != nil {
return sdk.Coin{}, err
}
if !isAuctionTx {
return sdk.Coin{}, fmt.Errorf("transaction is not an auction transaction")
}
msg, err := GetMsgAuctionBidFromTx(tx)
if err != nil {
return sdk.Coin{}, err
}
return msg.Bid, nil
}
// GetBundledTransactions defines a default function that returns the bundled
// transactions that the user wants to execute at the top of the block. In the default case,
// the bundled transactions will be the raw bytes of sdk.Tx's that are included in the
// MsgAuctionBid.
func (config *DefaultConfig) GetBundledTransactions(tx sdk.Tx) ([][]byte, error) {
isAuctionTx, err := config.IsAuctionTx(tx)
if err != nil {
return nil, err
}
if !isAuctionTx {
return nil, fmt.Errorf("transaction is not an auction transaction")
}
msg, err := GetMsgAuctionBidFromTx(tx)
if err != nil {
return nil, err
}
return msg.Transactions, nil
}
// GetTimeout defines a default function that returns the timeout of an auction transaction.
func (config *DefaultConfig) GetTimeout(tx sdk.Tx) (uint64, error) {
timeoutTx, ok := tx.(TxWithTimeoutHeight)
if !ok {
return 0, fmt.Errorf("transaction does not implement TxWithTimeoutHeight")
}
return timeoutTx.GetTimeoutHeight(), nil
}
// GetAuctionBidInfo returns the auction bid info from an auction transaction.
func (config *DefaultConfig) GetAuctionBidInfo(tx sdk.Tx) (AuctionBidInfo, error) {
bid, err := config.GetBid(tx)
if err != nil {
return AuctionBidInfo{}, err
}
bidder, err := config.GetBidder(tx)
if err != nil {
return AuctionBidInfo{}, err
}
bundle, err := config.GetBundledTransactions(tx)
if err != nil {
return AuctionBidInfo{}, err
}
timeout, err := config.GetTimeout(tx)
if err != nil {
return AuctionBidInfo{}, err
}
return AuctionBidInfo{
Bid: bid,
Bidder: bidder,
Transactions: bundle,
Timeout: timeout,
}, nil
}

View File

@ -38,21 +38,21 @@ type AuctionMempool struct {
// to quickly check if a transaction is already in the mempool.
txIndex map[string]struct{}
// Config defines the transaction configuration for processing auction transactions.
Config
// 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 Config) TxPriority[string] {
func AuctionTxPriority(config AuctionFactory) TxPriority[string] {
return TxPriority[string]{
GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string {
bid, err := config.GetBid(tx)
bidInfo, err := config.GetAuctionBidInfo(tx)
if err != nil {
panic(err)
}
return bid.String()
return bidInfo.Bid.String()
},
Compare: func(a, b string) int {
aCoins, _ := sdk.ParseCoinsNormalized(a)
@ -85,7 +85,7 @@ func AuctionTxPriority(config Config) TxPriority[string] {
}
}
func NewAuctionMempool(txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, maxTx int, config Config) *AuctionMempool {
func NewAuctionMempool(txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, maxTx int, config AuctionFactory) *AuctionMempool {
return &AuctionMempool{
globalIndex: NewPriorityMempool(
PriorityNonceMempoolConfig[int64]{
@ -99,27 +99,26 @@ func NewAuctionMempool(txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, maxTx i
MaxTx: maxTx,
},
),
txDecoder: txDecoder,
txEncoder: txEncoder,
txIndex: make(map[string]struct{}),
Config: config,
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 {
isAuctionTx, err := am.IsAuctionTx(tx)
bidInfo, err := am.GetAuctionBidInfo(tx)
if err != nil {
return err
}
// Insert the transactions into the appropriate index.
switch {
case !isAuctionTx:
if bidInfo == nil {
if err := am.globalIndex.Insert(ctx, tx); err != nil {
return fmt.Errorf("failed to insert tx into global index: %w", err)
}
case isAuctionTx:
} else {
if err := am.auctionIndex.Insert(ctx, tx); err != nil {
return fmt.Errorf("failed to insert tx into auction index: %w", err)
}
@ -137,16 +136,15 @@ func (am *AuctionMempool) Insert(ctx context.Context, tx sdk.Tx) error {
// Remove removes a transaction from the mempool based on the transaction type (normal or auction).
func (am *AuctionMempool) Remove(tx sdk.Tx) error {
isAuctionTx, err := am.IsAuctionTx(tx)
bidInfo, err := am.GetAuctionBidInfo(tx)
if err != nil {
return err
}
// Remove the transactions from the appropriate index.
switch {
case !isAuctionTx:
if bidInfo == nil {
am.removeTx(am.globalIndex, tx)
case isAuctionTx:
} else {
am.removeTx(am.auctionIndex, tx)
}

View File

@ -19,7 +19,7 @@ type IntegrationTestSuite struct {
suite.Suite
encCfg testutils.EncodingConfig
config mempool.Config
config mempool.AuctionFactory
mempool *mempool.AuctionMempool
ctx sdk.Context
random *rand.Rand
@ -34,7 +34,7 @@ func TestMempoolTestSuite(t *testing.T) {
func (suite *IntegrationTestSuite) SetupTest() {
// Mempool setup
suite.encCfg = testutils.CreateTestEncodingConfig()
suite.config = mempool.NewDefaultConfig(suite.encCfg.TxConfig.TxDecoder())
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())

View File

@ -16,11 +16,8 @@ var _ sdk.AnteDecorator = BuilderDecorator{}
type (
Mempool interface {
Contains(tx sdk.Tx) (bool, error)
IsAuctionTx(tx sdk.Tx) (bool, error)
GetAuctionBidInfo(tx sdk.Tx) (mempool.AuctionBidInfo, error)
GetBundleSigners(txs [][]byte) ([]map[string]struct{}, error)
GetAuctionBidInfo(tx sdk.Tx) (*mempool.AuctionBidInfo, error)
GetTopAuctionTx(ctx context.Context) sdk.Tx
GetTimeout(tx sdk.Tx) (uint64, error)
}
BuilderDecorator struct {
@ -55,108 +52,51 @@ func (ad BuilderDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
}
}
isAuctionTx, err := ad.mempool.IsAuctionTx(tx)
bidInfo, err := ad.mempool.GetAuctionBidInfo(tx)
if err != nil {
return ctx, err
}
// Validate the auction bid if one exists.
if isAuctionTx {
if bidInfo != nil {
// Auction transactions must have a timeout set to a valid block height.
if err := ad.HasValidTimeout(ctx, tx); err != nil {
return ctx, err
if int64(bidInfo.Timeout) < ctx.BlockHeight() {
return ctx, fmt.Errorf("timeout height cannot be less than the current block height")
}
bidInfo, err := ad.mempool.GetAuctionBidInfo(tx)
if err != nil {
return ctx, err
}
// If the current transaction is the highest bidding transaction, then the highest bid is empty.
topBid := sdk.Coin{}
// We only need to verify the auction bid relative to the local validator's mempool if the mode
// is checkTx or recheckTx. Otherwise, the ABCI handlers (VerifyVoteExtension, ExtendVoteExtension, etc.)
// will always compare the auction bid to the highest bidding transaction in the mempool leading to
// poor liveness guarantees.
topBid := sdk.Coin{}
if ctx.IsCheckTx() || ctx.IsReCheckTx() {
isTopBidTx, err := ad.IsTopBidTx(ctx, tx)
if err != nil {
return ctx, errors.Wrap(err, "failed to check if current transaction is highest bidding transaction")
}
if !isTopBidTx {
// Set the top bid to the highest bidding transaction.
topBid, err = ad.GetTopAuctionBid(ctx)
if topBidTx := ad.mempool.GetTopAuctionTx(ctx); topBidTx != nil {
topBidBz, err := ad.txEncoder(topBidTx)
if err != nil {
return ctx, errors.Wrap(err, "failed to get highest auction bid")
return ctx, err
}
currentTxBz, err := ad.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)
if err != nil {
return ctx, err
}
topBid = topBidInfo.Bid
}
}
}
// Extract signers from bundle for verification.
signers, err := ad.mempool.GetBundleSigners(bidInfo.Transactions)
if err != nil {
return ctx, errors.Wrap(err, "failed to get bundle signers")
}
if err := ad.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo, signers); err != nil {
if err := ad.builderKeeper.ValidateBidInfo(ctx, topBid, bidInfo); err != nil {
return ctx, errors.Wrap(err, "failed to validate auction bid")
}
}
return next(ctx, tx, simulate)
}
// GetTopAuctionBid returns the highest auction bid if one exists.
func (ad BuilderDecorator) GetTopAuctionBid(ctx sdk.Context) (sdk.Coin, error) {
auctionTx := ad.mempool.GetTopAuctionTx(ctx)
if auctionTx == nil {
return sdk.Coin{}, nil
}
auctionBidInfo, err := ad.mempool.GetAuctionBidInfo(auctionTx)
if err != nil {
return sdk.Coin{}, err
}
return auctionBidInfo.Bid, nil
}
// IsTopBidTx returns true if the transaction inputted is the highest bidding auction transaction in the mempool.
func (ad BuilderDecorator) IsTopBidTx(ctx sdk.Context, tx sdk.Tx) (bool, error) {
auctionTx := ad.mempool.GetTopAuctionTx(ctx)
if auctionTx == nil {
return false, nil
}
topBidBz, err := ad.txEncoder(auctionTx)
if err != nil {
return false, err
}
currentTxBz, err := ad.txEncoder(tx)
if err != nil {
return false, err
}
return bytes.Equal(topBidBz, currentTxBz), nil
}
// HasValidTimeout returns true if the transaction has a valid timeout height.
func (ad BuilderDecorator) HasValidTimeout(ctx sdk.Context, tx sdk.Tx) error {
timeout, err := ad.mempool.GetTimeout(tx)
if err != nil {
return err
}
if timeout == 0 {
return fmt.Errorf("timeout height cannot be zero")
}
if timeout < uint64(ctx.BlockHeight()) {
return fmt.Errorf("timeout height cannot be less than the current block height")
}
return nil
}

View File

@ -46,8 +46,7 @@ func (suite *AnteTestSuite) SetupTest() {
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
suite.ctx = suite.ctx.WithIsCheckTx(true)
suite.ctx = testCtx.Ctx.WithIsCheckTx(true)
// Keepers set up
ctrl := gomock.NewController(suite.T())
@ -234,6 +233,8 @@ func (suite *AnteTestSuite) TestAnteHandler() {
suite.SetupTest()
tc.malleate()
suite.ctx = suite.ctx.WithBlockHeight(1)
// Set the auction params
err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.Params{
MaxBundleSize: maxBundleSize,
@ -245,7 +246,7 @@ func (suite *AnteTestSuite) TestAnteHandler() {
suite.Require().NoError(err)
// Insert the top bid into the mempool
config := mempool.NewDefaultConfig(suite.encodingConfig.TxConfig.TxDecoder())
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{})

View File

@ -8,7 +8,7 @@ import (
)
// ValidateBidInfo validates that the bid can be included in the auction.
func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo mempool.AuctionBidInfo, signers []map[string]struct{}) error {
func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo *mempool.AuctionBidInfo) error {
// Validate the bundle size.
maxBundleSize, err := k.GetMaxBundleSize(ctx)
if err != nil {
@ -31,7 +31,7 @@ func (k Keeper) ValidateBidInfo(ctx sdk.Context, highestBid sdk.Coin, bidInfo me
}
if protectionEnabled {
if err := k.ValidateAuctionBundle(bidInfo.Bidder, signers); err != nil {
if err := k.ValidateAuctionBundle(bidInfo.Bidder, bidInfo.Signers); err != nil {
return err
}
}

View File

@ -11,7 +11,7 @@ import (
buildertypes "github.com/skip-mev/pob/x/builder/types"
)
func (suite *KeeperTestSuite) TestValidateAuctionMsg() {
func (suite *KeeperTestSuite) TestValidateBidInfo() {
var (
// Tx building variables
accounts = []testutils.Account{} // tracks the order of signers in the bundle
@ -192,16 +192,23 @@ func (suite *KeeperTestSuite) TestValidateAuctionMsg() {
bundle = append(bundle, txBz)
}
bidInfo := mempool.AuctionBidInfo{
signers := make([]map[string]struct{}, len(accounts))
for index, acc := range accounts {
txSigners := map[string]struct{}{
acc.Address.String(): {},
}
signers[index] = txSigners
}
bidInfo := &mempool.AuctionBidInfo{
Bidder: bidder.Address,
Bid: bid,
Transactions: bundle,
Signers: signers,
}
signers, err := suite.mempool.GetBundleSigners(bundle)
suite.Require().NoError(err)
err = suite.builderKeeper.ValidateBidInfo(suite.ctx, highestBid, bidInfo, signers)
err := suite.builderKeeper.ValidateBidInfo(suite.ctx, highestBid, bidInfo)
if tc.pass {
suite.Require().NoError(err)
} else {
@ -302,23 +309,17 @@ func (suite *KeeperTestSuite) TestValidateBundle() {
// Malleate the test case
tc.malleate()
// Create the bundle of transactions ordered by accounts
bundle := make([][]byte, 0)
for _, acc := range accounts {
// Create a random tx
tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, acc, 0, 1, 1000)
suite.Require().NoError(err)
signers := make([]map[string]struct{}, len(accounts))
for index, acc := range accounts {
txSigners := map[string]struct{}{
acc.Address.String(): {},
}
txBz, err := suite.encCfg.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
bundle = append(bundle, txBz)
signers[index] = txSigners
}
signers, err := suite.mempool.GetBundleSigners(bundle)
suite.Require().NoError(err)
// Validate the bundle
err = suite.builderKeeper.ValidateAuctionBundle(bidder.Address, signers)
err := suite.builderKeeper.ValidateAuctionBundle(bidder.Address, signers)
if tc.pass {
suite.Require().NoError(err)
} else {

View File

@ -64,7 +64,7 @@ func (suite *KeeperTestSuite) SetupTest() {
err := suite.builderKeeper.SetParams(suite.ctx, types.DefaultParams())
suite.Require().NoError(err)
config := mempool.NewDefaultConfig(suite.encCfg.TxConfig.TxDecoder())
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)
}