feat(v1): Vote Extension Auction (#106)

This commit is contained in:
David Terpay 2023-05-09 16:47:27 -04:00 committed by GitHub
parent cc74b9e027
commit 7797110514
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 3244 additions and 399 deletions

View File

@ -1,13 +1,11 @@
package abci_test
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"testing"
"time"
comettypes "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/libs/log"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/testutil"
@ -99,7 +97,7 @@ func (suite *ABCITestSuite) SetupTest() {
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// Accounts set up
suite.accounts = testutils.RandomAccounts(suite.random, 1)
suite.accounts = testutils.RandomAccounts(suite.random, 10)
suite.balances = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(1000000000000000000)))
suite.nonces = make(map[string]uint64)
for _, acc := range suite.accounts {
@ -125,20 +123,6 @@ func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, simulate boo
return ctx, err
}
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
if err != nil {
return ctx, err
}
if !simulate {
hash := sha256.Sum256(bz)
txHash := hex.EncodeToString(hash[:])
if _, ok := suite.txs[txHash]; ok {
return ctx, fmt.Errorf("tx already in mempool")
}
suite.txs[txHash] = struct{}{}
}
return ctx, nil
}
@ -228,28 +212,10 @@ func (suite *ABCITestSuite) createFilledMempool(numNormalTxs, numAuctionTxs, num
return totalNumTxs
}
func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte {
func (suite *ABCITestSuite) exportMempool() [][]byte {
txs := make([][]byte, 0)
seenTxs := make(map[string]bool)
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
auctionTx := auctionIterator.Tx()
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx)
suite.Require().NoError(err)
txs = append(txs, txBz)
if exportRefTxs {
for _, refRawTx := range auctionTx.GetMsgs()[0].(*buildertypes.MsgAuctionBid).GetTransactions() {
txs = append(txs, refRawTx)
seenTxs[string(refRawTx)] = true
}
}
seenTxs[string(txBz)] = true
}
iterator := suite.mempool.Select(suite.ctx, nil)
for ; iterator != nil; iterator = iterator.Next() {
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(iterator.Tx())
@ -262,3 +228,108 @@ func (suite *ABCITestSuite) exportMempool(exportRefTxs bool) [][]byte {
return txs
}
func (suite *ABCITestSuite) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal {
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
tx := auctionIterator.Tx()
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
VoteExtension: txBz,
})
}
return comettypes.RequestPrepareProposal{
MaxTxBytes: maxBytes,
LocalLastCommit: comettypes.ExtendedCommitInfo{
Votes: voteExtensions,
},
}
}
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxBzs(txs [][]byte) []byte {
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
for _, txBz := range txs {
voteExtensions = append(voteExtensions, comettypes.ExtendedVoteInfo{
VoteExtension: txBz,
})
}
commitInfo := comettypes.ExtendedCommitInfo{
Votes: voteExtensions,
}
commitInfoBz, err := commitInfo.Marshal()
suite.Require().NoError(err)
return commitInfoBz
}
func (suite *ABCITestSuite) createAuctionInfoFromTxBzs(txs [][]byte, numTxs uint64) []byte {
auctionInfo := abci.AuctionInfo{
ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs),
NumTxs: numTxs,
MaxTxBytes: int64(len(txs[0])),
}
auctionInfoBz, err := auctionInfo.Marshal()
suite.Require().NoError(err)
return auctionInfoBz
}
func (suite *ABCITestSuite) getAllAuctionTxs() ([]sdk.Tx, [][]byte) {
auctionIterator := suite.mempool.AuctionBidSelect(suite.ctx)
txs := make([]sdk.Tx, 0)
txBzs := make([][]byte, 0)
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
txs = append(txs, auctionIterator.Tx())
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionIterator.Tx())
suite.Require().NoError(err)
txBzs = append(txBzs, bz)
}
return txs, txBzs
}
func (suite *ABCITestSuite) createExtendedCommitInfoFromTxs(txs []sdk.Tx) comettypes.ExtendedCommitInfo {
voteExtensions := make([][]byte, 0)
for _, tx := range txs {
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
voteExtensions = append(voteExtensions, bz)
}
return suite.createExtendedCommitInfo(voteExtensions)
}
func (suite *ABCITestSuite) createExtendedVoteInfo(voteExtensions [][]byte) []comettypes.ExtendedVoteInfo {
commitInfo := make([]comettypes.ExtendedVoteInfo, 0)
for _, voteExtension := range voteExtensions {
info := comettypes.ExtendedVoteInfo{
VoteExtension: voteExtension,
}
commitInfo = append(commitInfo, info)
}
return commitInfo
}
func (suite *ABCITestSuite) createExtendedCommitInfo(voteExtensions [][]byte) comettypes.ExtendedCommitInfo {
commitInfo := comettypes.ExtendedCommitInfo{
Votes: suite.createExtendedVoteInfo(voteExtensions),
}
return commitInfo
}

231
abci/auction.go Normal file
View File

@ -0,0 +1,231 @@
package abci
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"reflect"
"sort"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// TopOfBlock contains information about how the top of block should be built.
type TopOfBlock struct {
// Txs contains the transactions that should be included in the top of block.
Txs [][]byte
// Size is the total size of the top of block.
Size int64
// Cache is the cache of transactions that were seen, stored in order to ignore them
// when building the rest of the block.
Cache map[string]struct{}
}
func NewTopOfBlock() TopOfBlock {
return TopOfBlock{
Cache: make(map[string]struct{}),
}
}
// BuildTOB inputs all of the vote extensions and outputs a top of block proposal that includes
// the highest bidding valid transaction along with all the bundled transactions.
func (h *ProposalHandler) BuildTOB(ctx sdk.Context, voteExtensionInfo abci.ExtendedCommitInfo, maxBytes int64) TopOfBlock {
// Get the bid transactions from the vote extensions.
sortedBidTxs := h.GetBidsFromVoteExtensions(voteExtensionInfo.Votes)
// Track the transactions we can remove from the mempool
txsToRemove := make(map[sdk.Tx]struct{})
// Attempt to select the highest bid transaction that is valid and whose
// bundled transactions are valid.
var topOfBlock TopOfBlock
for _, bidTx := range sortedBidTxs {
// Cache the context so that we can write it back to the original context
// when we know we have a valid top of block bundle.
cacheCtx, write := ctx.CacheContext()
// Attempt to build the top of block using the bid transaction.
proposal, err := h.buildTOB(cacheCtx, bidTx)
if err != nil {
h.logger.Info(
"vote extension auction failed to verify auction tx",
"err", err,
)
txsToRemove[bidTx] = struct{}{}
continue
}
if proposal.Size <= maxBytes {
// At this point, both the bid transaction itself and all the bundled
// transactions are valid. So we select the bid transaction along with
// all the bundled transactions and apply the state changes to the cache
// context.
topOfBlock = proposal
write()
break
}
h.logger.Info(
"failed to select auction bid tx; auction tx size is too large",
"tx_size", proposal.Size,
"max_size", maxBytes,
)
}
// Remove all of the transactions that were not valid.
for tx := range txsToRemove {
h.RemoveTx(tx)
}
return topOfBlock
}
// VerifyTOB verifies that the set of vote extensions used in prepare proposal deterministically
// produce the same top of block proposal.
func (h *ProposalHandler) VerifyTOB(ctx sdk.Context, proposalTxs [][]byte) (*AuctionInfo, error) {
// Proposal must include at least the auction info.
if len(proposalTxs) < NumInjectedTxs {
return nil, fmt.Errorf("proposal is too small; expected at least %d slots", NumInjectedTxs)
}
// Extract the auction info from the proposal.
auctionInfo := &AuctionInfo{}
if err := auctionInfo.Unmarshal(proposalTxs[AuctionInfoIndex]); err != nil {
return nil, fmt.Errorf("failed to unmarshal auction info: %w", err)
}
// Verify that the proposal contains the expected number of top of block transactions.
if len(proposalTxs) < int(auctionInfo.NumTxs)+NumInjectedTxs {
return nil, fmt.Errorf("number of txs in proposal do not match expected in auction info; expected at least %d slots", auctionInfo.NumTxs+NumInjectedTxs)
}
// Unmarshall the vote extension information from the auction info.
lastCommitInfo := abci.ExtendedCommitInfo{}
if err := lastCommitInfo.Unmarshal(auctionInfo.ExtendedCommitInfo); err != nil {
return nil, fmt.Errorf("failed to unmarshal last commit info from auction info: %w", err)
}
// Build the top of block proposal from the auction info.
expectedTOB := h.BuildTOB(ctx, lastCommitInfo, auctionInfo.MaxTxBytes)
// Verify that the top of block txs matches the top of block proposal txs.
actualTOBTxs := proposalTxs[NumInjectedTxs : auctionInfo.NumTxs+NumInjectedTxs]
if !reflect.DeepEqual(actualTOBTxs, expectedTOB.Txs) {
return nil, fmt.Errorf("expected top of block txs does not match top of block proposal")
}
return auctionInfo, nil
}
// GetBidsFromVoteExtensions returns all of the auction bid transactions from
// the vote extensions in sorted descending order.
func (h *ProposalHandler) GetBidsFromVoteExtensions(voteExtensions []abci.ExtendedVoteInfo) []sdk.Tx {
bidTxs := make([]sdk.Tx, 0)
// Iterate through all vote extensions and extract the auction transactions.
for _, voteInfo := range voteExtensions {
voteExtension := voteInfo.VoteExtension
// Check if the vote extension contains an auction transaction.
if bidTx, err := h.getAuctionTxFromVoteExtension(voteExtension); err == nil {
bidTxs = append(bidTxs, bidTx)
}
}
// Sort the auction transactions by their bid amount in descending order.
sort.Slice(bidTxs, func(i, j int) bool {
// In the case of an error, we want to sort the transaction to the end of the list.
bidInfoI, err := h.mempool.GetAuctionBidInfo(bidTxs[i])
if err != nil {
return false
}
bidInfoJ, err := h.mempool.GetAuctionBidInfo(bidTxs[j])
if err != nil {
return true
}
return bidInfoI.Bid.IsGTE(bidInfoJ.Bid)
})
return bidTxs
}
// buildTOB verifies that the auction and bundled transactions are valid and returns the transactions that
// should be included in the top of block, size of the auction transaction and bundle, and a cache
// of all transactions that should be ignored.
func (h *ProposalHandler) buildTOB(ctx sdk.Context, bidTx sdk.Tx) (TopOfBlock, error) {
proposal := NewTopOfBlock()
// Ensure that the bid transaction is valid
bidTxBz, err := h.PrepareProposalVerifyTx(ctx, bidTx)
if err != nil {
return proposal, err
}
bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx)
if err != nil {
return proposal, err
}
// store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal
sdkTxBytes := make([][]byte, len(bidInfo.Transactions))
// Ensure that the bundled transactions are valid
for index, rawRefTx := range bidInfo.Transactions {
// convert the bundled raw transaction to a sdk.Tx
refTx, err := h.mempool.WrapBundleTransaction(rawRefTx)
if err != nil {
return TopOfBlock{}, err
}
txBz, err := h.PrepareProposalVerifyTx(ctx, refTx)
if err != nil {
return TopOfBlock{}, err
}
hashBz := sha256.Sum256(txBz)
hash := hex.EncodeToString(hashBz[:])
proposal.Cache[hash] = struct{}{}
sdkTxBytes[index] = txBz
}
// cache the bytes of the bid transaction
hashBz := sha256.Sum256(bidTxBz)
hash := hex.EncodeToString(hashBz[:])
proposal.Cache[hash] = struct{}{}
txs := [][]byte{bidTxBz}
txs = append(txs, sdkTxBytes...)
// Set the top of block transactions and size.
proposal.Txs = txs
proposal.Size = int64(len(bidTxBz))
return proposal, nil
}
// getAuctionTxFromVoteExtension extracts the auction transaction from the vote extension.
func (h *ProposalHandler) getAuctionTxFromVoteExtension(voteExtension []byte) (sdk.Tx, error) {
if len(voteExtension) == 0 {
return nil, fmt.Errorf("vote extension is empty")
}
// Attempt to unmarshal the auction transaction.
bidTx, err := h.txDecoder(voteExtension)
if err != nil {
return nil, err
}
// Verify the auction transaction has bid information.
if bidInfo, err := h.mempool.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil {
return nil, fmt.Errorf("vote extension does not contain an auction transaction")
}
return bidTx, nil
}

395
abci/auction.pb.go Normal file
View File

@ -0,0 +1,395 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: pob/abci/v1/auction.proto
package abci
import (
fmt "fmt"
proto "github.com/cosmos/gogoproto/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// AuctionInfo contains information about the top of block auction
// that was run in PrepareProposal using vote extensions.
type AuctionInfo struct {
// extended_commit_info contains the vote extensions that were used to run the auction.
ExtendedCommitInfo []byte `protobuf:"bytes,1,opt,name=extended_commit_info,json=extendedCommitInfo,proto3" json:"extended_commit_info,omitempty"`
// max_tx_bytes is the maximum number of bytes that were allowed for the proposal.
MaxTxBytes int64 `protobuf:"varint,2,opt,name=max_tx_bytes,json=maxTxBytes,proto3" json:"max_tx_bytes,omitempty"`
// num_txs is the number of transactions that were included in the proposal.
NumTxs uint64 `protobuf:"varint,3,opt,name=num_txs,json=numTxs,proto3" json:"num_txs,omitempty"`
}
func (m *AuctionInfo) Reset() { *m = AuctionInfo{} }
func (m *AuctionInfo) String() string { return proto.CompactTextString(m) }
func (*AuctionInfo) ProtoMessage() {}
func (*AuctionInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_ea32f9b647554bf5, []int{0}
}
func (m *AuctionInfo) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *AuctionInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_AuctionInfo.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *AuctionInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuctionInfo.Merge(m, src)
}
func (m *AuctionInfo) XXX_Size() int {
return m.Size()
}
func (m *AuctionInfo) XXX_DiscardUnknown() {
xxx_messageInfo_AuctionInfo.DiscardUnknown(m)
}
var xxx_messageInfo_AuctionInfo proto.InternalMessageInfo
func (m *AuctionInfo) GetExtendedCommitInfo() []byte {
if m != nil {
return m.ExtendedCommitInfo
}
return nil
}
func (m *AuctionInfo) GetMaxTxBytes() int64 {
if m != nil {
return m.MaxTxBytes
}
return 0
}
func (m *AuctionInfo) GetNumTxs() uint64 {
if m != nil {
return m.NumTxs
}
return 0
}
func init() {
proto.RegisterType((*AuctionInfo)(nil), "pob.abci.v1.AuctionInfo")
}
func init() { proto.RegisterFile("pob/abci/v1/auction.proto", fileDescriptor_ea32f9b647554bf5) }
var fileDescriptor_ea32f9b647554bf5 = []byte{
// 224 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2c, 0xc8, 0x4f, 0xd2,
0x4f, 0x4c, 0x4a, 0xce, 0xd4, 0x2f, 0x33, 0xd4, 0x4f, 0x2c, 0x4d, 0x2e, 0xc9, 0xcc, 0xcf, 0xd3,
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2e, 0xc8, 0x4f, 0xd2, 0x03, 0x49, 0xe9, 0x95, 0x19,
0x2a, 0x55, 0x71, 0x71, 0x3b, 0x42, 0x64, 0x3d, 0xf3, 0xd2, 0xf2, 0x85, 0x0c, 0xb8, 0x44, 0x52,
0x2b, 0x4a, 0x52, 0xf3, 0x52, 0x52, 0x53, 0xe2, 0x93, 0xf3, 0x73, 0x73, 0x33, 0x4b, 0xe2, 0x33,
0xf3, 0xd2, 0xf2, 0x25, 0x18, 0x15, 0x18, 0x35, 0x78, 0x82, 0x84, 0x60, 0x72, 0xce, 0x60, 0x29,
0xb0, 0x0e, 0x05, 0x2e, 0x9e, 0xdc, 0xc4, 0x8a, 0xf8, 0x92, 0x8a, 0xf8, 0xa4, 0xca, 0x92, 0xd4,
0x62, 0x09, 0x26, 0x05, 0x46, 0x0d, 0xe6, 0x20, 0xae, 0xdc, 0xc4, 0x8a, 0x90, 0x0a, 0x27, 0x90,
0x88, 0x90, 0x38, 0x17, 0x7b, 0x5e, 0x69, 0x6e, 0x7c, 0x49, 0x45, 0xb1, 0x04, 0xb3, 0x02, 0xa3,
0x06, 0x4b, 0x10, 0x5b, 0x5e, 0x69, 0x6e, 0x48, 0x45, 0xb1, 0x93, 0xd9, 0x89, 0x47, 0x72, 0x8c,
0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72,
0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xc9, 0xa4, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7,
0xe7, 0xea, 0x17, 0x67, 0x67, 0x16, 0xe8, 0xe6, 0xa6, 0x96, 0xe9, 0xc3, 0x7c, 0x94, 0xc4, 0x06,
0xf6, 0x87, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xce, 0x6f, 0x84, 0x78, 0xe4, 0x00, 0x00, 0x00,
}
func (m *AuctionInfo) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *AuctionInfo) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *AuctionInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.NumTxs != 0 {
i = encodeVarintAuction(dAtA, i, uint64(m.NumTxs))
i--
dAtA[i] = 0x18
}
if m.MaxTxBytes != 0 {
i = encodeVarintAuction(dAtA, i, uint64(m.MaxTxBytes))
i--
dAtA[i] = 0x10
}
if len(m.ExtendedCommitInfo) > 0 {
i -= len(m.ExtendedCommitInfo)
copy(dAtA[i:], m.ExtendedCommitInfo)
i = encodeVarintAuction(dAtA, i, uint64(len(m.ExtendedCommitInfo)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintAuction(dAtA []byte, offset int, v uint64) int {
offset -= sovAuction(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}
func (m *AuctionInfo) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.ExtendedCommitInfo)
if l > 0 {
n += 1 + l + sovAuction(uint64(l))
}
if m.MaxTxBytes != 0 {
n += 1 + sovAuction(uint64(m.MaxTxBytes))
}
if m.NumTxs != 0 {
n += 1 + sovAuction(uint64(m.NumTxs))
}
return n
}
func sovAuction(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
func sozAuction(x uint64) (n int) {
return sovAuction(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *AuctionInfo) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuction
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: AuctionInfo: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: AuctionInfo: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ExtendedCommitInfo", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuction
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
byteLen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if byteLen < 0 {
return ErrInvalidLengthAuction
}
postIndex := iNdEx + byteLen
if postIndex < 0 {
return ErrInvalidLengthAuction
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ExtendedCommitInfo = append(m.ExtendedCommitInfo[:0], dAtA[iNdEx:postIndex]...)
if m.ExtendedCommitInfo == nil {
m.ExtendedCommitInfo = []byte{}
}
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxTxBytes", wireType)
}
m.MaxTxBytes = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuction
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.MaxTxBytes |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field NumTxs", wireType)
}
m.NumTxs = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAuction
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.NumTxs |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipAuction(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthAuction
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipAuction(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAuction
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAuction
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
case 1:
iNdEx += 8
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAuction
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthAuction
}
iNdEx += length
case 3:
depth++
case 4:
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupAuction
}
depth--
case 5:
iNdEx += 4
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthAuction
}
if depth == 0 {
return iNdEx, nil
}
}
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthAuction = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowAuction = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupAuction = fmt.Errorf("proto: unexpected end of group")
)

512
abci/auction_test.go Normal file
View File

@ -0,0 +1,512 @@
package abci_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"
testutils "github.com/skip-mev/pob/testutils"
buildertypes "github.com/skip-mev/pob/x/builder/types"
)
func (suite *ABCITestSuite) TestGetBidsFromVoteExtensions() {
testCases := []struct {
name string
createVoteExtensions func() ([][]byte, [][]byte) // returns (vote extensions, expected bids)
}{
{
"no vote extensions",
func() ([][]byte, [][]byte) {
return nil, [][]byte{}
},
},
{
"no vote extensions",
func() ([][]byte, [][]byte) {
return [][]byte{}, [][]byte{}
},
},
{
"single vote extension",
func() ([][]byte, [][]byte) {
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
voteExtensions := [][]byte{
bidTxBz,
}
expectedBids := [][]byte{
bidTxBz,
}
return voteExtensions, expectedBids
},
},
{
"multiple vote extensions",
func() ([][]byte, [][]byte) {
bidTxBz1, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
voteExtensions := [][]byte{
bidTxBz1,
bidTxBz2,
}
expectedBids := [][]byte{
bidTxBz1,
bidTxBz2,
}
return voteExtensions, expectedBids
},
},
{
"multiple vote extensions with some noise",
func() ([][]byte, [][]byte) {
bidTxBz1, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
voteExtensions := [][]byte{
bidTxBz1,
nil,
bidTxBz2,
[]byte("noise"),
[]byte("noise p2"),
}
expectedBids := [][]byte{
bidTxBz1,
bidTxBz2,
}
return voteExtensions, expectedBids
},
},
{
"multiple vote extensions with some normal txs",
func() ([][]byte, [][]byte) {
bidTxBz1, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
randomBz, err := testutils.CreateRandomTxBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
0,
1,
0,
)
suite.Require().NoError(err)
voteExtensions := [][]byte{
bidTxBz1,
bidTxBz2,
nil,
randomBz,
[]byte("noise p2"),
}
expectedBids := [][]byte{
bidTxBz1,
bidTxBz2,
}
return voteExtensions, expectedBids
},
},
{
"multiple vote extensions with some normal txs in unsorted order",
func() ([][]byte, [][]byte) {
bidTxBz1, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(1001)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
randomBz, err := testutils.CreateRandomTxBz(
suite.encodingConfig.TxConfig,
suite.accounts[0],
0,
1,
0,
)
suite.Require().NoError(err)
voteExtensions := [][]byte{
bidTxBz2,
bidTxBz1,
nil,
randomBz,
[]byte("noise p2"),
}
expectedBids := [][]byte{
bidTxBz1,
bidTxBz2,
}
return voteExtensions, expectedBids
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
voteExtensions, expectedBids := tc.createVoteExtensions()
commitInfo := suite.createExtendedVoteInfo(voteExtensions)
// get the bids from the vote extensions
bids := suite.proposalHandler.GetBidsFromVoteExtensions(commitInfo)
// Check invarients
suite.Require().Equal(len(expectedBids), len(bids))
for i, bid := range expectedBids {
actualBz, err := suite.encodingConfig.TxConfig.TxEncoder()(bids[i])
suite.Require().NoError(err)
suite.Require().Equal(bid, actualBz)
}
})
}
}
func (suite *ABCITestSuite) TestBuildTOB() {
params := buildertypes.Params{
MaxBundleSize: 4,
MinBuyInFee: sdk.NewCoin("foo", sdk.NewInt(100)),
ReserveFee: sdk.NewCoin("foo", sdk.NewInt(100)),
MinBidIncrement: sdk.NewCoin("foo", sdk.NewInt(100)),
FrontRunningProtection: true,
}
suite.builderKeeper.SetParams(suite.ctx, params)
testCases := []struct {
name string
getBidTxs func() ([]sdk.Tx, sdk.Tx) // returns the bids and the winning bid
maxBytes int64
}{
{
"no bids",
func() ([]sdk.Tx, sdk.Tx) {
return []sdk.Tx{}, nil
},
1000000000,
},
{
"single bid",
func() ([]sdk.Tx, sdk.Tx) {
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx}, bidTx
},
1000000000,
},
{
"single invalid bid (bid is too small)",
func() ([]sdk.Tx, sdk.Tx) {
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(1)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx}, nil
},
1000000000,
},
{
"single invalid bid with front-running",
func() ([]sdk.Tx, sdk.Tx) {
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(1000)),
0,
1,
[]testutils.Account{suite.accounts[0], suite.accounts[1]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx}, nil
},
1000000000,
},
{
"single invalid bid with too many transactions in the bundle",
func() ([]sdk.Tx, sdk.Tx) {
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx}, nil
},
1000000000,
},
{
"single bid but max bytes is too small",
func() ([]sdk.Tx, sdk.Tx) {
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx}, nil
},
1,
},
{
"multiple bids",
func() ([]sdk.Tx, sdk.Tx) {
bidTx1, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("foo", sdk.NewInt(102)),
0,
1,
[]testutils.Account{suite.accounts[1]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx2, bidTx1}, bidTx2
},
1000000000,
},
{
"multiple bids with front-running",
func() ([]sdk.Tx, sdk.Tx) {
bidTx1, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(1000)),
0,
1,
[]testutils.Account{suite.accounts[0], suite.accounts[1]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("foo", sdk.NewInt(200)),
0,
1,
[]testutils.Account{suite.accounts[1]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx1, bidTx2}, bidTx2
},
1000000000,
},
{
"multiple bids with too many transactions in the bundle",
func() ([]sdk.Tx, sdk.Tx) {
bidTx1, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
suite.accounts[0],
},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("foo", sdk.NewInt(102)),
0,
1,
[]testutils.Account{suite.accounts[1]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx1, bidTx2}, bidTx2
},
1000000000,
},
{
"multiple bids unsorted",
func() ([]sdk.Tx, sdk.Tx) {
bidTx1, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(101)),
0,
1,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("foo", sdk.NewInt(102)),
0,
1,
[]testutils.Account{suite.accounts[1]},
)
suite.Require().NoError(err)
return []sdk.Tx{bidTx1, bidTx2}, bidTx2
},
1000000000,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
bidTxs, winningBid := tc.getBidTxs()
commitInfo := suite.createExtendedCommitInfoFromTxs(bidTxs)
// Host the auction
proposal := suite.proposalHandler.BuildTOB(suite.ctx, commitInfo, tc.maxBytes)
// Size of the proposal should be less than or equal to the max bytes
suite.Require().LessOrEqual(proposal.Size, tc.maxBytes)
if winningBid == nil {
suite.Require().Len(proposal.Txs, 0)
suite.Require().Equal(proposal.Size, int64(0))
} else {
// Get info about the winning bid
winningBidBz, err := suite.encodingConfig.TxConfig.TxEncoder()(winningBid)
suite.Require().NoError(err)
auctionBidInfo, err := suite.mempool.GetAuctionBidInfo(winningBid)
suite.Require().NoError(err)
// Verify that the size of the proposal is the size of the winning bid
// plus the size of the bundle
suite.Require().Equal(len(proposal.Txs), len(auctionBidInfo.Transactions)+1)
// Verify that the winning bid is the first transaction in the proposal
suite.Require().Equal(proposal.Txs[0], winningBidBz)
// Verify the ordering of transactions in the proposal
for index, tx := range proposal.Txs[1:] {
suite.Equal(tx, auctionBidInfo.Transactions[index])
}
}
})
}
}

View File

@ -1,7 +1,6 @@
package abci
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
@ -12,7 +11,17 @@ 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"
mempool "github.com/skip-mev/pob/mempool"
)
const (
// NumInjectedTxs is the minimum number of transactions that were injected into
// the proposal but are not actual transactions. In this case, the auction
// info is injected into the proposal but should be ignored by the application.ß
NumInjectedTxs = 1
// AuctionInfoIndex is the index of the auction info in the proposal.
AuctionInfoIndex = 0
)
type (
@ -63,132 +72,72 @@ func NewProposalHandler(
// top-of-block auctioning and general block proposal construction.
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
return func(ctx sdk.Context, req abci.RequestPrepareProposal) abci.ResponsePrepareProposal {
var (
selectedTxs [][]byte
totalTxBytes int64
)
// Proposal includes all of the transactions that will be included in the
// block along with the vote extensions from the previous block included at
// the beginning of the proposal. Vote extensions must be included in the
// first slot of the proposal because they are inaccessible in ProcessProposal.
proposal := make([][]byte, 0)
bidTxIterator := h.mempool.AuctionBidSelect(ctx)
txsToRemove := make(map[sdk.Tx]struct{}, 0)
seenTxs := make(map[string]struct{}, 0)
// Build the top of block portion of the proposal given the vote extensions
// from the previous block.
topOfBlock := h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes)
// Attempt to select the highest bid transaction that is valid and whose
// bundled transactions are valid.
selectBidTxLoop:
for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() {
cacheCtx, write := ctx.CacheContext()
tmpBidTx := bidTxIterator.Tx()
bidTxBz, err := h.PrepareProposalVerifyTx(cacheCtx, tmpBidTx)
if err != nil {
txsToRemove[tmpBidTx] = struct{}{}
continue selectBidTxLoop
}
bidTxSize := int64(len(bidTxBz))
if bidTxSize <= req.MaxTxBytes {
bidInfo, err := h.mempool.GetAuctionBidInfo(tmpBidTx)
if err != nil {
// Some transactions in the bundle may be malformatted or invalid, so
// we remove the bid transaction and try the next top bid.
txsToRemove[tmpBidTx] = struct{}{}
continue selectBidTxLoop
}
// store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal
bundledTransactions := bidInfo.Transactions
sdkTxBytes := make([][]byte, len(bundledTransactions))
// Ensure that the bundled transactions are valid
for index, rawRefTx := range bundledTransactions {
refTx, err := h.mempool.WrapBundleTransaction(rawRefTx)
if err != nil {
// Malformed bundled transaction, so we remove the bid transaction
// and try the next top bid.
txsToRemove[tmpBidTx] = struct{}{}
continue selectBidTxLoop
}
txBz, err := h.PrepareProposalVerifyTx(cacheCtx, refTx)
if err != nil {
// Invalid bundled transaction, so we remove the bid transaction
// and try the next top bid.
txsToRemove[tmpBidTx] = struct{}{}
continue selectBidTxLoop
}
sdkTxBytes[index] = txBz
}
// At this point, both the bid transaction itself and all the bundled
// transactions are valid. So we select the bid transaction along with
// all the bundled transactions. We also mark these transactions as seen and
// update the total size selected thus far.
totalTxBytes += bidTxSize
selectedTxs = append(selectedTxs, bidTxBz)
selectedTxs = append(selectedTxs, sdkTxBytes...)
for _, refTxRaw := range sdkTxBytes {
hash := sha256.Sum256(refTxRaw)
txHash := hex.EncodeToString(hash[:])
seenTxs[txHash] = struct{}{}
}
// Write the cache context to the original context when we know we have a
// valid top of block bundle.
write()
break selectBidTxLoop
}
txsToRemove[tmpBidTx] = struct{}{}
h.logger.Info(
"failed to select auction bid tx; tx size is too large",
"tx_size", bidTxSize,
"max_size", req.MaxTxBytes,
)
// If information is unable to be marshaled, we return an empty proposal. This will
// cause another proposal to be generated after it is rejected in ProcessProposal.
lastCommitInfo, err := req.LocalLastCommit.Marshal()
if err != nil {
return abci.ResponsePrepareProposal{Txs: proposal}
}
// Remove all invalid transactions from the mempool.
for tx := range txsToRemove {
h.RemoveTx(tx)
auctionInfo := AuctionInfo{
ExtendedCommitInfo: lastCommitInfo,
MaxTxBytes: req.MaxTxBytes,
NumTxs: uint64(len(topOfBlock.Txs)),
}
iterator := h.mempool.Select(ctx, nil)
txsToRemove = map[sdk.Tx]struct{}{}
// Add the auction info and top of block transactions into the proposal.
auctionInfoBz, err := auctionInfo.Marshal()
if err != nil {
return abci.ResponsePrepareProposal{Txs: proposal}
}
proposal = append(proposal, auctionInfoBz)
proposal = append(proposal, topOfBlock.Txs...)
// Select remaining transactions for the block proposal until we've reached
// size capacity.
selectTxLoop:
for ; iterator != nil; iterator = iterator.Next() {
totalTxBytes := topOfBlock.Size
txsToRemove := make(map[sdk.Tx]struct{}, 0)
for iterator := h.mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
memTx := iterator.Tx()
// If the transaction is already included in the proposal, then we skip it.
// If the transaction has already been seen in the top of block, skip it.
txBz, err := h.txEncoder(memTx)
if err != nil {
txsToRemove[memTx] = struct{}{}
continue selectTxLoop
continue
}
hash := sha256.Sum256(txBz)
txHash := hex.EncodeToString(hash[:])
if _, ok := seenTxs[txHash]; ok {
continue selectTxLoop
hashBz := sha256.Sum256(txBz)
hash := hex.EncodeToString(hashBz[:])
if _, ok := topOfBlock.Cache[hash]; ok {
continue
}
// Verify that the transaction is valid.
txBz, err = h.PrepareProposalVerifyTx(ctx, memTx)
if err != nil {
txsToRemove[memTx] = struct{}{}
continue selectTxLoop
continue
}
txSize := int64(len(txBz))
if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes {
selectedTxs = append(selectedTxs, txBz)
proposal = append(proposal, txBz)
} else {
// We've reached capacity per req.MaxTxBytes so we cannot select any
// more transactions.
break selectTxLoop
break
}
}
@ -197,7 +146,7 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
h.RemoveTx(tx)
}
return abci.ResponsePrepareProposal{Txs: selectedTxs}
return abci.ResponsePrepareProposal{Txs: proposal}
}
}
@ -205,50 +154,44 @@ 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 {
for index, txBz := range req.Txs {
proposal := req.Txs
// Verify that the same top of block transactions can be built from the vote
// extensions included in the proposal.
auctionInfo, err := h.VerifyTOB(ctx, proposal)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
// Track the transactions that need to be removed from the mempool.
txsToRemove := make(map[sdk.Tx]struct{}, 0)
invalidProposal := false
// Verify that the remaining transactions in the proposal are valid.
for _, txBz := range proposal[auctionInfo.NumTxs+NumInjectedTxs:] {
tx, err := h.ProcessProposalVerifyTx(ctx, txBz)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
bidInfo, err := h.mempool.GetAuctionBidInfo(tx)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
// If the transaction is an auction bid, then we need to ensure that it is
// the first transaction in the block proposal and that the order of
// transactions in the block proposal follows the order of transactions in
// the bid.
if bidInfo != nil {
if index != 0 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
if tx == nil || err != nil {
invalidProposal = true
if tx != nil {
txsToRemove[tx] = struct{}{}
}
bundledTransactions := bidInfo.Transactions
if len(req.Txs) < len(bundledTransactions)+1 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
for i, refTxRaw := range bundledTransactions {
// Wrap and then encode the bundled transaction to ensure that the underlying
// reference transaction can be processed as an sdk.Tx.
wrappedTx, err := h.mempool.WrapBundleTransaction(refTxRaw)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
refTxBz, err := h.txEncoder(wrappedTx)
if err != nil {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
if !bytes.Equal(refTxBz, req.Txs[i+1]) {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
}
continue
}
// The only auction transactions that should be included in the block proposal
// must be at the top of the block.
if bidInfo, err := h.mempool.GetAuctionBidInfo(tx); err != nil || bidInfo != nil {
invalidProposal = true
}
}
// Remove all invalid transactions from the mempool.
for tx := range txsToRemove {
h.RemoveTx(tx)
}
if invalidProposal {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
}
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}

View File

@ -1,9 +1,7 @@
package abci_test
import (
"bytes"
abcitypes "github.com/cometbft/cometbft/abci/types"
comettypes "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/abci"
testutils "github.com/skip-mev/pob/testutils"
@ -17,10 +15,11 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
maxTxBytes int64 = 1000000000000000000
// mempool configuration
numNormalTxs = 100
numAuctionTxs = 100
numBundledTxs = 3
insertRefTxs = false
numNormalTxs = 100
numAuctionTxs = 100
numBundledTxs = 3
insertRefTxs = false
expectedTopAuctionTx sdk.Tx
// auction configuration
maxBundleSize uint32 = 10
@ -30,11 +29,11 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
)
cases := []struct {
name string
malleate func()
expectedNumberProposalTxs int
expectedNumberTxsInMempool int
isTopBidValid bool
name string
malleate func()
expectedNumberProposalTxs int
expectedNumberTxsInMempool int
expectedNumberTxsInAuctionMempool int
}{
{
"single bundle in the mempool",
@ -43,10 +42,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
4,
5,
3,
true,
1,
},
{
"single bundle in the mempool, no ref txs in mempool",
@ -55,10 +58,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
4,
5,
0,
true,
1,
},
{
"single bundle in the mempool, not valid",
@ -68,10 +75,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 0
numAuctionTxs = 1
numBundledTxs = 3
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = nil
},
1,
0,
0,
false,
},
{
"single bundle in the mempool, not valid with ref txs in mempool",
@ -82,10 +93,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = nil
},
4,
3,
3,
false,
0,
},
{
"multiple bundles in the mempool, no normal txs + no ref txs in mempool",
@ -96,22 +111,30 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numAuctionTxs = 10
numBundledTxs = 3
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
4,
5,
0,
true,
10,
},
{
"multiple bundles in the mempool, no normal txs + ref txs in mempool",
"multiple bundles in the mempool, normal txs + ref txs in mempool",
func() {
numNormalTxs = 0
numAuctionTxs = 10
numBundledTxs = 3
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
31,
32,
30,
true,
10,
},
{
"normal txs only",
@ -119,10 +142,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 1
numAuctionTxs = 0
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
2,
1,
1,
false,
0,
},
{
"many normal txs only",
@ -130,10 +157,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 100
numAuctionTxs = 0
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
101,
100,
100,
false,
0,
},
{
"single normal tx, single auction tx",
@ -141,10 +172,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 1
numAuctionTxs = 1
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
2,
3,
1,
1,
true,
},
{
"single normal tx, single auction tx with ref txs",
@ -152,11 +187,15 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 1
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = false
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
5,
6,
4,
1,
true,
},
{
"single normal tx, single failing auction tx with ref txs",
@ -167,10 +206,14 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
insertRefTxs = true
suite.auctionBidAmount = sdk.NewCoin("foo", sdk.NewInt(2000)) // this will fail the ante handler
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000000000))
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = nil
},
5,
4,
4,
false,
0,
},
{
"many normal tx, single auction tx with no ref txs",
@ -180,57 +223,72 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = nil
},
101,
102,
100,
true,
1,
},
{
"many normal tx, single auction tx with ref txs",
func() {
numNormalTxs = 100
numAuctionTxs = 1
numAuctionTxs = 100
numBundledTxs = 3
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
},
104,
103,
true,
},
{
"many normal tx, single auction tx with ref txs",
func() {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = false
},
104,
402,
400,
100,
true,
},
{
"many normal tx, many auction tx with ref txs",
"many normal tx, many auction tx with ref txs but top bid is invalid",
func() {
numNormalTxs = 100
numAuctionTxs = 100
numBundledTxs = 1
insertRefTxs = true
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
expectedTopAuctionTx = suite.mempool.GetTopAuctionTx(suite.ctx)
// create a new bid that is greater than the current top bid
bid := sdk.NewCoin("foo", sdk.NewInt(200000000000000000))
bidTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
bid,
0,
0,
[]testutils.Account{suite.accounts[0], suite.accounts[1]},
)
suite.Require().NoError(err)
// add the new bid to the mempool
err = suite.mempool.Insert(suite.ctx, bidTx)
suite.Require().NoError(err)
suite.Require().Equal(suite.mempool.CountAuctionTx(), 101)
},
201,
202,
200,
true,
100,
},
}
for _, tc := range cases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
tc.malleate()
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
// create a new auction
// Create a new auction.
params := buildertypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
@ -241,59 +299,54 @@ func (suite *ABCITestSuite) TestPrepareProposal() {
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxDecoder(), suite.encodingConfig.TxConfig.TxEncoder(), suite.mempool)
// reset the proposal handler with the new mempool
// Reset the proposal handler with the new mempool.
suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())
// Create a prepare proposal request based on the current state of the mempool.
handler := suite.proposalHandler.PrepareProposalHandler()
res := handler(suite.ctx, abcitypes.RequestPrepareProposal{
MaxTxBytes: maxTxBytes,
})
req := suite.createPrepareProposalRequest(maxTxBytes)
res := handler(suite.ctx, req)
// -------------------- Check Invariants -------------------- //
// 1. The auction tx must fail if we know it is invalid
suite.Require().Equal(tc.isTopBidValid, suite.isTopBidValid())
// The first slot in the proposal must be the auction info
auctionInfo := abci.AuctionInfo{}
err := auctionInfo.Unmarshal(res.Txs[abci.AuctionInfoIndex])
suite.Require().NoError(err)
// 2. total bytes must be less than or equal to maxTxBytes
// Total bytes must be less than or equal to maxTxBytes
totalBytes := int64(0)
if suite.isTopBidValid() {
totalBytes += int64(len(res.Txs[0]))
for _, tx := range res.Txs[1+numBundledTxs:] {
totalBytes += int64(len(tx))
}
} else {
for _, tx := range res.Txs {
totalBytes += int64(len(tx))
}
for _, tx := range res.Txs[abci.NumInjectedTxs:] {
totalBytes += int64(len(tx))
}
suite.Require().LessOrEqual(totalBytes, maxTxBytes)
// 3. the number of transactions in the response must be equal to the number of expected transactions
// The number of transactions in the response must be equal to the number of expected transactions
suite.Require().Equal(tc.expectedNumberProposalTxs, len(res.Txs))
// 4. if there are auction transactions, the first transaction must be the top bid
// If there are auction transactions, the first transaction must be the top bid
// and the rest of the bundle must be in the response
if suite.isTopBidValid() {
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[0])
if expectedTopAuctionTx != nil {
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
suite.Require().NoError(err)
bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTx)
suite.Require().NoError(err)
for index, tx := range bidInfo.Transactions {
suite.Require().Equal(tx, res.Txs[index+1])
suite.Require().Equal(tx, res.Txs[abci.NumInjectedTxs+index+1])
}
}
// 5. All of the transactions must be unique
uniqueTxs := make(map[string]bool)
for _, tx := range res.Txs {
for _, tx := range res.Txs[abci.NumInjectedTxs:] {
suite.Require().False(uniqueTxs[string(tx)])
uniqueTxs[string(tx)] = true
}
// 6. The number of transactions in the mempool must be correct
suite.Require().Equal(tc.expectedNumberTxsInMempool, suite.mempool.CountTx())
suite.Require().Equal(tc.expectedNumberTxsInAuctionMempool, suite.mempool.CountAuctionTx())
})
}
}
@ -301,213 +354,412 @@ 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
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 3
insertRefTxs = false
// auction set up
maxBundleSize uint32 = 10
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
minBuyInFee = sdk.NewCoin("foo", sdk.NewInt(1000))
frontRunningProtection = true
maxBundleSize uint32 = 10
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
minBuyInFee = sdk.NewCoin("foo", sdk.NewInt(1000))
)
cases := []struct {
name string
malleate func()
isTopBidValid bool
response abcitypes.ResponseProcessProposal_ProposalStatus
name string
createTxs func() [][]byte
response comettypes.ResponseProcessProposal_ProposalStatus
}{
{
"single normal tx, no auction tx",
func() {
"single normal tx, no vote extension info",
func() [][]byte {
numNormalTxs = 1
numAuctionTxs = 0
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
txs := suite.exportMempool()
return txs
},
false,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, no normal txs",
func() {
"single auction tx, no vote extension info",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 1
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
return suite.exportMempool()
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, single auction tx",
func() {
"single auction tx, single auction tx, no vote extension info",
func() [][]byte {
numNormalTxs = 1
numAuctionTxs = 1
numBundledTxs = 0
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
return suite.exportMempool()
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, single auction tx with ref txs",
func() {
"single auction tx with ref txs (no unwrapping)",
func() [][]byte {
numNormalTxs = 1
numAuctionTxs = 1
numBundledTxs = 4
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(topAuctionTx)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
proposal := append([][]byte{
auctionInfo,
txBz,
}, suite.exportMempool()...)
return proposal
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, single auction tx with no ref txs",
func() {
numNormalTxs = 1
"single auction tx with ref txs (with unwrapping)",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 1
numBundledTxs = 4
insertRefTxs = false
exportRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(topAuctionTx)
bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx)
suite.Require().NoError(err)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
proposal := append([][]byte{
auctionInfo,
txBz,
}, bidInfo.Transactions...)
return proposal
},
true,
abcitypes.ResponseProcessProposal_REJECT,
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"multiple auction txs, single normal tx",
func() {
numNormalTxs = 1
"single auction tx but no inclusion of ref txs",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 1
numBundledTxs = 4
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(topAuctionTx)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 5)
return [][]byte{
auctionInfo,
txBz,
}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, but auction tx is not valid",
func() [][]byte {
tx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
1,
0, // invalid timeout
[]testutils.Account{},
)
suite.Require().NoError(err)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
return [][]byte{
auctionInfoBz,
txBz,
}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx with ref txs, but auction tx is not valid",
func() [][]byte {
tx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(100)),
1,
1,
[]testutils.Account{suite.accounts[1], suite.accounts[1], suite.accounts[0]},
)
suite.Require().NoError(err)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(tx)
suite.Require().NoError(err)
auctionInfoBz := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 4)
bidInfo, err := suite.mempool.GetAuctionBidInfo(tx)
suite.Require().NoError(err)
return append([][]byte{
auctionInfoBz,
txBz,
}, bidInfo.Transactions...)
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"multiple auction txs but wrong auction tx is at top of block",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 2
numBundledTxs = 4
insertRefTxs = true
exportRefTxs = true
numBundledTxs = 0
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
_, auctionTxBzs := suite.getAllAuctionTxs()
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
proposal := [][]byte{
auctionInfo,
auctionTxBzs[1],
}
return proposal
},
true,
abcitypes.ResponseProcessProposal_REJECT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction txs, multiple normal tx",
func() {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 4
"multiple auction txs included in block",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 2
numBundledTxs = 0
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
_, auctionTxBzs := suite.getAllAuctionTxs()
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
proposal := [][]byte{
auctionInfo,
auctionTxBzs[0],
auctionTxBzs[1],
}
return proposal
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single invalid auction tx, multiple normal tx",
func() {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 4
reserveFee = sdk.NewCoin("foo", sdk.NewInt(100000000000000000))
insertRefTxs = true
},
false,
abcitypes.ResponseProcessProposal_REJECT,
},
{
"single valid auction txs but missing ref txs",
func() {
"single auction tx, but rest of the mempool is invalid",
func() [][]byte {
numNormalTxs = 0
numAuctionTxs = 1
numBundledTxs = 4
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
numBundledTxs = 0
insertRefTxs = false
exportRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(topAuctionTx)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
proposal := [][]byte{
auctionInfo,
txBz,
[]byte("invalid tx"),
}
return proposal
},
true,
abcitypes.ResponseProcessProposal_REJECT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"single valid auction txs but missing ref txs, with many normal txs",
func() {
"single auction tx with filled mempool, but rest of the mempool is invalid",
func() [][]byte {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 4
reserveFee = sdk.NewCoin("foo", sdk.NewInt(1000))
numBundledTxs = 0
insertRefTxs = false
exportRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx := suite.mempool.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(topAuctionTx)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 1)
proposal := append([][]byte{
auctionInfo,
txBz,
}, suite.exportMempool()...)
proposal = append(proposal, []byte("invalid tx"))
return proposal
},
true,
abcitypes.ResponseProcessProposal_REJECT,
comettypes.ResponseProcessProposal_REJECT,
},
{
"auction tx with frontrunning",
func() {
randomAccount := testutils.RandomAccounts(suite.random, 1)[0]
bidder := suite.accounts[0]
bid := sdk.NewCoin("foo", sdk.NewInt(696969696969))
nonce := suite.nonces[bidder.Address.String()]
frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount})
suite.Require().NotNil(frontRunningTx)
"multiple auction txs with filled mempool",
func() [][]byte {
numNormalTxs = 100
numAuctionTxs = 1
numBundledTxs = 4
insertRefTxs = true
exportRefTxs = true
numAuctionTxs = 10
numBundledTxs = 0
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
_, auctionTxBzs := suite.getAllAuctionTxs()
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 1)
proposal := append([][]byte{
auctionInfo,
auctionTxBzs[0],
}, suite.exportMempool()...)
return proposal
},
false,
abcitypes.ResponseProcessProposal_REJECT,
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"auction tx with frontrunning, but frontrunning protection disabled",
func() {
randomAccount := testutils.RandomAccounts(suite.random, 1)[0]
bidder := suite.accounts[0]
bid := sdk.NewCoin("foo", sdk.NewInt(696969696969))
nonce := suite.nonces[bidder.Address.String()]
frontRunningTx, _ = testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, suite.accounts[0], bid, nonce+1, 1000, []testutils.Account{bidder, randomAccount})
suite.Require().NotNil(frontRunningTx)
"multiple auction txs with ref txs + filled mempool",
func() [][]byte {
numNormalTxs = 100
numAuctionTxs = 10
numBundledTxs = 10
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
auctionTxs, auctionTxBzs := suite.getAllAuctionTxs()
auctionInfo := suite.createAuctionInfoFromTxBzs(auctionTxBzs, 11)
bidInfo, err := suite.mempool.GetAuctionBidInfo(auctionTxs[0])
suite.Require().NoError(err)
proposal := append([][]byte{
auctionInfo,
auctionTxBzs[0],
}, bidInfo.Transactions...)
proposal = append(proposal, suite.exportMempool()...)
return proposal
},
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"auction tx with front-running",
func() [][]byte {
numNormalTxs = 100
numAuctionTxs = 0
frontRunningProtection = false
numBundledTxs = 0
insertRefTxs = false
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
topAuctionTx, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[0],
sdk.NewCoin("foo", sdk.NewInt(1000000)),
0,
1,
[]testutils.Account{suite.accounts[0], suite.accounts[1]}, // front-running
)
suite.Require().NoError(err)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(topAuctionTx)
suite.Require().NoError(err)
bidInfo, err := suite.mempool.GetAuctionBidInfo(topAuctionTx)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{txBz}, 3)
proposal := append([][]byte{
auctionInfo,
txBz,
}, bidInfo.Transactions...)
proposal = append(proposal, suite.exportMempool()...)
return proposal
},
true,
abcitypes.ResponseProcessProposal_ACCEPT,
comettypes.ResponseProcessProposal_REJECT,
},
}
for _, tc := range cases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
tc.malleate()
suite.createFilledMempool(numNormalTxs, numAuctionTxs, numBundledTxs, insertRefTxs)
// reset the proposal handler with the new mempool
suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())
if frontRunningTx != nil {
suite.Require().NoError(suite.mempool.Insert(suite.ctx, frontRunningTx))
}
// create a new auction
params := buildertypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
MinBuyInFee: minBuyInFee,
FrontRunningProtection: frontRunningProtection,
FrontRunningProtection: true,
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.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))
}
// reset the proposal handler with the new mempool
suite.proposalHandler = abci.NewProposalHandler(suite.mempool, suite.logger, suite.anteHandler, suite.encodingConfig.TxConfig.TxEncoder(), suite.encodingConfig.TxConfig.TxDecoder())
handler := suite.proposalHandler.ProcessProposalHandler()
res := handler(suite.ctx, abcitypes.RequestProcessProposal{
Txs: txs,
res := handler(suite.ctx, comettypes.RequestProcessProposal{
Txs: tc.createTxs(),
})
// Check if the response is valid
@ -515,16 +767,3 @@ func (suite *ABCITestSuite) TestProcessProposal() {
})
}
}
// isTopBidValid returns true if the top bid is valid. We purposefully insert invalid
// auction transactions into the mempool to test the handlers.
func (suite *ABCITestSuite) isTopBidValid() bool {
iterator := suite.mempool.AuctionBidSelect(suite.ctx)
if iterator == nil {
return false
}
// check if the top bid is valid
_, err := suite.anteHandler(suite.ctx, iterator.Tx(), true)
return err == nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
syntax = "proto3";
package pob.abci.v1;
option go_package = "github.com/skip-mev/pob/abci";
// AuctionInfo contains information about the top of block auction
// that was run in PrepareProposal using vote extensions.
message AuctionInfo {
// extended_commit_info contains the vote extensions that were used to run the auction.
bytes extended_commit_info = 1;
// max_tx_bytes is the maximum number of bytes that were allowed for the proposal.
int64 max_tx_bytes = 2;
// num_txs is the number of transactions that were included in the proposal.
uint64 num_txs = 3;
}

View File

@ -125,6 +125,15 @@ func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, t
return txBuilder.GetTx(), nil
}
func CreateRandomTxBz(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64) ([]byte, error) {
tx, err := CreateRandomTx(txCfg, account, nonce, numberMsgs, timeout)
if err != nil {
return nil, err
}
return txCfg.TxEncoder()(tx)
}
func CreateTxWithSigners(txCfg client.TxConfig, nonce, timeout uint64, signers []Account) (authsigning.Tx, error) {
msgs := []sdk.Msg{}
for _, signer := range signers {
@ -199,6 +208,20 @@ func CreateAuctionTxWithSigners(txCfg client.TxConfig, bidder Account, bid sdk.C
return txBuilder.GetTx(), nil
}
func CreateAuctionTxWithSignerBz(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, timeout uint64, signers []Account) ([]byte, error) {
bidTx, err := CreateAuctionTxWithSigners(txCfg, bidder, bid, nonce, timeout, signers)
if err != nil {
return nil, err
}
bz, err := txCfg.TxEncoder()(bidTx)
if err != nil {
return nil, err
}
return bz, nil
}
func CreateRandomMsgs(acc sdk.AccAddress, numberMsgs int) []sdk.Msg {
msgs := make([]sdk.Msg, numberMsgs)
for i := 0; i < numberMsgs; i++ {