[ENG-540]: Process Proposal + Process Proposal Testing (#31)

This commit is contained in:
David Terpay 2023-03-22 14:06:15 -04:00 committed by GitHub
parent c8115a0d81
commit 717238ffd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 411 additions and 22 deletions

View File

@ -1,6 +1,7 @@
package abci
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
@ -175,7 +176,38 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
// block proposal verification.
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req abci.RequestProcessProposal) abci.ResponseProcessProposal {
panic("not implemented")
for index, txBz := range req.Txs {
tx, err := h.txVerifier.ProcessProposalVerifyTx(txBz)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
msgAuctionBid, err := mempool.GetMsgAuctionBidFromTx(tx)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
if msgAuctionBid != nil {
// Only the first transaction can be an auction bid tx
if index != 0 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
// The order of transactions in the block proposal must follow the order of transactions in the bid.
if len(req.Txs) < len(msgAuctionBid.Transactions)+1 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
for i, refTxRaw := range msgAuctionBid.Transactions {
if !bytes.Equal(refTxRaw, req.Txs[i+1]) {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
}
}
}
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}
}
}

View File

@ -1,6 +1,7 @@
package abci_test
import (
"bytes"
"math/rand"
"testing"
"time"
@ -15,7 +16,7 @@ import (
"github.com/skip-mev/pob/mempool"
"github.com/skip-mev/pob/x/auction/ante"
"github.com/skip-mev/pob/x/auction/keeper"
"github.com/skip-mev/pob/x/auction/types"
auctiontypes "github.com/skip-mev/pob/x/auction/types"
"github.com/stretchr/testify/suite"
)
@ -50,7 +51,7 @@ type ABCITestSuite struct {
nonces map[string]uint64
}
func TestPrepareProposalSuite(t *testing.T) {
func TestABCISuite(t *testing.T) {
suite.Run(t, new(ABCITestSuite))
}
@ -58,7 +59,7 @@ func (suite *ABCITestSuite) SetupTest() {
// General config
suite.encodingConfig = createTestEncodingConfig()
suite.random = rand.New(rand.NewSource(time.Now().Unix()))
suite.key = sdk.NewKVStoreKey(types.StoreKey)
suite.key = sdk.NewKVStoreKey(auctiontypes.StoreKey)
testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, sdk.NewTransientStoreKey("transient_test"))
suite.ctx = testCtx.Ctx
@ -70,7 +71,7 @@ func (suite *ABCITestSuite) SetupTest() {
// Mock keepers set up
ctrl := gomock.NewController(suite.T())
suite.accountKeeper = NewMockAccountKeeper(ctrl)
suite.accountKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(sdk.AccAddress{}).AnyTimes()
suite.accountKeeper.EXPECT().GetModuleAddress(auctiontypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes()
suite.bankKeeper = NewMockBankKeeper(ctrl)
suite.distrKeeper = NewMockDistributionKeeper(ctrl)
suite.stakingKeeper = NewMockStakingKeeper(ctrl)
@ -86,9 +87,9 @@ func (suite *ABCITestSuite) SetupTest() {
suite.stakingKeeper,
suite.authorityAccount.String(),
)
err := suite.auctionKeeper.SetParams(suite.ctx, types.DefaultParams())
err := suite.auctionKeeper.SetParams(suite.ctx, auctiontypes.DefaultParams())
suite.Require().NoError(err)
suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool)
suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// Accounts set up
suite.accounts = RandomAccounts(suite.random, 1)
@ -117,8 +118,18 @@ func (suite *ABCITestSuite) PrepareProposalVerifyTx(tx sdk.Tx) ([]byte, error) {
return txBz, nil
}
func (suite *ABCITestSuite) ProcessProposalVerifyTx(_ []byte) (sdk.Tx, error) {
return nil, nil
func (suite *ABCITestSuite) ProcessProposalVerifyTx(txBz []byte) (sdk.Tx, error) {
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz)
if err != nil {
return nil, err
}
_, err = suite.executeAnteHandler(tx)
if err != nil {
return tx, err
}
return tx, nil
}
func (suite *ABCITestSuite) executeAnteHandler(tx sdk.Tx) (sdk.Context, error) {
@ -216,6 +227,41 @@ func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, num
return totalNumTxs
}
func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte {
txs := make([][]byte, 0)
seenTxs := make(map[string]bool)
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
auctionTx := auctionIterator.Tx().(*mempool.WrappedBidTx).Tx
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx)
suite.Require().NoError(err)
txs = append(txs, txBz)
if exportRefTxs {
for _, refRawTx := range auctionTx.GetMsgs()[0].(*auctiontypes.MsgAuctionBid).GetTransactions() {
txs = append(txs, refRawTx)
seenTxs[string(refRawTx)] = true
}
}
seenTxs[string(txBz)] = true
}
iterator := suite.mempool.Select(suite.ctx, nil)
for ; iterator != nil; iterator = iterator.Next() {
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx())
suite.Require().NoError(err)
if !seenTxs[string(txBz)] {
txs = append(txs, txBz)
}
}
return txs
}
func (suite *ABCITestSuite) TestPrepareProposal() {
var (
// the modified transactions cannot exceed this size
@ -436,7 +482,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
// create a new auction
params := types.Params{
params := auctiontypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
MinBuyInFee: minBuyInFee,
@ -444,7 +490,7 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
MinBidIncrement: suite.minBidIncrement,
}
suite.auctionKeeper.SetParams(suite.ctx, params)
suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.mempool)
suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
handler := suite.proposalHandler.PrepareProposalHandler()
res := handler(suite.ctx, abcitypes.RequestPrepareProposal{
@ -500,6 +546,221 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
}
}
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.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
minBuyInFee = sdk.NewCoins(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.NewCoins(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.NewCoins(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.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000)))
insertRefTxs = false
exportRefTxs = false
},
true,
abcitypes.ResponseProcessProposal_REJECT,
},
{
"auction tx with frontrunning",
func() {
randomAccount := RandomAccounts(suite.random, 1)[0]
bidder := suite.accounts[0]
bid := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(696969696969)))
nonce := suite.nonces[bidder.Address.String()]
frontRunningTx, _ = createAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, []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 := RandomAccounts(suite.random, 1)[0]
bidder := suite.accounts[0]
bid := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(696969696969)))
nonce := suite.nonces[bidder.Address.String()]
frontRunningTx, _ = createAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, []Account{bidder, randomAccount})
suite.Require().NotNil(frontRunningTx)
numAuctionTxs = 0
frontRunningProtection = false
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
},
}
for _, tc := range cases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
tc.malleate()
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
if frontRunningTx != nil {
suite.Require().NoError(suite.mempool.Insert(suite.ctx, frontRunningTx))
}
// create a new auction
params := auctiontypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
MinBuyInFee: minBuyInFee,
FrontRunningProtection: frontRunningProtection,
MinBidIncrement: suite.minBidIncrement,
}
suite.auctionKeeper.SetParams(suite.ctx, params)
suite.auctionDecorator = ante.NewAuctionDecorator(suite.auctionKeeper, suite.encodingConfig.TxConfig.TxDecoder(), 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)
})
}
}
// 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 {

View File

@ -146,3 +146,45 @@ func createMsgAuctionBid(txCfg client.TxConfig, bidder Account, bid sdk.Coins, n
return bidMsg, nil
}
func createAuctionTxWithSigners(txCfg client.TxConfig, bidder Account, bid sdk.Coins, nonce uint64, signers []Account) (authsigning.Tx, error) {
bidMsg := &auctiontypes.MsgAuctionBid{
Bidder: bidder.Address.String(),
Bid: bid,
Transactions: make([][]byte, len(signers)),
}
for i := 0; i < len(signers); i++ {
randomMsg := createRandomMsgs(signers[i].Address, 1)
randomTx, err := createTx(txCfg, signers[i], 0, randomMsg)
if err != nil {
return nil, err
}
bz, err := txCfg.TxEncoder()(randomTx)
if err != nil {
return nil, err
}
bidMsg.Transactions[i] = bz
}
txBuilder := txCfg.NewTxBuilder()
if err := txBuilder.SetMsgs(bidMsg); err != nil {
return nil, err
}
sigV2 := signing.SignatureV2{
PubKey: bidder.PrivKey.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: txCfg.SignModeHandler().DefaultMode(),
Signature: nil,
},
Sequence: nonce,
}
if err := txBuilder.SetSignatures(sigV2); err != nil {
return nil, err
}
return txBuilder.GetTx(), nil
}

View File

@ -54,6 +54,10 @@ func GetMsgAuctionBidFromTx(tx sdk.Tx) (*auctiontypes.MsgAuctionBid, error) {
// UnwrapBidTx attempts to unwrap a WrappedBidTx from an sdk.Tx if one exists.
func UnwrapBidTx(tx sdk.Tx) sdk.Tx {
if tx == nil {
return nil
}
wTx, ok := tx.(*WrappedBidTx)
if ok {
return wTx.Tx

View File

@ -3,6 +3,7 @@ package mempool_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
pobcodec "github.com/skip-mev/pob/codec"
"github.com/skip-mev/pob/mempool"
@ -46,3 +47,22 @@ func TestGetMsgAuctionBidFromTx_NoBid(t *testing.T) {
require.NoError(t, err)
require.Nil(t, msg)
}
func TestGetUnwrappedTx(t *testing.T) {
encCfg := pobcodec.CreateEncodingConfig()
txBuilder := encCfg.TxConfig.NewTxBuilder()
txBuilder.SetMsgs(&auctiontypes.MsgAuctionBid{})
tx := txBuilder.GetTx()
bid := sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000)))
wrappedTx := mempool.NewWrappedBidTx(tx, bid)
unWrappedTx := mempool.UnwrapBidTx(wrappedTx)
unwrappedBz, err := encCfg.TxConfig.TxEncoder()(unWrappedTx)
require.NoError(t, err)
txBz, err := encCfg.TxConfig.TxEncoder()(tx)
require.NoError(t, err)
require.Equal(t, txBz, unwrappedBz)
}

View File

@ -1,6 +1,8 @@
package ante
import (
"bytes"
"cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/mempool"
@ -12,13 +14,15 @@ var _ sdk.AnteDecorator = AuctionDecorator{}
type AuctionDecorator struct {
auctionKeeper keeper.Keeper
txDecoder sdk.TxDecoder
txEncoder sdk.TxEncoder
mempool *mempool.AuctionMempool
}
func NewAuctionDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, mempool *mempool.AuctionMempool) AuctionDecorator {
func NewAuctionDecorator(ak keeper.Keeper, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder, mempool *mempool.AuctionMempool) AuctionDecorator {
return AuctionDecorator{
auctionKeeper: ak,
txDecoder: txDecoder,
txEncoder: txEncoder,
mempool: mempool,
}
}
@ -48,12 +52,23 @@ func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
transactions[i] = decodedTx
}
highestBid, err := ad.GetTopAuctionBid(ctx, tx)
topBid := sdk.NewCoins()
// If the current transaction is the highest bidding transaction, then the highest bid is empty.
isTopBidTx, err := ad.IsTopBidTx(ctx, tx)
if err != nil {
return ctx, errors.Wrap(err, "failed to get highest auction bid")
return ctx, errors.Wrap(err, "failed to check if current transaction is highest bidding transaction")
}
if err := ad.auctionKeeper.ValidateAuctionMsg(ctx, bidder, auctionMsg.Bid, highestBid, transactions); err != nil {
if !isTopBidTx {
// Set the top bid to the highest bidding transaction.
topBid, err = ad.GetTopAuctionBid(ctx)
if err != nil {
return ctx, errors.Wrap(err, "failed to get highest auction bid")
}
}
if err := ad.auctionKeeper.ValidateAuctionMsg(ctx, bidder, auctionMsg.Bid, topBid, transactions); err != nil {
return ctx, errors.Wrap(err, "failed to validate auction bid")
}
}
@ -61,18 +76,33 @@ func (ad AuctionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
return next(ctx, tx, simulate)
}
// GetTopAuctionBid returns the highest auction bid if one exists. If the current transaction is the highest
// bidding transaction, then an empty coin set is returned.
func (ad AuctionDecorator) GetTopAuctionBid(ctx sdk.Context, currTx sdk.Tx) (sdk.Coins, error) {
// GetTopAuctionBid returns the highest auction bid if one exists.
func (ad AuctionDecorator) GetTopAuctionBid(ctx sdk.Context) (sdk.Coins, error) {
auctionTx := ad.mempool.GetTopAuctionTx(ctx)
if auctionTx == nil {
return sdk.NewCoins(), nil
}
wrappedTx := auctionTx.(*mempool.WrappedBidTx)
if wrappedTx.Tx == currTx {
return sdk.NewCoins(), nil
return auctionTx.(*mempool.WrappedBidTx).GetBid(), nil
}
// IsTopBidTx returns true if the transaction inputted is the highest bidding auction transaction in the mempool.
func (ad AuctionDecorator) IsTopBidTx(ctx sdk.Context, tx sdk.Tx) (bool, error) {
auctionTx := ad.mempool.GetTopAuctionTx(ctx)
if auctionTx == nil {
return false, nil
}
return wrappedTx.GetBid(), nil
topBidTx := mempool.UnwrapBidTx(auctionTx)
topBidBz, err := ad.txEncoder(topBidTx)
if err != nil {
return false, err
}
currentTxBz, err := ad.txEncoder(tx)
if err != nil {
return false, err
}
return bytes.Equal(topBidBz, currentTxBz), nil
}