feat(ITS): [ENG-1627]: Moving ITS from release branch to main (#236)

Co-authored-by: Nikhil <nikhil@skip.money>
This commit is contained in:
David Terpay 2023-08-11 14:22:39 -04:00 committed by GitHub
parent 14f83b5e25
commit 1ed1adc856
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 3771 additions and 6086 deletions

View File

@ -64,4 +64,4 @@ jobs:
- name: Test E2E
if: env.GIT_DIFF
run: |
make test-e2e
make test-integration

View File

@ -85,29 +85,46 @@ build-and-start-app: build-test-app
.PHONY: build-test-app build-and-start-app
###############################################################################
## Workspaces ##
###############################################################################
use-main:
go work edit -use .
go work edit -dropuse ./tests/integration
use-integration:
go work edit -dropuse .
go work edit -use ./tests/integration
.PHONY: docker-build docker-build-integration
###############################################################################
## Docker ##
###############################################################################
docker-build:
docker-build: use-main
@echo "Building E2E Docker image..."
@DOCKER_BUILDKIT=1 docker build -t skip-mev/pob-e2e -f contrib/images/pob.e2e.Dockerfile .
docker-build-integration: use-main
@echo "Building integration-test Docker image..."
@DOCKER_BUILDKIT=1 docker build -t pob-integration -f contrib/images/pob.integration.Dockerfile .
###############################################################################
### Tests ###
###############################################################################
TEST_E2E_TAGS = e2e
TEST_E2E_DEPS = docker-build
TEST_INTEGRATION_DEPS = docker-build-integration use-integration
TEST_INTEGRATION_TAGS = integration
test-e2e: $(TEST_E2E_DEPS)
@echo "Running E2E tests..."
@go test ./tests/e2e/... -mod=readonly -timeout 30m -race -v -tags='$(TEST_E2E_TAGS)'
test-integration: $(TEST_INTEGRATION_DEPS)
@ echo "Running integration tests..."
@go test ./tests/integration/pob_integration_test.go -timeout 30m -race -v -tags='$(TEST_INTEGRATION_TAGS)'
test:
test: use-main
@go test -v -race $(shell go list ./... | grep -v tests/)
.PHONY: test test-e2e
.PHONY: test test-integration
###############################################################################
### Protobuf ###
@ -149,12 +166,12 @@ proto-update-deps:
golangci_lint_cmd=golangci-lint
golangci_version=v1.51.2
lint:
lint: use-main
@echo "--> Running linter"
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version)
@golangci-lint run
lint-fix:
lint-fix: use-main
@echo "--> Running linter"
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version)
@golangci-lint run --fix

View File

@ -1,317 +0,0 @@
package abci_test
import (
"math/rand"
"testing"
"time"
"cosmossdk.io/log"
"cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"
comettypes "github.com/cometbft/cometbft/abci/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"
"github.com/skip-mev/pob/abci"
"github.com/skip-mev/pob/blockbuster"
"github.com/skip-mev/pob/blockbuster/lanes/auction"
"github.com/skip-mev/pob/blockbuster/lanes/base"
testutils "github.com/skip-mev/pob/testutils"
"github.com/skip-mev/pob/x/builder/ante"
"github.com/skip-mev/pob/x/builder/keeper"
buildertypes "github.com/skip-mev/pob/x/builder/types"
"github.com/stretchr/testify/suite"
)
type ABCITestSuite struct {
suite.Suite
ctx sdk.Context
// mempool and lane set up
mempool blockbuster.Mempool
tobLane *auction.TOBLane
baseLane *base.DefaultLane
logger log.Logger
encodingConfig testutils.EncodingConfig
proposalHandler *abci.ProposalHandler
voteExtensionHandler *abci.VoteExtensionHandler
// builder setup
builderKeeper keeper.Keeper
bankKeeper *testutils.MockBankKeeper
accountKeeper *testutils.MockAccountKeeper
distrKeeper *testutils.MockDistributionKeeper
stakingKeeper *testutils.MockStakingKeeper
builderDecorator ante.BuilderDecorator
key *storetypes.KVStoreKey
authorityAccount sdk.AccAddress
// account set up
accounts []testutils.Account
balance sdk.Coin
random *rand.Rand
nonces map[string]uint64
}
func TestABCISuite(t *testing.T) {
suite.Run(t, new(ABCITestSuite))
}
func (suite *ABCITestSuite) SetupTest() {
// General config
suite.encodingConfig = testutils.CreateTestEncodingConfig()
suite.random = rand.New(rand.NewSource(time.Now().Unix()))
suite.key = storetypes.NewKVStoreKey(buildertypes.StoreKey)
testCtx := testutil.DefaultContextWithDB(suite.T(), suite.key, storetypes.NewTransientStoreKey("transient_test"))
suite.ctx = testCtx.Ctx.WithBlockHeight(10)
suite.logger = log.NewTestLogger(suite.T())
suite.ctx = suite.ctx.WithConsensusParams(cmtproto.ConsensusParams{
Abci: &cmtproto.ABCIParams{
VoteExtensionsEnableHeight: 1,
},
})
// Lanes configuration
//
// TOB lane set up
tobConfig := blockbuster.BaseLaneConfig{
Logger: suite.logger,
TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(),
TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(),
AnteHandler: suite.anteHandler,
MaxBlockSpace: math.LegacyZeroDec(),
}
suite.tobLane = auction.NewTOBLane(
tobConfig,
0, // No bound on the number of transactions in the lane
auction.NewDefaultAuctionFactory(suite.encodingConfig.TxConfig.TxDecoder()),
)
// Base lane set up
baseConfig := blockbuster.BaseLaneConfig{
Logger: suite.logger,
TxEncoder: suite.encodingConfig.TxConfig.TxEncoder(),
TxDecoder: suite.encodingConfig.TxConfig.TxDecoder(),
AnteHandler: suite.anteHandler,
MaxBlockSpace: math.LegacyZeroDec(),
IgnoreList: []blockbuster.Lane{suite.tobLane},
}
suite.baseLane = base.NewDefaultLane(
baseConfig,
)
// Mempool set up
suite.mempool = blockbuster.NewMempool(
suite.logger,
suite.tobLane,
suite.baseLane,
)
// Mock keepers set up
ctrl := gomock.NewController(suite.T())
suite.accountKeeper = testutils.NewMockAccountKeeper(ctrl)
suite.accountKeeper.EXPECT().GetModuleAddress(buildertypes.ModuleName).Return(sdk.AccAddress{}).AnyTimes()
suite.bankKeeper = testutils.NewMockBankKeeper(ctrl)
suite.distrKeeper = testutils.NewMockDistributionKeeper(ctrl)
suite.stakingKeeper = testutils.NewMockStakingKeeper(ctrl)
suite.authorityAccount = sdk.AccAddress([]byte("authority"))
// Builder keeper / decorator set up
suite.builderKeeper = keeper.NewKeeper(
suite.encodingConfig.Codec,
suite.key,
suite.accountKeeper,
suite.bankKeeper,
suite.distrKeeper,
suite.stakingKeeper,
suite.authorityAccount.String(),
)
err := suite.builderKeeper.SetParams(suite.ctx, buildertypes.DefaultParams())
suite.Require().NoError(err)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
// Accounts set up
suite.accounts = testutils.RandomAccounts(suite.random, 10)
suite.balance = sdk.NewCoin("stake", math.NewInt(1000000000000000000))
suite.nonces = make(map[string]uint64)
for _, acc := range suite.accounts {
suite.nonces[acc.Address.String()] = 0
}
// Proposal handler set up
suite.proposalHandler = abci.NewProposalHandler(
[]blockbuster.Lane{
suite.tobLane,
suite.baseLane,
},
suite.tobLane,
suite.logger,
suite.encodingConfig.TxConfig.TxEncoder(),
suite.encodingConfig.TxConfig.TxDecoder(),
abci.NoOpValidateVoteExtensionsFn(),
)
suite.voteExtensionHandler = abci.NewVoteExtensionHandler(
log.NewTestLogger(suite.T()),
suite.tobLane,
suite.encodingConfig.TxConfig.TxDecoder(),
suite.encodingConfig.TxConfig.TxEncoder(),
)
}
func (suite *ABCITestSuite) anteHandler(ctx sdk.Context, tx sdk.Tx, _ bool) (sdk.Context, error) {
suite.bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), suite.balance.Denom).AnyTimes().Return(suite.balance)
next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) {
return ctx, nil
}
return suite.builderDecorator.AnteHandle(ctx, tx, false, next)
}
// fillBaseLane fills the base lane with numTxs transactions that are randomly created.
func (suite *ABCITestSuite) fillBaseLane(numTxs int) {
for i := 0; i < numTxs; i++ {
// randomly select an account to create the tx
randomIndex := suite.random.Intn(len(suite.accounts))
acc := suite.accounts[randomIndex]
// create a few random msgs and construct the tx
nonce := suite.nonces[acc.Address.String()]
randomMsgs := testutils.CreateRandomMsgs(acc.Address, 3)
tx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, acc, nonce, 1000, randomMsgs)
suite.Require().NoError(err)
// insert the tx into the lane and update the account
suite.nonces[acc.Address.String()]++
priority := suite.random.Int63n(100) + 1
suite.Require().NoError(suite.mempool.Insert(suite.ctx.WithPriority(priority), tx))
}
}
// fillTOBLane fills the TOB lane with numTxs transactions that are randomly created.
func (suite *ABCITestSuite) fillTOBLane(numTxs int, numBundledTxs int) {
// Insert a bunch of auction transactions into the global mempool and auction mempool
for i := 0; i < numTxs; i++ {
// randomly select a bidder to create the tx
randomIndex := suite.random.Intn(len(suite.accounts))
acc := suite.accounts[randomIndex]
// create a randomized auction transaction
nonce := suite.nonces[acc.Address.String()]
bidAmount := math.NewInt(int64(suite.random.Intn(1000) + 1))
bid := sdk.NewCoin("stake", bidAmount)
signers := []testutils.Account{}
for j := 0; j < numBundledTxs; j++ {
signers = append(signers, suite.accounts[0])
}
tx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, acc, bid, nonce, 1000, signers)
suite.Require().NoError(err)
// insert the auction tx into the global mempool
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
suite.nonces[acc.Address.String()]++
}
}
func (suite *ABCITestSuite) createPrepareProposalRequest(maxBytes int64) comettypes.RequestPrepareProposal {
voteExtensions := make([]comettypes.ExtendedVoteInfo, 0)
auctionIterator := suite.tobLane.Select(suite.ctx, nil)
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) 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
}
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, maxTxBytes int64) []byte {
auctionInfo := abci.AuctionInfo{
ExtendedCommitInfo: suite.createExtendedCommitInfoFromTxBzs(txs),
NumTxs: numTxs,
MaxTxBytes: maxTxBytes,
}
auctionInfoBz, err := auctionInfo.Marshal()
suite.Require().NoError(err)
return auctionInfoBz
}
func (suite *ABCITestSuite) getAuctionBidInfoFromTxBz(txBz []byte) *buildertypes.BidInfo {
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz)
suite.Require().NoError(err)
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
suite.Require().NoError(err)
return bidInfo
}

View File

@ -1,395 +0,0 @@
// 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")
)

View File

@ -1,512 +0,0 @@
package abci_test
import (
"cosmossdk.io/math"
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("stake", math.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("stake", math.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("stake", math.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("stake", math.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("stake", math.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("stake", math.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("stake", math.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("stake", math.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("stake", math.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,
ReserveFee: sdk.NewCoin("stake", math.NewInt(100)),
MinBidIncrement: sdk.NewCoin("stake", math.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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(1)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(1000)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("stake", math.NewInt(102)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(1000)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]testutils.Account{suite.accounts[0], suite.accounts[1]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("stake", math.NewInt(200)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(102)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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("stake", math.NewInt(101)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]testutils.Account{suite.accounts[0]},
)
suite.Require().NoError(err)
bidTx2, err := testutils.CreateAuctionTxWithSigners(
suite.encodingConfig.TxConfig,
suite.accounts[1],
sdk.NewCoin("stake", math.NewInt(102)),
0,
uint64(suite.ctx.BlockHeight())+2,
[]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.GetTotalTxBytes(), tc.maxBytes)
if winningBid == nil {
suite.Require().Len(proposal.GetTxs(), 0)
suite.Require().Equal(proposal.GetTotalTxBytes(), int64(0))
} else {
// Get info about the winning bid
winningBidBz, err := suite.encodingConfig.TxConfig.TxEncoder()(winningBid)
suite.Require().NoError(err)
auctionBidInfo, err := suite.tobLane.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.GetTxs()), len(auctionBidInfo.Transactions)+1)
// Verify that the winning bid is the first transaction in the proposal
suite.Require().Equal(proposal.GetTxs()[0], winningBidBz)
// Verify the ordering of transactions in the proposal
for index, tx := range proposal.GetTxs()[1:] {
suite.Equal(tx, auctionBidInfo.Transactions[index])
}
}
})
}
}

View File

@ -1,212 +0,0 @@
package abci
import (
"fmt"
"reflect"
"sort"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/blockbuster"
"github.com/skip-mev/pob/blockbuster/utils"
)
// 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) *blockbuster.Proposal {
// 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.
topOfBlock := blockbuster.NewProposal(maxBytes)
for _, bidTx := range sortedBidTxs {
// Cache the context so that we can write it back to the original context
// when we know we have a valid top of block bundle.
cacheCtx, write := ctx.CacheContext()
// Attempt to build the top of block using the bid transaction.
proposal, err := h.buildTOB(cacheCtx, bidTx, maxBytes)
if err != nil {
h.logger.Info(
"vote extension auction failed to verify auction tx",
"err", err,
)
txsToRemove[bidTx] = struct{}{}
continue
}
// 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
}
// Remove all of the transactions that were not valid.
if err := utils.RemoveTxsFromLane(txsToRemove, h.tobLane); err != nil {
h.logger.Error(
"failed to remove transactions from lane",
"err", err,
)
}
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)
}
// unmarshal 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)
}
// verify that the included vote extensions are valid in accordance with the
// the preferences of the application
cacheCtx, _ := ctx.CacheContext()
if err := h.validateVoteExtensionsFn(cacheCtx, cacheCtx.BlockHeight(), lastCommitInfo); err != nil {
return nil, fmt.Errorf("failed to validate vote extensions: %w", err)
}
// Build the top of block proposal from the auction info.
expectedTOB := h.BuildTOB(cacheCtx, 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.GetTxs()) {
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.tobLane.GetAuctionBidInfo(bidTxs[i])
if err != nil {
return false
}
bidInfoJ, err := h.tobLane.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, maxBytes int64) (*blockbuster.Proposal, error) {
proposal := blockbuster.NewProposal(maxBytes)
// cache the bytes of the bid transaction
txBz, _, err := utils.GetTxHashStr(h.txEncoder, bidTx)
if err != nil {
return proposal, err
}
// Ensure that the bid transaction is valid
if err := h.tobLane.VerifyTx(ctx, bidTx); err != nil {
return proposal, err
}
bidInfo, err := h.tobLane.GetAuctionBidInfo(bidTx)
if err != nil {
return proposal, err
}
// store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal
txs := [][]byte{txBz}
// Ensure that the bundled transactions are valid
for _, rawRefTx := range bidInfo.Transactions {
// convert the bundled raw transaction to a sdk.Tx
refTx, err := h.tobLane.WrapBundleTransaction(rawRefTx)
if err != nil {
return proposal, err
}
// convert the sdk.Tx to a hash and bytes
txBz, _, err := utils.GetTxHashStr(h.txEncoder, refTx)
if err != nil {
return proposal, err
}
txs = append(txs, txBz)
}
// Add the bundled transactions to the proposal.
if err := proposal.UpdateProposal(h.tobLane, txs); err != nil {
return proposal, err
}
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.tobLane.GetAuctionBidInfo(bidTx); err != nil || bidInfo == nil {
return nil, fmt.Errorf("vote extension does not contain an auction transaction")
}
return bidTx, nil
}

View File

@ -1,231 +0,0 @@
package abci
import (
"cosmossdk.io/log"
"cosmossdk.io/math"
cometabci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/skip-mev/pob/blockbuster"
"github.com/skip-mev/pob/blockbuster/abci"
"github.com/skip-mev/pob/blockbuster/lanes/auction"
"github.com/skip-mev/pob/blockbuster/utils"
)
const (
// NumInjectedTxs is the minimum number of transactions that were injected into
// the proposal but are not actual transactions. In this case, the auction
// info is injected into the proposal but should be ignored by the application.ß
NumInjectedTxs = 1
// AuctionInfoIndex is the index of the auction info in the proposal.
AuctionInfoIndex = 0
)
type (
// TOBLaneProposal is the interface that defines all of the dependencies that
// are required to interact with the top of block lane.
TOBLaneProposal interface {
sdkmempool.Mempool
// Factory defines the API/functionality which is responsible for determining
// if a transaction is a bid transaction and how to extract relevant
// information from the transaction (bid, timeout, bidder, etc.).
auction.Factory
// VerifyTx is utilized to verify a bid transaction according to the preferences
// of the top of block lane.
VerifyTx(ctx sdk.Context, tx sdk.Tx) error
// GetMaxBlockSpace returns the maximum block space that can be used by the top of
// block lane as a percentage of the total block space.
GetMaxBlockSpace() math.LegacyDec
// Logger returns the logger for the top of block lane.
Logger() log.Logger
// Name returns the name of the top of block lane.
Name() string
}
// ProposalHandler contains the functionality and handlers required to\
// process, validate and build blocks.
ProposalHandler struct {
logger log.Logger
txEncoder sdk.TxEncoder
txDecoder sdk.TxDecoder
// prepareLanesHandler is responsible for preparing the proposal by selecting
// transactions from each lane according to each lane's selection logic.
prepareLanesHandler blockbuster.PrepareLanesHandler
// processLanesHandler is responsible for verifying that the proposal is valid
// according to each lane's verification logic.
processLanesHandler blockbuster.ProcessLanesHandler
// tobLane is the top of block lane which is utilized to verify transactions that
// should be included in the top of block.
tobLane TOBLaneProposal
// validateVoteExtensionsFn is the function responsible for validating vote extensions.
validateVoteExtensionsFn ValidateVoteExtensionsFn
}
)
// NewProposalHandler returns a ProposalHandler that contains the functionality and handlers
// required to process, validate and build blocks.
func NewProposalHandler(
lanes []blockbuster.Lane,
tobLane TOBLaneProposal,
logger log.Logger,
txEncoder sdk.TxEncoder,
txDecoder sdk.TxDecoder,
validateVeFN ValidateVoteExtensionsFn,
) *ProposalHandler {
return &ProposalHandler{
// We prepare lanes skipping the first lane because the first lane is the top of block lane.
prepareLanesHandler: abci.ChainPrepareLanes(lanes[1:]...),
processLanesHandler: abci.ChainProcessLanes(lanes...),
tobLane: tobLane,
logger: logger,
txEncoder: txEncoder,
txDecoder: txDecoder,
validateVoteExtensionsFn: validateVeFN,
}
}
// PrepareProposalHandler returns the PrepareProposal ABCI handler that performs
// top-of-block auctioning and general block proposal construction. This handler
// will first attempt to construct the top of the block by utilizing the vote
// extensions from the previous height. If the vote extensions are not available,
// then no top of block auction is performed. After this, the rest of the proposal
// will be constructed by selecting transactions from each lane according to each
// lane's selection logic.
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
return func(ctx sdk.Context, req *cometabci.RequestPrepareProposal) (*cometabci.ResponsePrepareProposal, error) {
partialProposal := blockbuster.NewProposal(req.MaxTxBytes)
voteExtensionsEnabled := h.VoteExtensionsEnabled(ctx)
h.logger.Info(
"preparing proposal",
"height", req.Height,
"vote_extensions_enabled", voteExtensionsEnabled,
)
if voteExtensionsEnabled {
// Build the top of block portion of the proposal given the vote extensions
// from the previous height.
partialProposal = h.BuildTOB(ctx, req.LocalLastCommit, req.MaxTxBytes)
h.logger.Info(
"built top of block",
"num_txs", partialProposal.GetNumTxs(),
"size", partialProposal.GetTotalTxBytes(),
)
// If information is unable to be marshaled, we return an empty proposal. This will
// cause another proposal to be generated after it is rejected in ProcessProposal.
lastCommitInfo, err := req.LocalLastCommit.Marshal()
if err != nil {
h.logger.Error("failed to marshal last commit info", "err", err)
return &cometabci.ResponsePrepareProposal{Txs: nil}, err
}
auctionInfo := &AuctionInfo{
ExtendedCommitInfo: lastCommitInfo,
MaxTxBytes: req.MaxTxBytes,
NumTxs: uint64(partialProposal.GetNumTxs()),
}
// Add the auction info and top of block transactions into the proposal.
auctionInfoBz, err := auctionInfo.Marshal()
if err != nil {
h.logger.Error("failed to marshal auction info", "err", err)
return &cometabci.ResponsePrepareProposal{Txs: nil}, err
}
partialProposal.AddVoteExtension(auctionInfoBz)
}
// Prepare the proposal by selecting transactions from each lane according to
// each lane's selection logic.
finalProposal, err := h.prepareLanesHandler(ctx, partialProposal)
if err != nil {
h.logger.Error("failed to prepare proposal", "err", err)
return &cometabci.ResponsePrepareProposal{Txs: nil}, err
}
h.logger.Info(
"prepared proposal",
"num_txs", finalProposal.GetNumTxs(),
"size", finalProposal.GetTotalTxBytes(),
)
return &cometabci.ResponsePrepareProposal{Txs: finalProposal.GetProposal()}, err
}
}
// ProcessProposalHandler returns the ProcessProposal ABCI handler that performs
// block proposal verification. This handler will first attempt to verify the top
// of block transactions by utilizing the vote extensions from the previous height.
// If the vote extensions are not available, then no top of block verification is done.
// After this, the rest of the proposal will be verified according to each lane's
// verification logic.
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req *cometabci.RequestProcessProposal) (*cometabci.ResponseProcessProposal, error) {
txs := req.Txs
voteExtensionsEnabled := h.VoteExtensionsEnabled(ctx)
h.logger.Info(
"processing proposal",
"height", req.Height,
"vote_extensions_enabled", voteExtensionsEnabled,
"num_txs", len(req.Txs),
)
// If vote extensions have been enabled, verify that the same top of block transactions can be
// built from the vote extensions included in the proposal. Otherwise verify that the proposal
// is valid according to each lane's verification logic.
if voteExtensionsEnabled {
auctionInfo, err := h.VerifyTOB(ctx, txs)
if err != nil {
h.logger.Error("failed to verify top of block transactions", "err", err)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
h.logger.Info(
"verified top of block",
"num_txs", auctionInfo.NumTxs,
)
txs = req.Txs[NumInjectedTxs:]
}
decodedTxs, err := utils.GetDecodedTxs(h.txDecoder, txs)
if err != nil {
h.logger.Error("failed to decode transactions", "err", err)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
// Verify that the rest of the proposal is valid according to each lane's verification logic.
if _, err = h.processLanesHandler(ctx, decodedTxs); err != nil {
h.logger.Error("failed to process proposal", "err", err)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, nil
}
}
// VoteExtensionsEnabled determines if vote extensions are enabled for the current block.
func (h *ProposalHandler) VoteExtensionsEnabled(ctx sdk.Context) bool {
cp := ctx.ConsensusParams()
if cp.Abci == nil || cp.Abci.VoteExtensionsEnableHeight == 0 {
return false
}
// We do a > here because the vote extensions are enabled at block height H
// but will only be used at block height H+1.
return ctx.BlockHeight() > cp.Abci.VoteExtensionsEnableHeight
}

View File

@ -1,816 +0,0 @@
package abci_test
import (
"cosmossdk.io/log"
"cosmossdk.io/math"
comettypes "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/abci"
"github.com/skip-mev/pob/blockbuster"
"github.com/skip-mev/pob/blockbuster/lanes/auction"
"github.com/skip-mev/pob/blockbuster/lanes/base"
testutils "github.com/skip-mev/pob/testutils"
"github.com/skip-mev/pob/x/builder/ante"
buildertypes "github.com/skip-mev/pob/x/builder/types"
)
func (suite *ABCITestSuite) TestPrepareProposal() {
var (
// the modified transactions cannot exceed this size
maxTxBytes int64 = 1000000000000000000
// mempool configuration
normalTxs []sdk.Tx
auctionTxs []sdk.Tx
winningBidTx sdk.Tx
insertBundledTxs = false
// auction configuration
maxBundleSize uint32 = 10
reserveFee = sdk.NewCoin("stake", math.NewInt(1000))
frontRunningProtection = true
)
cases := []struct {
name string
malleate func()
expectedNumberProposalTxs int
expectedMempoolDistribution map[string]int
}{
{
"single valid tob transaction in the mempool",
func() {
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{}
auctionTxs = []sdk.Tx{bidTx}
winningBidTx = bidTx
insertBundledTxs = false
},
2,
map[string]int{
base.LaneName: 0,
auction.LaneName: 1,
},
},
{
"single invalid tob transaction in the mempool",
func() {
bidder := suite.accounts[0]
bid := reserveFee.Sub(sdk.NewCoin("stake", math.NewInt(1))) // bid is less than the reserve fee
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{}
auctionTxs = []sdk.Tx{bidTx}
winningBidTx = nil
insertBundledTxs = false
},
0,
map[string]int{
base.LaneName: 0,
auction.LaneName: 0,
},
},
{
"normal transactions in the mempool",
func() {
account := suite.accounts[0]
nonce := suite.nonces[account.Address.String()]
timeout := uint64(100)
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{normalTx}
auctionTxs = []sdk.Tx{}
winningBidTx = nil
insertBundledTxs = false
},
1,
map[string]int{
base.LaneName: 1,
auction.LaneName: 0,
},
},
{
"normal transactions and tob transactions in the mempool",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create a valid default transaction
account := suite.accounts[1]
nonce = suite.nonces[account.Address.String()] + 1
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{normalTx}
auctionTxs = []sdk.Tx{bidTx}
winningBidTx = bidTx
insertBundledTxs = false
},
3,
map[string]int{
base.LaneName: 1,
auction.LaneName: 1,
},
},
{
"multiple tob transactions where the first is invalid",
func() {
// Create an invalid tob transaction (frontrunning)
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000000000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder, bidder, suite.accounts[1]}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create a valid tob transaction
bidder = suite.accounts[1]
bid = sdk.NewCoin("stake", math.NewInt(1000))
nonce = suite.nonces[bidder.Address.String()]
timeout = uint64(100)
signers = []testutils.Account{bidder}
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{}
auctionTxs = []sdk.Tx{bidTx, bidTx2}
winningBidTx = bidTx2
insertBundledTxs = false
},
2,
map[string]int{
base.LaneName: 0,
auction.LaneName: 1,
},
},
{
"multiple tob transactions where the first is valid",
func() {
// Create an valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(10000000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{suite.accounts[2], bidder}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create a valid tob transaction
bidder = suite.accounts[1]
bid = sdk.NewCoin("stake", math.NewInt(1000))
nonce = suite.nonces[bidder.Address.String()]
timeout = uint64(100)
signers = []testutils.Account{bidder}
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{}
auctionTxs = []sdk.Tx{bidTx, bidTx2}
winningBidTx = bidTx
insertBundledTxs = false
frontRunningProtection = false
},
3,
map[string]int{
base.LaneName: 0,
auction.LaneName: 2,
},
},
{
"single tob transactions where the first is valid and bundle is inserted into mempool",
func() {
frontRunningProtection = false
// Create an valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(10000000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{}
auctionTxs = []sdk.Tx{bidTx}
winningBidTx = bidTx
insertBundledTxs = true
},
6,
map[string]int{
base.LaneName: 5,
auction.LaneName: 1,
},
},
{
"single tob transaction with other normal transactions in the mempool",
func() {
// Create an valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(10000000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
account := suite.accounts[5]
nonce = suite.nonces[account.Address.String()]
timeout = uint64(100)
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
normalTxs = []sdk.Tx{normalTx}
auctionTxs = []sdk.Tx{bidTx}
winningBidTx = bidTx
insertBundledTxs = true
},
7,
map[string]int{
base.LaneName: 6,
auction.LaneName: 1,
},
},
}
for _, tc := range cases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
tc.malleate()
// Insert all of the normal transactions into the default lane
for _, tx := range normalTxs {
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
}
// Insert all of the auction transactions into the TOB lane
for _, tx := range auctionTxs {
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
}
// Insert all of the bundled transactions into the mempool if desired
if insertBundledTxs {
for _, tx := range auctionTxs {
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
suite.Require().NoError(err)
for _, txBz := range bidInfo.Transactions {
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(txBz)
suite.Require().NoError(err)
suite.Require().NoError(suite.mempool.Insert(suite.ctx, tx))
}
}
}
// create a new auction
params := buildertypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
FrontRunningProtection: frontRunningProtection,
}
suite.builderKeeper.SetParams(suite.ctx, params)
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
suite.proposalHandler = abci.NewProposalHandler(
[]blockbuster.Lane{
suite.tobLane,
suite.baseLane,
},
suite.tobLane,
suite.logger,
suite.encodingConfig.TxConfig.TxEncoder(),
suite.encodingConfig.TxConfig.TxDecoder(),
abci.NoOpValidateVoteExtensionsFn(),
)
handler := suite.proposalHandler.PrepareProposalHandler()
req := suite.createPrepareProposalRequest(maxTxBytes)
res, _ := handler(suite.ctx, &req)
// -------------------- Check Invariants -------------------- //
// The first slot in the proposal must be the auction info (if vote extensions are enabled)
auctionInfo := abci.AuctionInfo{}
err := auctionInfo.Unmarshal(res.Txs[abci.AuctionInfoIndex])
suite.Require().NoError(err)
// Total bytes must be less than or equal to maxTxBytes
totalBytes := int64(0)
for _, tx := range res.Txs[abci.NumInjectedTxs:] {
totalBytes += int64(len(tx))
}
suite.Require().LessOrEqual(totalBytes, maxTxBytes)
// 2. the number of transactions in the response must be equal to the number of expected transactions
// NOTE: We add 1 to the expected number of transactions because the first transaction in the response
// is the auction transaction
suite.Require().Equal(tc.expectedNumberProposalTxs+1, len(res.Txs))
// 3. if there are auction transactions, the first transaction must be the top bid
// and the rest of the bundle must be in the response
if winningBidTx != nil {
auctionTx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
suite.Require().NoError(err)
bidInfo, err := suite.tobLane.GetAuctionBidInfo(auctionTx)
suite.Require().NoError(err)
for index, tx := range bidInfo.Transactions {
suite.Require().Equal(tx, res.Txs[index+1+abci.NumInjectedTxs])
}
} else if len(res.Txs) > 1 {
tx, err := suite.encodingConfig.TxConfig.TxDecoder()(res.Txs[1])
suite.Require().NoError(err)
bidInfo, err := suite.tobLane.GetAuctionBidInfo(tx)
suite.Require().NoError(err)
suite.Require().Nil(bidInfo)
}
// 4. All of the transactions must be unique
uniqueTxs := make(map[string]bool)
for _, tx := range res.Txs {
suite.Require().False(uniqueTxs[string(tx)])
uniqueTxs[string(tx)] = true
}
// 5. The number of transactions in the mempool must be correct
suite.Require().Equal(tc.expectedMempoolDistribution, suite.mempool.GetTxDistribution())
})
}
}
func (suite *ABCITestSuite) TestPrepareProposalPreVoteExtensions() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Insert the bid transaction into the mempool
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx))
account := suite.accounts[5]
nonce = suite.nonces[account.Address.String()]
timeout = uint64(100)
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTx(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
// Insert the normal transaction into the mempool
suite.Require().NoError(suite.mempool.Insert(suite.ctx, normalTx))
handler := suite.proposalHandler.PrepareProposalHandler()
req := suite.createPrepareProposalRequest(1000000000000)
suite.ctx = suite.ctx.WithBlockHeight(0)
res, _ := handler(suite.ctx, &req)
suite.Require().Equal(1, len(res.Txs))
}
func (suite *ABCITestSuite) TestProcessProposal() {
var (
// auction configuration
maxBundleSize uint32 = 10
reserveFee = sdk.NewCoin("stake", math.NewInt(1000))
frontRunningProtection = true
maxTxBytes int64 = 1000000000000000000
// mempool configuration
proposal [][]byte
)
params := buildertypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
FrontRunningProtection: frontRunningProtection,
}
suite.builderKeeper.SetParams(suite.ctx, params)
cases := []struct {
name string
createTxs func()
response comettypes.ResponseProcessProposal_ProposalStatus
}{
{
"no transactions in mempool with no vote extension info",
func() {
proposal = nil
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"no transactions in mempool with empty vote extension info",
func() {
proposal = [][]byte{}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single normal tx, no vote extension info",
func() {
account := suite.accounts[0]
nonce := suite.nonces[account.Address.String()]
timeout := uint64(100)
numberMsgs := uint64(3)
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
proposal = [][]byte{normalTxBz}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, single auction tx, no vote extension info",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create a valid default transaction
account := suite.accounts[1]
nonce = suite.nonces[account.Address.String()] + 1
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
proposal = [][]byte{bidTx, normalTx}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx with ref txs (no unwrapping)",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTx, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create a valid default transaction
account := suite.accounts[1]
nonce = suite.nonces[account.Address.String()] + 1
numberMsgs := uint64(3)
normalTx, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, account, nonce, numberMsgs, timeout)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTx}, 2, maxTxBytes)
proposal = [][]byte{
auctionInfo,
bidTx,
normalTx,
}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx with ref txs (with unwrapping)",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
},
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"single auction tx with ref txs but misplaced in proposal",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{suite.accounts[1], bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = [][]byte{
auctionInfo,
bidTxBz,
bidInfo.Transactions[1],
bidInfo.Transactions[0],
}
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, but auction tx is not valid",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder, suite.accounts[1]} // front-running
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 3, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"multiple auction txs but wrong auction tx is at top of block",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder, bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create another valid tob transaction
bidder = suite.accounts[1]
bid = sdk.NewCoin("stake", math.NewInt(1000000))
nonce = suite.nonces[bidder.Address.String()]
timeout = uint64(100)
signers = []testutils.Account{bidder}
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 3, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"multiple auction txs and correct auction tx is selected",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder, bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create another valid tob transaction
bidder = suite.accounts[1]
bid = sdk.NewCoin("stake", math.NewInt(1000000))
nonce = suite.nonces[bidder.Address.String()]
timeout = uint64(100)
signers = []testutils.Account{bidder}
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz2,
},
bidInfo.Transactions...,
)
},
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"multiple auction txs included in block",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder, bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
// Create another valid tob transaction
bidder = suite.accounts[1]
bid = sdk.NewCoin("stake", math.NewInt(1000000))
nonce = suite.nonces[bidder.Address.String()]
timeout = uint64(100)
signers = []testutils.Account{bidder}
bidTxBz2, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz, bidTxBz2}, 2, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz2)
bidInfo2 := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz2,
},
bidInfo.Transactions...,
)
proposal = append(proposal, bidTxBz)
proposal = append(proposal, bidInfo2.Transactions...)
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"single auction tx, but rest of the mempool is invalid",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
proposal = append(proposal, []byte("invalid tx"))
},
comettypes.ResponseProcessProposal_REJECT,
},
{
"multiple auction txs with ref txs + normal transactions",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(1000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{bidder}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, 2, maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[1], nonce, 3, timeout)
suite.Require().NoError(err)
proposal = append(proposal, normalTxBz)
normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[2], nonce, 3, timeout)
suite.Require().NoError(err)
proposal = append(proposal, normalTxBz)
},
comettypes.ResponseProcessProposal_ACCEPT,
},
{
"front-running protection disabled",
func() {
// Create a valid tob transaction
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(10000000))
nonce := suite.nonces[bidder.Address.String()]
timeout := uint64(100)
signers := []testutils.Account{suite.accounts[2], suite.accounts[1], bidder, suite.accounts[3], suite.accounts[4]}
bidTxBz, err := testutils.CreateAuctionTxWithSignerBz(suite.encodingConfig.TxConfig, bidder, bid, nonce, timeout, signers)
suite.Require().NoError(err)
auctionInfo := suite.createAuctionInfoFromTxBzs([][]byte{bidTxBz}, uint64(len(signers)+1), maxTxBytes)
bidInfo := suite.getAuctionBidInfoFromTxBz(bidTxBz)
proposal = append(
[][]byte{
auctionInfo,
bidTxBz,
},
bidInfo.Transactions...,
)
normalTxBz, err := testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[5], nonce, 3, timeout)
suite.Require().NoError(err)
proposal = append(proposal, normalTxBz)
normalTxBz, err = testutils.CreateRandomTxBz(suite.encodingConfig.TxConfig, suite.accounts[6], nonce, 3, timeout)
suite.Require().NoError(err)
proposal = append(proposal, normalTxBz)
// disable frontrunning protection
params := buildertypes.Params{
MaxBundleSize: maxBundleSize,
ReserveFee: reserveFee,
FrontRunningProtection: false,
}
suite.builderKeeper.SetParams(suite.ctx, params)
},
comettypes.ResponseProcessProposal_ACCEPT,
},
}
for _, tc := range cases {
suite.Run(tc.name, func() {
// suite.SetupTest() // reset
suite.builderDecorator = ante.NewBuilderDecorator(suite.builderKeeper, suite.encodingConfig.TxConfig.TxEncoder(), suite.tobLane, suite.mempool)
// reset the proposal handler with the new mempool
suite.proposalHandler = abci.NewProposalHandler(
[]blockbuster.Lane{
suite.tobLane,
suite.baseLane,
},
suite.tobLane, log.NewTestLogger(suite.T()),
suite.encodingConfig.TxConfig.TxEncoder(),
suite.encodingConfig.TxConfig.TxDecoder(),
abci.NoOpValidateVoteExtensionsFn(),
)
tc.createTxs()
handler := suite.proposalHandler.ProcessProposalHandler()
res, _ := handler(suite.ctx, &comettypes.RequestProcessProposal{
Txs: proposal,
})
// Check if the response is valid
suite.Require().Equal(tc.response, res.Status)
})
}
}

View File

@ -1,21 +0,0 @@
package abci
import (
cometabci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// ValidateVoteExtensionsFn defines the function for validating vote extensions. This
// function is not explicitly used to validate the oracle data but rather that
// the signed vote extensions included in the proposal are valid and provide
// a supermajority of vote extensions for the current block. This method is
// expected to be used in ProcessProposal, the expected ctx is the ProcessProposalState's ctx.
type ValidateVoteExtensionsFn func(ctx sdk.Context, currentHeight int64, extendedCommitInfo cometabci.ExtendedCommitInfo) error
// NoOpValidateVoteExtensionsFn returns a ValidateVoteExtensionsFn that does nothing. This should NOT
// be used in production.
func NoOpValidateVoteExtensionsFn() ValidateVoteExtensionsFn {
return func(_ sdk.Context, _ int64, _ cometabci.ExtendedCommitInfo) error {
return nil
}
}

View File

@ -1,225 +0,0 @@
package abci
import (
"crypto/sha256"
"encoding/hex"
"cosmossdk.io/log"
cometabci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/skip-mev/pob/blockbuster/lanes/auction"
"github.com/skip-mev/pob/blockbuster/utils"
)
type (
// TOBLaneVE contains the methods required by the VoteExtensionHandler
// to interact with the local mempool i.e. the top of block lane.
TOBLaneVE interface {
sdkmempool.Mempool
// Factory defines the API/functionality which is responsible for determining
// if a transaction is a bid transaction and how to extract relevant
// information from the transaction (bid, timeout, bidder, etc.).
auction.Factory
// VerifyTx is utilized to verify a bid transaction according to the preferences
// of the top of block lane.
VerifyTx(ctx sdk.Context, tx sdk.Tx) error
}
// VoteExtensionHandler contains the functionality and handlers required to
// process, validate and build vote extensions.
VoteExtensionHandler struct {
logger log.Logger
// tobLane is the top of block lane which is used to extract the top bidding
// auction transaction from the local mempool.
tobLane TOBLaneVE
// txDecoder is used to decode the top bidding auction transaction
txDecoder sdk.TxDecoder
// txEncoder is used to encode the top bidding auction transaction
txEncoder sdk.TxEncoder
// cache is used to store the results of the vote extension verification
// for a given block height.
cache map[string]error
// currentHeight is the block height the cache is valid for.
currentHeight int64
}
)
// NewVoteExtensionHandler returns an VoteExtensionHandler that contains the functionality and handlers
// required to inject, process, and validate vote extensions.
func NewVoteExtensionHandler(logger log.Logger, lane TOBLaneVE, txDecoder sdk.TxDecoder, txEncoder sdk.TxEncoder) *VoteExtensionHandler {
return &VoteExtensionHandler{
logger: logger,
tobLane: lane,
txDecoder: txDecoder,
txEncoder: txEncoder,
cache: make(map[string]error),
currentHeight: 0,
}
}
// ExtendVoteHandler returns the ExtendVoteHandler ABCI handler that extracts
// the top bidding valid auction transaction from a validator's local mempool and
// returns it in its vote extension.
func (h *VoteExtensionHandler) ExtendVoteHandler() sdk.ExtendVoteHandler {
return func(ctx sdk.Context, req *cometabci.RequestExtendVote) (*cometabci.ResponseExtendVote, error) {
// Iterate through auction bids until we find a valid one
auctionIterator := h.tobLane.Select(ctx, nil)
txsToRemove := make(map[sdk.Tx]struct{}, 0)
defer func() {
if err := utils.RemoveTxsFromLane(txsToRemove, h.tobLane); err != nil {
h.logger.Info(
"failed to remove transactions from lane",
"err", err,
)
}
}()
for ; auctionIterator != nil; auctionIterator = auctionIterator.Next() {
bidTx := auctionIterator.Tx()
// Verify the bid tx can be encoded and included in vote extension
bidTxBz, hash, err := utils.GetTxHashStr(h.txEncoder, bidTx)
if err != nil {
h.logger.Info(
"failed to get hash of auction bid tx",
"err", err,
)
txsToRemove[bidTx] = struct{}{}
continue
}
// Validate the auction transaction against a cache state
cacheCtx, _ := ctx.CacheContext()
if err := h.tobLane.VerifyTx(cacheCtx, bidTx); err != nil {
h.logger.Info(
"failed to verify auction bid tx",
"tx_hash", hash,
"err", err,
)
txsToRemove[bidTx] = struct{}{}
continue
}
h.logger.Info("extending vote with auction transaction", "tx_hash", hash)
return &cometabci.ResponseExtendVote{VoteExtension: bidTxBz}, nil
}
h.logger.Info(
"extending vote with no auction transaction",
"height", ctx.BlockHeight(),
)
return &cometabci.ResponseExtendVote{VoteExtension: []byte{}}, nil
}
}
// VerifyVoteExtensionHandler returns the VerifyVoteExtensionHandler ABCI handler
// that verifies the vote extension included in RequestVerifyVoteExtension.
// In particular, it verifies that the vote extension is a valid auction transaction.
func (h *VoteExtensionHandler) VerifyVoteExtensionHandler() sdk.VerifyVoteExtensionHandler {
return func(ctx sdk.Context, req *cometabci.RequestVerifyVoteExtension) (*cometabci.ResponseVerifyVoteExtension, error) {
txBz := req.VoteExtension
if len(txBz) == 0 {
h.logger.Info(
"verified vote extension with no auction transaction",
"height", ctx.BlockHeight(),
)
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_ACCEPT}, nil
}
// Reset the cache if necessary
h.resetCache(ctx.BlockHeight())
hashBz := sha256.Sum256(txBz)
hash := hex.EncodeToString(hashBz[:])
// Short circuit if we have already verified this vote extension
if err, ok := h.cache[hash]; ok {
if err != nil {
h.logger.Info(
"rejected vote extension",
"tx_hash", hash,
"height", ctx.BlockHeight(),
)
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_REJECT}, err
}
h.logger.Info(
"verified vote extension",
"tx_hash", hash,
"height", ctx.BlockHeight(),
)
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_ACCEPT}, nil
}
// Decode the vote extension which should be a valid auction transaction
bidTx, err := h.txDecoder(txBz)
if err != nil {
h.logger.Info(
"rejected vote extension",
"tx_hash", hash,
"height", ctx.BlockHeight(),
"err", err,
)
h.cache[hash] = err
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_REJECT}, err
}
// Verify the auction transaction and cache the result
if err = h.tobLane.VerifyTx(ctx, bidTx); err != nil {
h.logger.Info(
"rejected vote extension",
"tx_hash", hash,
"height", ctx.BlockHeight(),
"err", err,
)
if err := h.tobLane.Remove(bidTx); err != nil {
h.logger.Info(
"failed to remove auction transaction from lane",
"tx_hash", hash,
"height", ctx.BlockHeight(),
"err", err,
)
}
h.cache[hash] = err
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_REJECT}, err
}
h.cache[hash] = nil
h.logger.Info(
"verified vote extension",
"tx_hash", hash,
"height", ctx.BlockHeight(),
)
return &cometabci.ResponseVerifyVoteExtension{Status: cometabci.ResponseVerifyVoteExtension_ACCEPT}, nil
}
}
// checkStaleCache checks if the current height differs than the previous height at which
// the vote extensions were verified in. If so, it resets the cache to allow transactions to be
// reverified.
func (h *VoteExtensionHandler) resetCache(blockHeight int64) {
if h.currentHeight != blockHeight {
h.cache = make(map[string]error)
h.currentHeight = blockHeight
}
}

View File

@ -1,346 +0,0 @@
package abci_test
import (
"cosmossdk.io/log"
"cosmossdk.io/math"
cometabci "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"
"github.com/skip-mev/pob/x/builder/types"
)
func (suite *ABCITestSuite) TestExtendVoteExtensionHandler() {
params := types.Params{
MaxBundleSize: 5,
ReserveFee: sdk.NewCoin("stake", math.NewInt(10)),
FrontRunningProtection: true,
}
testCases := []struct {
name string
getExpectedVE func() []byte
}{
{
"empty mempool",
func() []byte {
return []byte{}
},
},
{
"filled mempool with no auction transactions",
func() []byte {
suite.fillBaseLane(10)
return []byte{}
},
},
{
"mempool with invalid auction transaction (too many bundled transactions)",
func() []byte {
suite.fillTOBLane(3, int(params.MaxBundleSize)+1)
return []byte{}
},
},
{
"mempool with invalid auction transaction (invalid bid)",
func() []byte {
bidder := suite.accounts[0]
bid := params.ReserveFee.Sub(sdk.NewCoin("stake", math.NewInt(1)))
signers := []testutils.Account{bidder}
timeout := 1
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
suite.Require().NoError(err)
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx))
// this should return nothing since the top bid is not valid
return []byte{}
},
},
{
"mempool contains only invalid auction bids (bid is too low)",
func() []byte {
params.ReserveFee = sdk.NewCoin("stake", math.NewInt(10000000000000000))
err := suite.builderKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)
// this way all of the bids will be too small
suite.fillTOBLane(4, 1)
return []byte{}
},
},
{
"mempool contains bid that has an invalid timeout",
func() []byte {
bidder := suite.accounts[0]
bid := params.ReserveFee
signers := []testutils.Account{bidder}
timeout := 0
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
suite.Require().NoError(err)
suite.Require().NoError(suite.tobLane.Insert(suite.ctx, bidTx))
// this should return nothing since the top bid is not valid
return []byte{}
},
},
{
"top bid is invalid but next best is valid",
func() []byte {
params.ReserveFee = sdk.NewCoin("stake", math.NewInt(10))
bidder := suite.accounts[0]
bid := params.ReserveFee.Add(params.ReserveFee)
signers := []testutils.Account{bidder}
timeout := 0
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
suite.Require().NoError(err)
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx))
bidder = suite.accounts[1]
bid = params.ReserveFee
signers = []testutils.Account{bidder}
timeout = 100
bidTx2, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
suite.Require().NoError(err)
suite.Require().NoError(suite.mempool.Insert(suite.ctx, bidTx2))
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx2)
suite.Require().NoError(err)
return bz
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
expectedVE := tc.getExpectedVE()
err := suite.builderKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)
// Reset the handler with the new mempool
suite.voteExtensionHandler = abci.NewVoteExtensionHandler(
log.NewTestLogger(suite.T()),
suite.tobLane,
suite.encodingConfig.TxConfig.TxDecoder(),
suite.encodingConfig.TxConfig.TxEncoder(),
)
handler := suite.voteExtensionHandler.ExtendVoteHandler()
resp, err := handler(suite.ctx, nil)
suite.Require().NoError(err)
suite.Require().Equal(expectedVE, resp.VoteExtension)
})
}
}
func (suite *ABCITestSuite) TestVerifyVoteExtensionHandler() {
params := types.Params{
MaxBundleSize: 5,
ReserveFee: sdk.NewCoin("stake", math.NewInt(100)),
FrontRunningProtection: true,
}
err := suite.builderKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)
testCases := []struct {
name string
req func() *cometabci.RequestVerifyVoteExtension
expectedErr bool
}{
{
"invalid vote extension bytes",
func() *cometabci.RequestVerifyVoteExtension {
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: []byte("invalid vote extension"),
}
},
true,
},
{
"empty vote extension bytes",
func() *cometabci.RequestVerifyVoteExtension {
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: []byte{},
}
},
false,
},
{
"nil vote extension bytes",
func() *cometabci.RequestVerifyVoteExtension {
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: nil,
}
},
false,
},
{
"invalid extension with bid tx with bad timeout",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(10))
signers := []testutils.Account{bidder}
timeout := 0
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
true,
},
{
"invalid vote extension with bid tx with bad bid",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := sdk.NewCoin("stake", math.NewInt(0))
signers := []testutils.Account{bidder}
timeout := 10
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
true,
},
{
"valid vote extension",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := params.ReserveFee
signers := []testutils.Account{bidder}
timeout := 10
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
false,
},
{
"invalid vote extension with front running bid tx",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := params.ReserveFee
timeout := 10
bundlee := testutils.RandomAccounts(suite.random, 1)[0]
signers := []testutils.Account{bidder, bundlee}
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
true,
},
{
"invalid vote extension with too many bundle txs",
func() *cometabci.RequestVerifyVoteExtension {
// disable front running protection
params.FrontRunningProtection = false
err := suite.builderKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)
bidder := suite.accounts[0]
bid := params.ReserveFee
signers := testutils.RandomAccounts(suite.random, int(params.MaxBundleSize)+1)
timeout := 10
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
true,
},
{
"invalid vote extension with a failing bundle tx",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := params.ReserveFee
msgAuctionBid, err := testutils.CreateMsgAuctionBid(suite.encodingConfig.TxConfig, bidder, bid, 0, 0)
suite.Require().NoError(err)
// Create a failing tx
msgAuctionBid.Transactions = [][]byte{{0x01}}
bidTx, err := testutils.CreateTx(suite.encodingConfig.TxConfig, suite.accounts[0], 0, 1, []sdk.Msg{msgAuctionBid})
suite.Require().NoError(err)
bz, err := suite.encodingConfig.TxConfig.TxEncoder()(bidTx)
suite.Require().NoError(err)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
true,
},
{
"valid vote extension + no comparison to local mempool",
func() *cometabci.RequestVerifyVoteExtension {
bidder := suite.accounts[0]
bid := params.ReserveFee
signers := []testutils.Account{bidder}
timeout := 10
bz := suite.createAuctionTxBz(bidder, bid, signers, timeout)
// Add a bid to the mempool that is greater than the one in the vote extension
bid = bid.Add(params.ReserveFee)
bidTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 10, 1, signers)
suite.Require().NoError(err)
err = suite.mempool.Insert(suite.ctx, bidTx)
suite.Require().NoError(err)
tx := suite.tobLane.GetTopAuctionTx(suite.ctx)
suite.Require().NotNil(tx)
return &cometabci.RequestVerifyVoteExtension{
VoteExtension: bz,
}
},
false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
req := tc.req()
handler := suite.voteExtensionHandler.VerifyVoteExtensionHandler()
_, err := handler(suite.ctx, req)
if tc.expectedErr {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
}
})
}
}
func (suite *ABCITestSuite) createAuctionTxBz(bidder testutils.Account, bid sdk.Coin, signers []testutils.Account, timeout int) []byte {
auctionTx, err := testutils.CreateAuctionTxWithSigners(suite.encodingConfig.TxConfig, bidder, bid, 0, uint64(timeout), signers)
suite.Require().NoError(err)
txBz, err := suite.encodingConfig.TxConfig.TxEncoder()(auctionTx)
suite.Require().NoError(err)
return txBz
}

View File

@ -1,17 +1,14 @@
FROM golang:1.20-bullseye AS builder
WORKDIR /src/pob
COPY go.mod go.sum ./
RUN go mod download
WORKDIR /src/pob
COPY . .
RUN go mod tidy
RUN make build-test-app
## Prepare the final clear binary
FROM ubuntu:rolling
EXPOSE 26656 26657 1317 9090 7171
ENTRYPOINT ["testappd", "start"]
COPY --from=builder /src/pob/build/* /usr/local/bin/
RUN apt-get update && apt-get install ca-certificates -y

3
go.work Normal file
View File

@ -0,0 +1,3 @@
go 1.20
use .

14
go.work.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8=
github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ=
github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 h1:zlCp9n3uwQieELltZWHRmwPmPaZ8+XoL2Sj+A2YJlr8=
github.com/getsentry/sentry-go v0.17.0 h1:UustVWnOoDFHBS7IJUB2QK/nB5pap748ZEp0swnQJak=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=

View File

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

@ -18,7 +18,6 @@ import (
feegrantkeeper "cosmossdk.io/x/feegrant/keeper"
feegrantmodule "cosmossdk.io/x/feegrant/module"
cometabci "github.com/cometbft/cometbft/abci/types"
tmtypes "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
@ -62,7 +61,6 @@ import (
"github.com/cosmos/cosmos-sdk/x/staking"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
veabci "github.com/skip-mev/pob/abci"
"github.com/skip-mev/pob/blockbuster"
"github.com/skip-mev/pob/blockbuster/abci"
"github.com/skip-mev/pob/blockbuster/lanes/auction"
@ -340,27 +338,14 @@ func New(
app.App.SetAnteHandler(anteHandler)
// Set the proposal handlers on base app
proposalHandler := veabci.NewProposalHandler(
lanes,
tobLane,
proposalHandler := abci.NewProposalHandler(
app.Logger(),
app.txConfig.TxEncoder(),
app.txConfig.TxDecoder(),
veabci.NoOpValidateVoteExtensionsFn(),
app.TxConfig().TxDecoder(),
mempool,
)
app.App.SetPrepareProposal(proposalHandler.PrepareProposalHandler())
app.App.SetProcessProposal(proposalHandler.ProcessProposalHandler())
// Set the vote extension handler on the app.
voteExtensionHandler := veabci.NewVoteExtensionHandler(
app.Logger(),
tobLane,
app.txConfig.TxDecoder(),
app.txConfig.TxEncoder(),
)
app.App.SetExtendVoteHandler(voteExtensionHandler.ExtendVoteHandler())
app.App.SetVerifyVoteExtensionHandler(voteExtensionHandler.VerifyVoteExtensionHandler())
// Set the custom CheckTx handler on BaseApp.
checkTxHandler := abci.NewCheckTxHandler(
app.App,
@ -415,31 +400,6 @@ func (app *TestApp) SetCheckTx(handler abci.CheckTx) {
app.checkTxHandler = handler
}
// TODO: remove this once we have a proper config file
func (app *TestApp) InitChain(req *cometabci.RequestInitChain) (*cometabci.ResponseInitChain, error) {
req.ConsensusParams.Abci.VoteExtensionsEnableHeight = 2
resp, err := app.App.InitChain(req)
if resp == nil {
resp = &cometabci.ResponseInitChain{}
}
resp.ConsensusParams = &tmtypes.ConsensusParams{
Abci: &tmtypes.ABCIParams{
VoteExtensionsEnableHeight: 2,
},
}
return resp, err
}
// TODO: remove this once we have a proper config file
func (app *TestApp) FinalizeBlock(req *cometabci.RequestFinalizeBlock) (*cometabci.ResponseFinalizeBlock, error) {
resp, err := app.App.FinalizeBlock(req)
if resp != nil {
resp.ConsensusParamUpdates = nil
}
return resp, err
}
// Name returns the name of the App
func (app *TestApp) Name() string { return app.BaseApp.Name() }

View File

@ -1,94 +0,0 @@
package e2e
import (
"fmt"
"os"
"cosmossdk.io/log"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/codec"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/skip-mev/pob/tests/app"
"github.com/skip-mev/pob/tests/app/params"
)
const (
keyringPassphrase = "testpassphrase"
keyringAppName = "testnet"
)
var (
encodingConfig params.EncodingConfig
cdc codec.Codec
)
func init() {
testApp := app.New(log.NewNopLogger(), dbm.NewMemDB(), nil, true, simtestutil.NewAppOptionsWithFlagHome(app.DefaultNodeHome))
encodingConfig = params.EncodingConfig{
InterfaceRegistry: testApp.InterfaceRegistry(),
Codec: testApp.AppCodec(),
TxConfig: testApp.TxConfig(),
Amino: testApp.LegacyAmino(),
}
cdc = encodingConfig.Codec
}
type chain struct {
dataDir string
id string
validators []*validator
}
func newChain() (*chain, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
tmpDir, err := os.MkdirTemp(pwd, ".pob-e2e-testnet-")
if err != nil {
return nil, err
}
return &chain{
id: app.ChainID,
dataDir: tmpDir,
}, nil
}
func (c *chain) configDir() string {
return fmt.Sprintf("%s/%s", c.dataDir, c.id)
}
func (c *chain) createAndInitValidators(count int) error {
for i := 0; i < count; i++ {
node := c.createValidator(i)
// generate genesis files
if err := node.init(); err != nil {
return err
}
c.validators = append(c.validators, node)
// create keys
if err := node.createKey("val"); err != nil {
return err
}
if err := node.createNodeKey(); err != nil {
return err
}
if err := node.createConsensusKey(); err != nil {
return err
}
}
return nil
}
func (c *chain) createValidator(index int) *validator {
return &validator{
chain: c,
index: index,
moniker: "testapp",
}
}

View File

@ -1,323 +0,0 @@
package e2e
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"cosmossdk.io/math"
cometcfg "github.com/cometbft/cometbft/config"
cometjson "github.com/cometbft/cometbft/libs/json"
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/server"
srvconfig "github.com/cosmos/cosmos-sdk/server/config"
sdk "github.com/cosmos/cosmos-sdk/types"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/skip-mev/pob/tests/app"
"github.com/skip-mev/pob/x/builder/types"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
)
var (
numValidators = 4
minGasPrice = sdk.NewDecCoinFromDec(app.BondDenom, math.LegacyMustNewDecFromStr("0.02")).String()
initBalanceStr = sdk.NewInt64Coin(app.BondDenom, 1000000000000000000).String()
stakeAmount = math.NewInt(100000000000)
stakeAmountCoin = sdk.NewCoin(app.BondDenom, stakeAmount)
)
type (
TestAccount struct {
PrivateKey *secp256k1.PrivKey
Address sdk.AccAddress
}
IntegrationTestSuite struct {
suite.Suite
tmpDirs []string
chain *chain
dkrPool *dockertest.Pool
dkrNet *dockertest.Network
valResources []*dockertest.Resource
}
)
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up e2e integration test suite...")
var err error
s.chain, err = newChain()
s.Require().NoError(err)
s.T().Logf("starting e2e infrastructure; chain-id: %s; datadir: %s", s.chain.id, s.chain.dataDir)
s.dkrPool, err = dockertest.NewPool("")
s.Require().NoError(err)
s.dkrNet, err = s.dkrPool.CreateNetwork(fmt.Sprintf("%s-testnet", s.chain.id))
s.Require().NoError(err)
// The bootstrapping phase is as follows:
//
// 1. Initialize TestApp validator nodes.
// 2. Create and initialize TestApp validator genesis files, i.e. setting
// delegate keys for validators.
// 3. Start TestApp network.
s.initNodes()
s.initGenesis()
s.initValidatorConfigs()
s.runValidators()
}
func (s *IntegrationTestSuite) TearDownSuite() {
if str := os.Getenv("POB_E2E_SKIP_CLEANUP"); len(str) > 0 {
skipCleanup, err := strconv.ParseBool(str)
s.Require().NoError(err)
if skipCleanup {
return
}
}
s.T().Log("tearing down e2e integration test suite...")
for _, vc := range s.valResources {
s.Require().NoError(s.dkrPool.Purge(vc))
}
s.Require().NoError(s.dkrPool.RemoveNetwork(s.dkrNet))
os.RemoveAll(s.chain.dataDir)
for _, td := range s.tmpDirs {
os.RemoveAll(td)
}
}
func (s *IntegrationTestSuite) initNodes() {
s.Require().NoError(s.chain.createAndInitValidators(numValidators))
// initialize a genesis file for the first validator
val0ConfigDir := s.chain.validators[0].configDir()
// Define the builder module parameters
params := types.Params{
MaxBundleSize: 5,
EscrowAccountAddress: sdk.MustAccAddressFromBech32("cosmos14j5j2lsx7629590jvpk3vj0xe9w8203jf4yknk").Bytes(),
ReserveFee: sdk.NewCoin(app.BondDenom, math.NewInt(1000000)),
MinBidIncrement: sdk.NewCoin(app.BondDenom, math.NewInt(1000000)),
ProposerFee: math.LegacyMustNewDecFromStr("0.1"),
FrontRunningProtection: true,
}
for _, val := range s.chain.validators {
valAddr, err := val.keyInfo.GetAddress()
s.Require().NoError(err)
s.Require().NoError(initGenesisFile(val0ConfigDir, "", initBalanceStr, valAddr, params))
}
// copy the genesis file to the remaining validators
for _, val := range s.chain.validators[1:] {
_, err := copyFile(
filepath.Join(val0ConfigDir, "config", "genesis.json"),
filepath.Join(val.configDir(), "config", "genesis.json"),
)
s.Require().NoError(err)
}
}
func (s *IntegrationTestSuite) initGenesis() {
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(s.chain.validators[0].configDir())
config.Moniker = s.chain.validators[0].moniker
genFilePath := config.GenesisFile()
appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath)
s.T().Log("starting e2e infrastructure; validator_0 config:", genFilePath)
s.Require().NoError(err)
// x/gov
var govGenState govtypesv1.GenesisState
s.Require().NoError(cdc.UnmarshalJSON(appGenState[govtypes.ModuleName], &govGenState))
votingPeriod := 5 * time.Second
govGenState.Params.VotingPeriod = &votingPeriod
govGenState.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(app.BondDenom, math.NewInt(100)))
bz, err := cdc.MarshalJSON(&govGenState)
s.Require().NoError(err)
appGenState[govtypes.ModuleName] = bz
var genUtilGenState genutiltypes.GenesisState
s.Require().NoError(cdc.UnmarshalJSON(appGenState[genutiltypes.ModuleName], &genUtilGenState))
// x/genutil genesis txs
genTxs := make([]json.RawMessage, len(s.chain.validators))
for i, val := range s.chain.validators {
createValMsg, err := val.buildCreateValidatorMsg(stakeAmountCoin)
s.Require().NoError(err)
signedTx, err := val.signMsg(createValMsg)
s.Require().NoError(err)
txRaw, err := cdc.MarshalJSON(signedTx)
s.Require().NoError(err)
genTxs[i] = txRaw
}
genUtilGenState.GenTxs = genTxs
bz, err = cdc.MarshalJSON(&genUtilGenState)
s.Require().NoError(err)
appGenState[genutiltypes.ModuleName] = bz
bz, err = json.MarshalIndent(appGenState, "", " ")
s.Require().NoError(err)
genDoc.AppState = bz
bz, err = cometjson.MarshalIndent(genDoc, "", " ")
s.Require().NoError(err)
// write the updated genesis file to each validator
for _, val := range s.chain.validators {
writeFile(filepath.Join(val.configDir(), "config", "genesis.json"), bz)
}
}
func (s *IntegrationTestSuite) initValidatorConfigs() {
for i, val := range s.chain.validators {
tmCfgPath := filepath.Join(val.configDir(), "config", "config.toml")
vpr := viper.New()
vpr.SetConfigFile(tmCfgPath)
s.Require().NoError(vpr.ReadInConfig())
valConfig := cometcfg.DefaultConfig()
s.Require().NoError(vpr.Unmarshal(valConfig))
valConfig.P2P.ListenAddress = "tcp://0.0.0.0:26656"
valConfig.P2P.AddrBookStrict = false
valConfig.P2P.ExternalAddress = fmt.Sprintf("%s:%d", val.instanceName(), 26656)
valConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657"
valConfig.StateSync.Enable = false
valConfig.LogLevel = "info"
valConfig.BaseConfig.Genesis = filepath.Join("config", "genesis.json")
valConfig.RootDir = filepath.Join("root", ".simapp")
valConfig.Consensus.TimeoutCommit = 2 * time.Second
var peers []string
for j := 0; j < len(s.chain.validators); j++ {
if i == j {
continue
}
peer := s.chain.validators[j]
peerID := fmt.Sprintf("%s@%s%d:26656", peer.nodeKey.ID(), peer.moniker, j)
peers = append(peers, peerID)
}
valConfig.P2P.PersistentPeers = strings.Join(peers, ",")
cometcfg.WriteConfigFile(tmCfgPath, valConfig)
// set application configuration
appCfgPath := filepath.Join(val.configDir(), "config", "app.toml")
appConfig := srvconfig.DefaultConfig()
appConfig.API.Enable = true
appConfig.MinGasPrices = minGasPrice
appConfig.API.Address = "tcp://0.0.0.0:1317"
appConfig.GRPC.Address = "0.0.0.0:9090"
srvconfig.WriteConfigFile(appCfgPath, appConfig)
}
}
func (s *IntegrationTestSuite) runValidators() {
s.T().Log("starting POB TestApp validator containers...")
s.valResources = make([]*dockertest.Resource, len(s.chain.validators))
for i, val := range s.chain.validators {
runOpts := &dockertest.RunOptions{
Name: val.instanceName(),
NetworkID: s.dkrNet.Network.ID,
Mounts: []string{
fmt.Sprintf("%s/:/root/.testapp", val.configDir()),
},
Repository: "docker.io/skip-mev/pob-e2e",
}
// expose the first validator for debugging and communication
if val.index == 0 {
runOpts.PortBindings = map[docker.Port][]docker.PortBinding{
"1317/tcp": {{HostIP: "", HostPort: "1317"}},
"6060/tcp": {{HostIP: "", HostPort: "6060"}},
"6061/tcp": {{HostIP: "", HostPort: "6061"}},
"6062/tcp": {{HostIP: "", HostPort: "6062"}},
"6063/tcp": {{HostIP: "", HostPort: "6063"}},
"6064/tcp": {{HostIP: "", HostPort: "6064"}},
"6065/tcp": {{HostIP: "", HostPort: "6065"}},
"9090/tcp": {{HostIP: "", HostPort: "9090"}},
"26656/tcp": {{HostIP: "", HostPort: "26656"}},
"26657/tcp": {{HostIP: "", HostPort: "26657"}},
}
}
resource, err := s.dkrPool.RunWithOptions(runOpts, noRestart)
s.Require().NoError(err)
s.valResources[i] = resource
s.T().Logf("started POB TestApp validator container: %s", resource.Container.ID)
}
rpcClient, err := rpchttp.New("tcp://localhost:26657", "/websocket")
s.Require().NoError(err)
s.Require().Eventually(
func() bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
status, err := rpcClient.Status(ctx)
if err != nil {
return false
}
// let the node produce a few blocks
if status.SyncInfo.CatchingUp || status.SyncInfo.LatestBlockHeight < 3 {
return false
}
return true
},
2*time.Minute,
time.Second,
"POB TestApp node failed to produce blocks",
)
}
func noRestart(config *docker.HostConfig) {
// in this case we don't want the nodes to restart on failure
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,180 +0,0 @@
package e2e
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ory/dockertest/v3/docker"
"github.com/skip-mev/pob/tests/app"
buildertypes "github.com/skip-mev/pob/x/builder/types"
)
// execMsgSendTx executes a send transaction on the given validator given the provided
// recipient and amount. This function returns the transaction hash. It does not wait for the
// transaction to be committed.
func (s *IntegrationTestSuite) execMsgSendTx(valIdx int, to sdk.AccAddress, amount sdk.Coin) string {
address, err := s.chain.validators[valIdx].keyInfo.GetAddress()
s.Require().NoError(err)
s.T().Logf(
"sending %s from %s to %s",
amount, address, to,
)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{
Context: ctx,
AttachStdout: true,
AttachStderr: true,
Container: s.valResources[valIdx].Container.ID,
User: "root",
Cmd: []string{
"testappd",
"tx",
"bank",
"send",
address.String(), // sender
to.String(), // receiver
amount.String(), // amount
fmt.Sprintf("--%s=%s", flags.FlagFrom, s.chain.validators[valIdx].keyInfo.Name),
fmt.Sprintf("--%s=%s", flags.FlagChainID, s.chain.id),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoin(app.BondDenom, math.NewInt(1000000000)).String()),
"--keyring-backend=test",
"--broadcast-mode=sync",
"-y",
},
})
s.Require().NoError(err)
var (
outBuf bytes.Buffer
errBuf bytes.Buffer
)
err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{
Context: ctx,
Detach: false,
OutputStream: &outBuf,
ErrorStream: &errBuf,
})
s.Require().NoErrorf(err, "stdout: %s, stderr: %s", outBuf.String(), errBuf.String())
output := outBuf.String()
resp := strings.Split(output, ":")
txHash := strings.TrimSpace(resp[len(resp)-1])
return txHash
}
// createAuctionBidTx creates a transaction that bids on an auction given the provided bidder, bid, and transactions.
func (s *IntegrationTestSuite) createAuctionBidTx(account TestAccount, bid sdk.Coin, transactions [][]byte, sequenceOffset, height, gasLimit uint64, fees sdk.Coins) []byte {
msgs := []sdk.Msg{
&buildertypes.MsgAuctionBid{
Bidder: account.Address.String(),
Bid: bid,
Transactions: transactions,
},
}
return s.createTx(account, msgs, sequenceOffset, height, gasLimit, fees)
}
// createMsgSendTx creates a send transaction given the provided signer, recipient, amount, sequence number offset, and block height timeout.
// This function is primarily used to create bundles of transactions.
func (s *IntegrationTestSuite) createMsgSendTx(account TestAccount, toAddress string, amount sdk.Coins, sequenceOffset, height, gasLimit uint64, fees sdk.Coins) []byte {
msgs := []sdk.Msg{
&banktypes.MsgSend{
FromAddress: account.Address.String(),
ToAddress: toAddress,
Amount: amount,
},
}
return s.createTx(account, msgs, sequenceOffset, height, gasLimit, fees)
}
// createMsgDelegateTx creates a delegate transaction given the provided signer, validator, amount, sequence number offset
// and block height timeout.
func (s *IntegrationTestSuite) createMsgDelegateTx(account TestAccount, validator string, amount sdk.Coin, sequenceOffset, height, gasLimit uint64, fees sdk.Coins) []byte {
msgs := []sdk.Msg{
&stakingtypes.MsgDelegate{
DelegatorAddress: account.Address.String(),
ValidatorAddress: validator,
Amount: amount,
},
}
return s.createTx(account, msgs, sequenceOffset, height, gasLimit, fees)
}
// createTx creates a transaction given the provided messages, sequence number offset, and block height timeout.
func (s *IntegrationTestSuite) createTx(account TestAccount, msgs []sdk.Msg, sequenceOffset, height, gasLimit uint64, fees sdk.Coins) []byte {
txConfig := encodingConfig.TxConfig
txBuilder := txConfig.NewTxBuilder()
// Get account info of the sender to set the account number and sequence number
baseAccount := s.queryAccount(account.Address)
sequenceNumber := baseAccount.Sequence + sequenceOffset
s.Require().NoError(txBuilder.SetMsgs(msgs...))
txBuilder.SetFeeAmount(fees)
txBuilder.SetGasLimit(gasLimit)
txBuilder.SetTimeoutHeight(height)
signerData := authsigning.SignerData{
ChainID: app.ChainID,
AccountNumber: baseAccount.AccountNumber,
Sequence: sequenceNumber,
PubKey: account.PrivateKey.PubKey(),
}
sig := signing.SignatureV2{
PubKey: account.PrivateKey.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
Sequence: sequenceNumber,
}
s.Require().NoError(txBuilder.SetSignatures(sig))
bytesToSign, err := authsigning.GetSignBytesAdapter(
context.Background(),
encodingConfig.TxConfig.SignModeHandler(),
signing.SignMode_SIGN_MODE_DIRECT,
signerData,
txBuilder.GetTx(),
)
s.Require().NoError(err)
sigBytes, err := account.PrivateKey.Sign(bytesToSign)
s.Require().NoError(err)
sig = signing.SignatureV2{
PubKey: account.PrivateKey.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
Signature: sigBytes,
},
Sequence: sequenceNumber,
}
s.Require().NoError(txBuilder.SetSignatures(sig))
signedTx := txBuilder.GetTx()
bz, err := encodingConfig.TxConfig.TxEncoder()(signedTx)
s.Require().NoError(err)
return bz
}

View File

@ -1,401 +0,0 @@
package e2e
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
cmtclient "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
buildertypes "github.com/skip-mev/pob/x/builder/types"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// createClientContext creates a client.Context for use in integration tests.
// Note, it assumes all queries and broadcasts go to the first node.
func (s *IntegrationTestSuite) createClientContext() client.Context {
node := s.valResources[0]
rpcURI := node.GetHostPort("26657/tcp")
gRPCURI := node.GetHostPort("9090/tcp")
rpcClient, err := client.NewClientFromNode(rpcURI)
s.Require().NoError(err)
grpcClient, err := grpc.Dial(gRPCURI, []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}...)
s.Require().NoError(err)
return client.Context{}.
WithNodeURI(rpcURI).
WithClient(rpcClient).
WithGRPCClient(grpcClient).
WithInterfaceRegistry(encodingConfig.InterfaceRegistry).
WithCodec(encodingConfig.Codec).
WithChainID(s.chain.id).
WithBroadcastMode(flags.BroadcastSync)
}
// createTestAccounts creates and funds test accounts with a balance.
func (s *IntegrationTestSuite) createTestAccounts(numAccounts int, balance sdk.Coin) []TestAccount {
accounts := make([]TestAccount, numAccounts)
for i := 0; i < numAccounts; i++ {
// Generate a new account with private key that will be used to sign transactions.
privKey := secp256k1.GenPrivKey()
pubKey := privKey.PubKey()
addr := sdk.AccAddress(pubKey.Address())
account := TestAccount{
PrivateKey: privKey,
Address: addr,
}
// Fund the account.
s.execMsgSendTx(0, account.Address, balance)
// Wait for the balance to be updated.
s.Require().Eventually(func() bool {
return !s.queryBalancesOf(addr.String()).IsZero()
},
10*time.Second,
1*time.Second,
)
accounts[i] = account
}
return accounts
}
// calculateProposerEscrowSplit calculates the amount of a bid that should go to the escrow account
// and the amount that should go to the proposer. The simulation e2e environment does not support
// checking the proposer's balance, it only validates that the escrow address has the correct balance.
func (s *IntegrationTestSuite) calculateProposerEscrowSplit(bid sdk.Coin) sdk.Coin {
// Get the params to determine the proposer fee.
params := s.queryBuilderParams()
proposerFee := params.ProposerFee
var proposerReward sdk.Coins
if proposerFee.IsZero() {
// send the entire bid to the escrow account when no proposer fee is set
return bid
}
// determine the amount of the bid that goes to the (previous) proposer
bidDec := sdk.NewDecCoinsFromCoins(bid)
proposerReward, _ = bidDec.MulDecTruncate(proposerFee).TruncateDecimal()
// Determine the amount of the remaining bid that goes to the escrow account.
// If a decimal remainder exists, it'll stay with the bidding account.
escrowTotal := bidDec.Sub(sdk.NewDecCoinsFromCoins(proposerReward...))
escrowReward, _ := escrowTotal.TruncateDecimal()
return sdk.NewCoin(bid.Denom, escrowReward.AmountOf(bid.Denom))
}
// waitForABlock will wait until the current block height has increased by a single block.
func (s *IntegrationTestSuite) waitForABlock() {
height := s.queryCurrentHeight()
s.Require().Eventually(
func() bool {
return s.queryCurrentHeight() >= height+1
},
10*time.Second,
50*time.Millisecond,
)
}
// waitForNBlocks will wait until the current block height has increased by n blocks.
func (s *IntegrationTestSuite) waitForNBlocks(n int) {
height := s.queryCurrentHeight()
s.Require().Eventually(
func() bool {
return s.queryCurrentHeight() >= height+uint64(n)
},
10*time.Second,
50*time.Millisecond,
)
}
// bundleToTxHashes converts a bundle to a slice of transaction hashes.
func (s *IntegrationTestSuite) bundleToTxHashes(bidTx []byte, bundle [][]byte) []string {
hashes := make([]string, len(bundle)+1)
// encode the bid transaction into a hash
hashBz := sha256.Sum256(bidTx)
hash := hex.EncodeToString(hashBz[:])
hashes[0] = hash
for i, hash := range s.normalTxsToTxHashes(bundle) {
hashes[i+1] = hash
}
return hashes
}
// normalTxsToTxHashes converts a slice of normal transactions to a slice of transaction hashes.
func (s *IntegrationTestSuite) normalTxsToTxHashes(txs [][]byte) []string {
hashes := make([]string, len(txs))
for i, tx := range txs {
hashBz := sha256.Sum256(tx)
hash := hex.EncodeToString(hashBz[:])
hashes[i] = hash
}
return hashes
}
// verifyTopOfBlockAuction verifies that blocks that include a bid transaction execute as expected.
func (s *IntegrationTestSuite) verifyTopOfBlockAuction(height uint64, bundle []string, expectedExecution map[string]bool) {
s.waitForABlock()
s.T().Logf("Verifying block %d", height)
// Get the block's transactions and display the expected and actual block for debugging.
txs := s.queryBlockTxs(height)
s.displayBlock(txs, bundle)
// Ensure that all transactions executed as expected (i.e. landed or failed to land).
for tx, landed := range expectedExecution {
s.T().Logf("Verifying tx %s executed as %t", tx, landed)
s.Require().Equal(landed, s.queryTxPassed(tx) == nil)
}
s.T().Logf("All txs executed as expected")
// Check that the block contains the expected transactions in the expected order
// iff the bid transaction was expected to execute.
if len(bundle) > 0 && len(expectedExecution) > 0 && expectedExecution[bundle[0]] && len(txs) > 0 {
if expectedExecution[bundle[0]] {
hashBz := sha256.Sum256(txs[0])
hash := hex.EncodeToString(hashBz[:])
s.Require().Equal(strings.ToUpper(bundle[0]), strings.ToUpper(hash))
for index, bundleTx := range bundle[1:] {
hashBz := sha256.Sum256(txs[index+1])
txHash := hex.EncodeToString(hashBz[:])
s.Require().Equal(strings.ToUpper(bundleTx), strings.ToUpper(txHash))
}
}
}
}
// verifyBlock verifies that the transactions in the block at the given height were seen
// and executed in the order they were submitted.
func (s *IntegrationTestSuite) verifyBlock(height uint64, txs []string, expectedExecution map[string]bool) {
s.waitForABlock()
s.T().Logf("Verifying block %d", height)
// Get the block's transactions and display the expected and actual block for debugging.
blockTxs := s.queryBlockTxs(height)
s.displayBlock(blockTxs, txs)
// Ensure that all transactions executed as expected (i.e. landed or failed to land).
for tx, landed := range expectedExecution {
s.T().Logf("Verifying tx %s executed as %t", tx, landed)
s.Require().Equal(landed, s.queryTxPassed(tx) == nil)
}
s.T().Logf("All txs executed as expected")
// Check that the block contains the expected transactions in the expected order.
s.Require().Equal(len(txs), len(blockTxs))
hashBlockTxs := s.normalTxsToTxHashes(blockTxs)
for index, tx := range txs {
s.Require().Equal(strings.ToUpper(tx), strings.ToUpper(hashBlockTxs[index]))
}
s.T().Logf("Block %d contains the expected transactions in the expected order", height)
}
// displayExpectedBlock displays the expected and actual blocks.
func (s *IntegrationTestSuite) displayBlock(txs [][]byte, expectedTxs []string) {
if len(expectedTxs) != 0 {
expectedBlock := fmt.Sprintf("Expected block:\n\t(%d, %s)\n", 0, expectedTxs[0])
for index, expectedTx := range expectedTxs[1:] {
expectedBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, expectedTx)
}
s.T().Logf(expectedBlock)
}
// Display the actual block.
if len(txs) == 0 {
s.T().Logf("Actual block is empty")
return
}
hashBz := sha256.Sum256(txs[0])
hash := hex.EncodeToString(hashBz[:])
actualBlock := fmt.Sprintf("Actual block:\n\t(%d, %s)\n", 0, hash)
for index, tx := range txs[1:] {
hashBz := sha256.Sum256(tx)
txHash := hex.EncodeToString(hashBz[:])
actualBlock += fmt.Sprintf("\t(%d, %s)\n", index+1, txHash)
}
s.T().Logf(actualBlock)
}
// displayExpectedBundle displays the expected order of the bid and bundled transactions.
func (s *IntegrationTestSuite) displayExpectedBundle(prefix string, bidTx []byte, bundle [][]byte) {
// encode the bid transaction into a hash
hashes := s.bundleToTxHashes(bidTx, bundle)
expectedBundle := fmt.Sprintf("%s expected bundle:\n\t(%d, %s)\n", prefix, 0, hashes[0])
for index, bundleTx := range hashes[1:] {
expectedBundle += fmt.Sprintf("\t(%d, %s)\n", index+1, bundleTx)
}
s.T().Logf(expectedBundle)
}
// broadcastTx broadcasts a transaction to the network using the given validator.
func (s *IntegrationTestSuite) broadcastTx(tx []byte, valIdx int) {
node := s.valResources[valIdx]
gRPCURI := node.GetHostPort("9090/tcp")
grpcConn, err := grpc.Dial(
gRPCURI,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
s.Require().NoError(err)
client := txtypes.NewServiceClient(grpcConn)
req := &txtypes.BroadcastTxRequest{TxBytes: tx, Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC}
client.BroadcastTx(context.Background(), req)
}
// queryTx queries a transaction by its hash and returns whether there was an
// error in including the transaction in a block.
func (s *IntegrationTestSuite) queryTxPassed(txHash string) error {
queryClient := txtypes.NewServiceClient(s.createClientContext())
req := &txtypes.GetTxRequest{Hash: txHash}
resp, err := queryClient.GetTx(context.Background(), req)
if err != nil {
return err
}
if resp.TxResponse.Code != 0 {
return fmt.Errorf("tx failed: %s", resp.TxResponse.RawLog)
}
return nil
}
// queryBuilderParams returns the params of the builder module.
func (s *IntegrationTestSuite) queryBuilderParams() buildertypes.Params {
queryClient := buildertypes.NewQueryClient(s.createClientContext())
req := &buildertypes.QueryParamsRequest{}
resp, err := queryClient.Params(context.Background(), req)
s.Require().NoError(err)
return resp.Params
}
// queryBalancesOf returns the balances of an account.
func (s *IntegrationTestSuite) queryBalancesOf(address string) sdk.Coins {
queryClient := banktypes.NewQueryClient(s.createClientContext())
req := &banktypes.QueryAllBalancesRequest{Address: address}
resp, err := queryClient.AllBalances(context.Background(), req)
s.Require().NoError(err)
return resp.Balances
}
// queryBalanceOf returns the balance of an account for a specific denom.
func (s *IntegrationTestSuite) queryBalanceOf(address string, denom string) sdk.Coin {
queryClient := banktypes.NewQueryClient(s.createClientContext())
req := &banktypes.QueryBalanceRequest{Address: address, Denom: denom}
resp, err := queryClient.Balance(context.Background(), req)
s.Require().NoError(err)
return *resp.Balance
}
// queryAccount returns the account of an address.
func (s *IntegrationTestSuite) queryAccount(address sdk.AccAddress) *authtypes.BaseAccount {
queryClient := authtypes.NewQueryClient(s.createClientContext())
req := &authtypes.QueryAccountRequest{Address: address.String()}
resp, err := queryClient.Account(context.Background(), req)
s.Require().NoError(err)
account := &authtypes.BaseAccount{}
err = account.Unmarshal(resp.Account.Value)
s.Require().NoError(err)
return account
}
// queryCurrentHeight returns the current block height.
func (s *IntegrationTestSuite) queryCurrentHeight() uint64 {
queryClient := cmtclient.NewServiceClient(s.createClientContext())
req := &cmtclient.GetLatestBlockRequest{}
resp, err := queryClient.GetLatestBlock(context.Background(), req)
s.Require().NoError(err)
return uint64(resp.SdkBlock.Header.Height)
}
// queryBlockTxs returns the txs of the block at the given height.
func (s *IntegrationTestSuite) queryBlockTxs(height uint64) [][]byte {
queryClient := cmtclient.NewServiceClient(s.createClientContext())
req := &cmtclient.GetBlockByHeightRequest{Height: int64(height)}
resp, err := queryClient.GetBlockByHeight(context.Background(), req)
s.Require().NoError(err)
txs := resp.GetSdkBlock().Data.Txs
// The first transaction is the vote extension.
s.Require().Greater(len(txs), 0)
return txs[1:]
}
// queryTx returns information about a transaction.
func (s *IntegrationTestSuite) queryTx(txHash string) *txtypes.GetTxResponse {
queryClient := txtypes.NewServiceClient(s.createClientContext())
req := &txtypes.GetTxRequest{Hash: txHash}
resp, err := queryClient.GetTx(context.Background(), req)
s.Require().NoError(err)
return resp
}
// queryTxExecutionHeight returns the block height at which a transaction was executed.
func (s *IntegrationTestSuite) queryTxExecutionHeight(txHash string) uint64 {
txResp := s.queryTx(txHash)
return uint64(txResp.TxResponse.Height)
}
// queryValidators returns the validators of the network.
func (s *IntegrationTestSuite) queryValidators() []stakingtypes.Validator {
queryClient := stakingtypes.NewQueryClient(s.createClientContext())
req := &stakingtypes.QueryValidatorsRequest{}
resp, err := queryClient.Validators(context.Background(), req)
s.Require().NoError(err)
return resp.Validators
}

View File

@ -1,121 +0,0 @@
package e2e
import (
"encoding/json"
"fmt"
"os"
comettypes "github.com/cometbft/cometbft/types"
"github.com/cosmos/cosmos-sdk/server"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
"github.com/skip-mev/pob/x/builder/types"
)
func getGenDoc(path string) (*comettypes.GenesisDoc, error) {
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(path)
genFile := config.GenesisFile()
doc := &comettypes.GenesisDoc{}
if _, err := os.Stat(genFile); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
var err error
doc, err = comettypes.GenesisDocFromFile(genFile)
if err != nil {
return nil, fmt.Errorf("failed to read genesis doc from file: %w", err)
}
}
return doc, nil
}
func initGenesisFile(path, moniker, amountStr string, accAddr sdk.AccAddress, params types.Params) error {
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(path)
config.Moniker = moniker
coins, err := sdk.ParseCoinsNormalized(amountStr)
if err != nil {
return fmt.Errorf("failed to parse coins: %w", err)
}
balances := banktypes.Balance{Address: accAddr.String(), Coins: coins.Sort()}
genAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0)
genFile := config.GenesisFile()
appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile)
if err != nil {
return fmt.Errorf("failed to unmarshal genesis state: %w", err)
}
authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState)
accs, err := authtypes.UnpackAccounts(authGenState.Accounts)
if err != nil {
return fmt.Errorf("failed to get accounts from any: %w", err)
}
if accs.Contains(accAddr) {
return fmt.Errorf("failed to add account to genesis state; account already exists: %s", accAddr)
}
// Add the new account to the set of genesis accounts and sanitize the
// accounts afterwards.
accs = append(accs, genAccount)
accs = authtypes.SanitizeGenesisAccounts(accs)
genAccs, err := authtypes.PackAccounts(accs)
if err != nil {
return fmt.Errorf("failed to convert accounts into any's: %w", err)
}
authGenState.Accounts = genAccs
authGenStateBz, err := cdc.MarshalJSON(&authGenState)
if err != nil {
return fmt.Errorf("failed to marshal auth genesis state: %w", err)
}
appState[authtypes.ModuleName] = authGenStateBz
bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState)
bankGenState.Balances = append(bankGenState.Balances, balances)
bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances)
bankGenStateBz, err := cdc.MarshalJSON(bankGenState)
if err != nil {
return fmt.Errorf("failed to marshal bank genesis state: %w", err)
}
appState[banktypes.ModuleName] = bankGenStateBz
builderGenState := types.GetGenesisStateFromAppState(cdc, appState)
builderGenState.Params = params
builderGenStateBz, err := cdc.MarshalJSON(&builderGenState)
if err != nil {
return fmt.Errorf("failed to marshal builder genesis state: %w", err)
}
appState[types.ModuleName] = builderGenStateBz
appStateJSON, err := json.Marshal(appState)
if err != nil {
return fmt.Errorf("failed to marshal application genesis state: %w", err)
}
genDoc.AppState = appStateJSON
return genutil.ExportGenesisFile(genDoc, genFile)
}

View File

@ -1,42 +0,0 @@
package e2e
import (
"fmt"
"io"
"os"
)
func copyFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return 0, err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destination.Close()
nBytes, err := io.Copy(destination, source)
return nBytes, err
}
func writeFile(path string, body []byte) error {
_, err := os.Create(path)
if err != nil {
return err
}
return os.WriteFile(path, body, 0o600)
}

View File

@ -1,19 +0,0 @@
package e2e
import (
"github.com/cosmos/go-bip39"
)
func createMnemonic() (string, error) {
entropySeed, err := bip39.NewEntropy(256)
if err != nil {
return "", err
}
mnemonic, err := bip39.NewMnemonic(entropySeed)
if err != nil {
return "", err
}
return mnemonic, nil
}

View File

@ -1,45 +0,0 @@
package e2e
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec/unknownproto"
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
)
func decodeTx(txBytes []byte) (*sdktx.Tx, error) {
var raw sdktx.TxRaw
// reject all unknown proto fields in the root TxRaw
err := unknownproto.RejectUnknownFieldsStrict(txBytes, &raw, encodingConfig.InterfaceRegistry)
if err != nil {
return nil, fmt.Errorf("failed to reject unknown fields: %w", err)
}
if err := cdc.Unmarshal(txBytes, &raw); err != nil {
return nil, err
}
var body sdktx.TxBody
if err := cdc.Unmarshal(raw.BodyBytes, &body); err != nil {
return nil, fmt.Errorf("failed to decode tx: %w", err)
}
var authInfo sdktx.AuthInfo
// reject all unknown proto fields in AuthInfo
err = unknownproto.RejectUnknownFieldsStrict(raw.AuthInfoBytes, &authInfo, encodingConfig.InterfaceRegistry)
if err != nil {
return nil, fmt.Errorf("failed to reject unknown fields: %w", err)
}
if err := cdc.Unmarshal(raw.AuthInfoBytes, &authInfo); err != nil {
return nil, fmt.Errorf("failed to decode auth info: %w", err)
}
return &sdktx.Tx{
Body: &body,
AuthInfo: &authInfo,
Signatures: raw.Signatures,
}, nil
}

View File

@ -1,292 +0,0 @@
package e2e
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"cosmossdk.io/math"
cometcfg "github.com/cometbft/cometbft/config"
"github.com/cometbft/cometbft/p2p"
"github.com/cometbft/cometbft/privval"
sdkcrypto "github.com/cosmos/cosmos-sdk/crypto"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/server"
sdk "github.com/cosmos/cosmos-sdk/types"
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutilstypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/skip-mev/pob/tests/app"
)
type validator struct {
chain *chain
index int
moniker string
mnemonic string
keyInfo keyring.Record
privateKey cryptotypes.PrivKey
consensusKey privval.FilePVKey
nodeKey p2p.NodeKey
}
func (v *validator) instanceName() string {
return fmt.Sprintf("%s%d", v.moniker, v.index)
}
func (v *validator) configDir() string {
return fmt.Sprintf("%s/%s", v.chain.configDir(), v.instanceName())
}
func (v *validator) createConfig() error {
p := path.Join(v.configDir(), "config")
return os.MkdirAll(p, 0o755)
}
func (v *validator) init() error {
if err := v.createConfig(); err != nil {
return err
}
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(v.configDir())
config.Moniker = v.moniker
genDoc, err := getGenDoc(v.configDir())
if err != nil {
return err
}
appState, err := json.MarshalIndent(app.ModuleBasics.DefaultGenesis(cdc), "", " ")
if err != nil {
return fmt.Errorf("failed to JSON encode app genesis state: %w", err)
}
genDoc.ChainID = v.chain.id
genDoc.Validators = nil
genDoc.AppState = appState
if err := genDoc.SaveAs(config.GenesisFile()); err != nil {
return err
}
genAppState, err := genutilstypes.AppGenesisFromFile(config.GenesisFile())
if err != nil {
return fmt.Errorf("failed to unmarshal genesis state: %w", err)
}
if err = genutil.ExportGenesisFile(genAppState, config.GenesisFile()); err != nil {
return fmt.Errorf("failed to export app genesis state: %w", err)
}
cometcfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config)
return nil
}
func (v *validator) createNodeKey() error {
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(v.configDir())
config.Moniker = v.moniker
nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile())
if err != nil {
return err
}
v.nodeKey = *nodeKey
return nil
}
func (v *validator) createConsensusKey() error {
serverCtx := server.NewDefaultContext()
config := serverCtx.Config
config.SetRoot(v.configDir())
config.Moniker = v.moniker
pvKeyFile := config.PrivValidatorKeyFile()
if err := os.MkdirAll(filepath.Dir(pvKeyFile), 0o777); err != nil {
return fmt.Errorf("could not create directory %q: %w", filepath.Dir(pvKeyFile), err)
}
pvStateFile := config.PrivValidatorStateFile()
if err := os.MkdirAll(filepath.Dir(pvStateFile), 0o777); err != nil {
return fmt.Errorf("could not create directory %q: %w", filepath.Dir(pvStateFile), err)
}
filePV := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile)
v.consensusKey = filePV.Key
return nil
}
func (v *validator) createKeyFromMnemonic(name, mnemonic string) error {
kb, err := keyring.New(keyringAppName, keyring.BackendTest, v.configDir(), nil, cdc)
if err != nil {
return err
}
keyringAlgos, _ := kb.SupportedAlgorithms()
algo, err := keyring.NewSigningAlgoFromString(string(hd.Secp256k1Type), keyringAlgos)
if err != nil {
return err
}
info, err := kb.NewAccount(name, mnemonic, "", sdk.FullFundraiserPath, algo)
if err != nil {
return err
}
privKeyArmor, err := kb.ExportPrivKeyArmor(name, keyringPassphrase)
if err != nil {
return err
}
privKey, _, err := sdkcrypto.UnarmorDecryptPrivKey(privKeyArmor, keyringPassphrase)
if err != nil {
return err
}
v.keyInfo = *info
v.mnemonic = mnemonic
v.privateKey = privKey
return nil
}
func (v *validator) createKey(name string) error {
mnemonic, err := createMnemonic()
if err != nil {
return err
}
return v.createKeyFromMnemonic(name, mnemonic)
}
func (v *validator) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) {
description := stakingtypes.NewDescription(v.moniker, "", "", "", "")
commissionRates := stakingtypes.CommissionRates{
Rate: math.LegacyMustNewDecFromStr("0.1"),
MaxRate: math.LegacyMustNewDecFromStr("0.2"),
MaxChangeRate: math.LegacyMustNewDecFromStr("0.01"),
}
// get the initial validator min self delegation
minSelfDelegation := math.NewInt(1)
valPubKey, err := cryptocodec.FromCmtPubKeyInterface(v.consensusKey.PubKey)
if err != nil {
return nil, err
}
valAddr, err := v.keyInfo.GetAddress()
if err != nil {
return nil, err
}
return stakingtypes.NewMsgCreateValidator(
sdk.ValAddress(valAddr),
valPubKey,
amount,
description,
commissionRates,
minSelfDelegation,
)
}
func (v *validator) signMsg(msgs ...sdk.Msg) (*sdktx.Tx, error) {
txBuilder := encodingConfig.TxConfig.NewTxBuilder()
if err := txBuilder.SetMsgs(msgs...); err != nil {
return nil, err
}
txBuilder.SetMemo(fmt.Sprintf("%s@%s:26656", v.nodeKey.ID(), v.instanceName()))
txBuilder.SetFeeAmount(sdk.NewCoins())
txBuilder.SetGasLimit(200_000)
pubKey, err := v.keyInfo.GetPubKey()
if err != nil {
return nil, err
}
signerData := authsigning.SignerData{
ChainID: v.chain.id,
AccountNumber: 0,
Sequence: 0,
PubKey: pubKey,
}
// For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on
// TxBuilder under the hood, and SignerInfos is needed to generate the sign
// bytes. This is the reason for setting SetSignatures here, with a nil
// signature.
//
// Note: This line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it
// also doesn't affect its generated sign bytes, so for code's simplicity
// sake, we put it here.
if err != nil {
return nil, err
}
sig := signing.SignatureV2{
PubKey: pubKey,
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
Signature: nil,
},
Sequence: 0,
}
if err := txBuilder.SetSignatures(sig); err != nil {
return nil, err
}
bytesToSign, err := authsigning.GetSignBytesAdapter(
context.Background(),
encodingConfig.TxConfig.SignModeHandler(),
signing.SignMode_SIGN_MODE_DIRECT,
signerData,
txBuilder.GetTx(),
)
if err != nil {
return nil, err
}
sigBytes, err := v.privateKey.Sign(bytesToSign)
if err != nil {
return nil, err
}
sig = signing.SignatureV2{
PubKey: pubKey,
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
Signature: sigBytes,
},
Sequence: 0,
}
if err := txBuilder.SetSignatures(sig); err != nil {
return nil, err
}
signedTx := txBuilder.GetTx()
bz, err := encodingConfig.TxConfig.TxEncoder()(signedTx)
if err != nil {
return nil, err
}
return decodeTx(bz)
}

View File

@ -0,0 +1,341 @@
package integration
import (
"context"
"encoding/hex"
"encoding/json"
"strings"
"testing"
"time"
rpctypes "github.com/cometbft/cometbft/rpc/core/types"
comettypes "github.com/cometbft/cometbft/types"
"github.com/cosmos/cosmos-sdk/client/tx"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
buildertypes "github.com/skip-mev/pob/x/builder/types"
interchaintest "github.com/strangelove-ventures/interchaintest/v7"
"github.com/strangelove-ventures/interchaintest/v7/chain/cosmos"
"github.com/strangelove-ventures/interchaintest/v7/ibc"
"github.com/strangelove-ventures/interchaintest/v7/testutil"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// ChainBuilderFromChainSpec creates an interchaintest chain builder factory given a ChainSpec
// and returns the associated chain
func ChainBuilderFromChainSpec(t *testing.T, spec *interchaintest.ChainSpec) ibc.Chain {
// require that NumFullNodes == NumValidators == 4
require.Equal(t, *spec.NumValidators, 4)
cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{spec})
chains, err := cf.Chains(t.Name())
require.NoError(t, err)
require.Len(t, chains, 1)
chain := chains[0]
_, ok := chain.(*cosmos.CosmosChain)
require.True(t, ok)
return chain
}
// BuildPOBInterchain creates a new Interchain testing env with the configured POB CosmosChain
func BuildPOBInterchain(t *testing.T, ctx context.Context, chain ibc.Chain) *interchaintest.Interchain {
ic := interchaintest.NewInterchain()
ic.AddChain(chain)
// create docker network
client, networkID := interchaintest.DockerSetup(t)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
// build the interchain
err := ic.Build(ctx, nil, interchaintest.InterchainBuildOptions{
SkipPathCreation: true,
Client: client,
NetworkID: networkID,
TestName: t.Name(),
})
require.NoError(t, err)
return ic
}
// CreateTx creates a new transaction to be signed by the given user, including a provided set of messages
func CreateTx(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, user cosmos.User, seqIncrement, height uint64, GasPrice int64, msgs ...sdk.Msg) []byte {
// create a broadcaster
broadcaster := cosmos.NewBroadcaster(t, chain)
// create tx factory + Client Context
txf, err := broadcaster.GetFactory(ctx, user)
require.NoError(t, err)
cc, err := broadcaster.GetClientContext(ctx, user)
require.NoError(t, err)
txf, err = txf.Prepare(cc)
require.NoError(t, err)
// set timeout height
if height != 0 {
txf = txf.WithTimeoutHeight(height)
}
// get gas for tx
_, gas, err := tx.CalculateGas(cc, txf, msgs...)
require.NoError(t, err)
txf.WithGas(gas)
// update sequence number
txf = txf.WithSequence(txf.Sequence() + seqIncrement)
txf = txf.WithGasPrices(sdk.NewDecCoins(sdk.NewDecCoin(chain.Config().Denom, sdk.NewInt(GasPrice))).String())
// sign the tx
txBuilder, err := txf.BuildUnsignedTx(msgs...)
require.NoError(t, err)
require.NoError(t, tx.Sign(txf, cc.GetFromName(), txBuilder, true))
// encode and return
bz, err := cc.TxConfig.TxEncoder()(txBuilder.GetTx())
require.NoError(t, err)
return bz
}
// SimulateTx simulates the provided messages, and checks whether the provided failure condition is met
func SimulateTx(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, user cosmos.User, height uint64, expectFail bool, msgs ...sdk.Msg) {
// create a broadcaster
broadcaster := cosmos.NewBroadcaster(t, chain)
// create tx factory + Client Context
txf, err := broadcaster.GetFactory(ctx, user)
require.NoError(t, err)
cc, err := broadcaster.GetClientContext(ctx, user)
require.NoError(t, err)
txf, err = txf.Prepare(cc)
require.NoError(t, err)
// set timeout height
if height != 0 {
txf = txf.WithTimeoutHeight(height)
}
// get gas for tx
_, _, err = tx.CalculateGas(cc, txf, msgs...)
require.Equal(t, err != nil, expectFail)
}
type Tx struct {
User cosmos.User
Msgs []sdk.Msg
GasPrice int64
SequenceIncrement uint64
Height uint64
SkipInclusionCheck bool
ExpectFail bool
}
// CreateAuctionBidMsg creates a new AuctionBid tx signed by the given user, the order of txs in the MsgAuctionBid will be determined by the contents + order of the MessageForUsers
func CreateAuctionBidMsg(t *testing.T, ctx context.Context, searcher cosmos.User, chain *cosmos.CosmosChain, bid sdk.Coin, txsPerUser []Tx) (*buildertypes.MsgAuctionBid, [][]byte) {
// for each MessagesForUser get the signed bytes
txs := make([][]byte, len(txsPerUser))
for i, tx := range txsPerUser {
txs[i] = CreateTx(t, ctx, chain, tx.User, tx.SequenceIncrement, tx.Height, tx.GasPrice, tx.Msgs...)
}
bech32SearcherAddress := searcher.FormattedAddress()
accAddr, err := sdk.AccAddressFromBech32(bech32SearcherAddress)
require.NoError(t, err)
// create a message auction bid
return buildertypes.NewMsgAuctionBid(
accAddr,
bid,
txs,
), txs
}
// BroadcastTxs broadcasts the given messages for each user. This function returns the broadcasted txs. If a message
// is not expected to be included in a block, set SkipInclusionCheck to true and the method
// will not block on the tx's inclusion in a block, otherwise this method will block on the tx's inclusion
func BroadcastTxs(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, msgsPerUser []Tx) [][]byte {
txs := make([][]byte, len(msgsPerUser))
for i, msg := range msgsPerUser {
txs[i] = CreateTx(t, ctx, chain, msg.User, msg.SequenceIncrement, msg.Height, msg.GasPrice, msg.Msgs...)
}
// broadcast each tx
require.True(t, len(chain.Nodes()) > 0)
client := chain.Nodes()[0].Client
for i, tx := range txs {
// broadcast tx
_, err := client.BroadcastTxSync(ctx, tx)
// check execution was successful
if !msgsPerUser[i].ExpectFail {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}
// block on all txs being included in block
eg := errgroup.Group{}
for i, tx := range txs {
// if we don't expect this tx to be included.. skip it
if msgsPerUser[i].SkipInclusionCheck || msgsPerUser[i].ExpectFail {
continue
}
tx := tx // pin
eg.Go(func() error {
return testutil.WaitForCondition(4*time.Second, 500*time.Millisecond, func() (bool, error) {
res, err := client.Tx(context.Background(), comettypes.Tx(tx).Hash(), false)
if err != nil || res.TxResult.Code != uint32(0) {
return false, nil
}
return true, nil
})
})
}
require.NoError(t, eg.Wait())
return txs
}
// QueryBuilderParams queries the x/builder module's params
func QueryBuilderParams(t *testing.T, chain ibc.Chain) buildertypes.Params {
// cast chain to cosmos-chain
cosmosChain, ok := chain.(*cosmos.CosmosChain)
require.True(t, ok)
// get nodes
nodes := cosmosChain.Nodes()
require.True(t, len(nodes) > 0)
// make params query to first node
resp, _, err := nodes[0].ExecQuery(context.Background(), "builder", "params")
require.NoError(t, err)
// unmarshal params
var params buildertypes.Params
err = json.Unmarshal(resp, &params)
require.NoError(t, err)
return params
}
// QueryValidators queries for all of the network's validators
func QueryValidators(t *testing.T, chain *cosmos.CosmosChain) []sdk.ValAddress {
// get grpc client of the node
grpcAddr := chain.GetHostGRPCAddress()
cc, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
client := stakingtypes.NewQueryClient(cc)
// query validators
resp, err := client.Validators(context.Background(), &stakingtypes.QueryValidatorsRequest{})
require.NoError(t, err)
addrs := make([]sdk.ValAddress, len(resp.Validators))
// unmarshal validators
for i, val := range resp.Validators {
addrBz, err := sdk.GetFromBech32(val.OperatorAddress, chain.Config().Bech32Prefix+sdk.PrefixValidator+sdk.PrefixOperator)
require.NoError(t, err)
addrs[i] = sdk.ValAddress(addrBz)
}
return addrs
}
// QueryAccountBalance queries a given account's balance on the chain
func QueryAccountBalance(t *testing.T, chain ibc.Chain, address, denom string) int64 {
// cast the chain to a cosmos-chain
cosmosChain, ok := chain.(*cosmos.CosmosChain)
require.True(t, ok)
// get nodes
balance, err := cosmosChain.GetBalance(context.Background(), address, denom)
require.NoError(t, err)
return balance
}
// QueryAccountSequence
func QueryAccountSequence(t *testing.T, chain *cosmos.CosmosChain, address string) uint64 {
// get nodes
nodes := chain.Nodes()
require.True(t, len(nodes) > 0)
resp, _, err := nodes[0].ExecQuery(context.Background(), "auth", "account", address)
require.NoError(t, err)
// unmarshal json response
var accResp codectypes.Any
require.NoError(t, json.Unmarshal(resp, &accResp))
// unmarshal into baseAccount
var acc authtypes.BaseAccount
require.NoError(t, acc.Unmarshal(accResp.Value))
return acc.GetSequence()
}
// Block returns the block at the given height
func Block(t *testing.T, chain *cosmos.CosmosChain, height int64) *rpctypes.ResultBlock {
// get nodes
nodes := chain.Nodes()
require.True(t, len(nodes) > 0)
client := nodes[0].Client
resp, err := client.Block(context.Background(), &height)
require.NoError(t, err)
return resp
}
// WaitForHeight waits for the chain to reach the given height
func WaitForHeight(t *testing.T, chain *cosmos.CosmosChain, height uint64) {
// wait for next height
err := testutil.WaitForCondition(30*time.Second, time.Second, func() (bool, error) {
pollHeight, err := chain.Height(context.Background())
if err != nil {
return false, err
}
return pollHeight == height, nil
})
require.NoError(t, err)
}
// VerifyBlock takes a Block and verifies that it contains the given bid at the 0-th index, and the bundled txs immediately after
func VerifyBlock(t *testing.T, block *rpctypes.ResultBlock, offset int, bidTxHash string, txs [][]byte) {
// verify the block
if bidTxHash != "" {
require.Equal(t, bidTxHash, TxHash(block.Block.Data.Txs[offset]))
offset += 1
}
// verify the txs in sequence
for i, tx := range txs {
require.Equal(t, TxHash(tx), TxHash(block.Block.Data.Txs[i+offset]))
}
}
func TxHash(tx []byte) string {
return strings.ToUpper(hex.EncodeToString(comettypes.Tx(tx).Hash()))
}

246
tests/integration/go.mod Normal file
View File

@ -0,0 +1,246 @@
module github.com/skip-mev/pob/tests/integration
go 1.20
replace (
// interchaintest supports ICS features so we need this for now
// github.com/cosmos/cosmos-sdk => github.com/cosmos/cosmos-sdk v0.45.13-ics
github.com/ChainSafe/go-schnorrkel => github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d
github.com/ChainSafe/go-schnorrkel/1 => github.com/ChainSafe/go-schnorrkel v1.0.0
github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.22.2 //indirect
github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
github.com/vedhavyas/go-subkey => github.com/strangelove-ventures/go-subkey v1.0.7
)
require (
cosmossdk.io/api v0.3.1 // indirect
cosmossdk.io/core v0.5.1 // indirect
cosmossdk.io/depinject v1.0.0-alpha.3 // indirect
cosmossdk.io/errors v1.0.0 // indirect
cosmossdk.io/log v1.1.1-0.20230704160919-88f2c830b0ca // indirect
cosmossdk.io/math v1.0.1 // indirect
cosmossdk.io/tools/rosetta v0.2.1 // indirect
github.com/cosmos/cosmos-sdk v0.47.4
github.com/skip-mev/pob v1.0.3 // reference local
github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230721183422-fb937bb0e165
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.24.0
golang.org/x/sync v0.3.0
)
require (
github.com/cometbft/cometbft v0.37.2
google.golang.org/grpc v1.56.2
)
require (
cloud.google.com/go v0.110.4 // indirect
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/storage v1.30.1 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.2 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect
github.com/ChainSafe/go-schnorrkel/1 v0.0.0-00010101000000-000000000000 // indirect
github.com/ComposableFi/go-subkey/v2 v2.0.0-tm03420 // indirect
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/StirlingMarketingGroup/go-namecase v1.0.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/avast/retry-go/v4 v4.3.4 // indirect
github.com/aws/aws-sdk-go v1.44.203 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/coinbase/rosetta-sdk-go/types v1.0.0 // indirect
github.com/cometbft/cometbft-db v0.8.0 // indirect
github.com/confio/ics23/go v0.9.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.4.10 // indirect
github.com/cosmos/iavl v0.20.0 // indirect
github.com/cosmos/ibc-go/v7 v7.2.0 // indirect
github.com/cosmos/ics23/go v0.10.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.12.1 // indirect
github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect
github.com/creachadair/taskgroup v0.4.2 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/base58 v1.0.4 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.4+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/ethereum/go-ethereum v1.10.20 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.3 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/orderedcode v0.0.1 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.7.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/huandu/skiplist v1.2.0 // indirect
github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845 // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.2.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.22.0 // indirect
github.com/libp2p/go-openssl v0.1.0 // indirect
github.com/linxGnu/grocksdb v1.7.16 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-pointer v0.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/misko9/go-substrate-rpc-client/v4 v4.0.0-20230413215336-5bd2aea337ae // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/multiformats/go-base32 v0.0.4 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multiaddr v0.6.0 // indirect
github.com/multiformats/go-multibase v0.1.1 // indirect
github.com/multiformats/go-multicodec v0.5.0 // indirect
github.com/multiformats/go-multihash v0.2.1 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect
github.com/pierrec/xxHash v0.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rakyll/statik v0.1.7 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/cors v1.8.3 // indirect
github.com/rs/zerolog v1.29.1 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tyler-smith/go-bip32 v1.0.0 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/zondax/hid v0.9.1 // indirect
github.com/zondax/ledger-go v0.14.1 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.126.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.1.7 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.24.0 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
nhooyr.io/websocket v1.8.6 // indirect
pgregory.net/rapid v0.5.5 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1764
tests/integration/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
package integration_test
import (
"fmt"
"testing"
testutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
buildertypes "github.com/skip-mev/pob/x/builder/types"
"github.com/strangelove-ventures/interchaintest/v7"
"github.com/skip-mev/pob/tests/integration"
"github.com/strangelove-ventures/interchaintest/v7/chain/cosmos"
"github.com/strangelove-ventures/interchaintest/v7/ibc"
"github.com/stretchr/testify/suite"
)
var (
// config params
numValidators = 4
numFullNodes = 0
denom = "stake"
image = ibc.DockerImage{
Repository: "pob-integration",
Version: "latest",
UidGid: "1000:1000",
}
encodingConfig = MakeEncodingConfig()
noHostMount = false
gasAdjustment = float64(2.0)
genesisKV = []cosmos.GenesisKV{
{
Key: "app_state.builder.params.max_bundle_size",
Value: 3,
},
}
// interchain specification
spec = &interchaintest.ChainSpec{
ChainName: "pob",
Name: "pob",
NumValidators: &numValidators,
NumFullNodes: &numFullNodes,
Version: "latest",
NoHostMount: &noHostMount,
GasAdjustment: &gasAdjustment,
ChainConfig: ibc.ChainConfig{
EncodingConfig: encodingConfig,
Images: []ibc.DockerImage{
image,
},
Type: "cosmos",
Name: "pob",
Denom: denom,
ChainID: "chain-id-0",
Bin: "testappd",
Bech32Prefix: "cosmos",
CoinType: "118",
GasAdjustment: gasAdjustment,
GasPrices: fmt.Sprintf("0%s", denom),
TrustingPeriod: "48h",
NoHostMount: noHostMount,
UsingNewGenesisCommand: true,
ModifyGenesis: cosmos.ModifyGenesis(genesisKV),
},
}
)
func MakeEncodingConfig() *testutil.TestEncodingConfig {
cfg := cosmos.DefaultEncoding()
// register builder types
buildertypes.RegisterInterfaces(cfg.InterfaceRegistry)
return &cfg
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, integration.NewPOBIntegrationTestSuiteFromSpec(spec))
}

File diff suppressed because it is too large Load Diff