From b9d67617762c5159a531309f4b6ee7bdd548cfe3 Mon Sep 17 00:00:00 2001 From: David Terpay <35130517+davidterpay@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:10:13 -0400 Subject: [PATCH] feat(ABCI): New Proposal Struct with Associated Metadata (#126) * new proto types for proposal info * new proposal type * nits * lane input * lint * feat(ABCI): Deprecating `CheckOrderHandler` with new Proposal MetaData (#127) * refactor without checkorder * nits * more nits * lint * nits * feat(ABCI): Updating MEV lane to have no `CheckOrder` handler + testing (#128) * updating mev lane * nits * preventing adding multiple bid txs in prepare * update --- Makefile | 4 + abci/abci.go | 271 ++++--- abci/abci_test.go | 984 ++++++++++++++++++++---- abci/utils.go | 93 +++ abci/utils_test.go | 212 +++++ block/{utils => }/ante.go | 8 +- block/base/abci.go | 88 ++- block/base/handlers.go | 126 ++- block/base/lane.go | 21 - block/base/mempool.go | 12 +- block/base/types.go | 32 +- block/lane.go | 37 +- block/{utils => }/mocks/lane.go | 65 +- block/{utils => }/mocks/lane_mempool.go | 3 +- block/proposals.go | 205 ----- block/proposals/proposals.go | 94 +++ block/proposals/proposals_test.go | 540 +++++++++++++ block/proposals/types/types.pb.go | 572 ++++++++++++++ block/proposals/update.go | 113 +++ block/proposals/utils.go | 39 + block/types.go | 11 +- block/utils/utils.go | 71 +- block/utils/utils_test.go | 78 -- lanes/base/abci_test.go | 644 +++++++++++++--- lanes/base/mempool_test.go | 11 + lanes/build-your-own/README.md | 2 +- lanes/mev/abci.go | 351 +++++---- lanes/mev/factory_test.go | 14 +- lanes/mev/lane.go | 3 - lanes/terminator/lane.go | 12 +- proto/sdk/proposals/v1/types.proto | 23 + tests/app/ante.go | 10 +- tests/app/app.go | 5 +- tests/integration/block_sdk_suite.go | 2 +- tests/integration/chain_setup.go | 30 +- testutils/mocks.go | 2 +- testutils/utils.go | 14 +- x/auction/keeper/auction_test.go | 2 +- 38 files changed, 3705 insertions(+), 1099 deletions(-) create mode 100644 abci/utils.go create mode 100644 abci/utils_test.go rename block/{utils => }/ante.go (79%) rename block/{utils => }/mocks/lane.go (70%) rename block/{utils => }/mocks/lane_mempool.go (99%) delete mode 100644 block/proposals.go create mode 100644 block/proposals/proposals.go create mode 100644 block/proposals/proposals_test.go create mode 100644 block/proposals/types/types.pb.go create mode 100644 block/proposals/update.go create mode 100644 block/proposals/utils.go delete mode 100644 block/utils/utils_test.go create mode 100644 proto/sdk/proposals/v1/types.proto diff --git a/Makefile b/Makefile index 42f0b99..6aa8ec6 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,10 @@ use-integration: go work edit -dropuse . go work edit -use ./tests/integration +tidy: + go mod tidy + gofmt -s -w ./ + .PHONY: docker-build docker-build-integration ############################################################################### ## Docker ## diff --git a/abci/abci.go b/abci/abci.go index 955ce85..f868706 100644 --- a/abci/abci.go +++ b/abci/abci.go @@ -8,8 +8,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block" - "github.com/skip-mev/block-sdk/block/utils" - "github.com/skip-mev/block-sdk/lanes/terminator" + "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/proposals/types" +) + +const ( + // ProposalInfoIndex is the index of the proposal metadata in the proposal. + ProposalInfoIndex = 0 ) type ( @@ -18,66 +23,92 @@ type ( ProposalHandler struct { logger log.Logger txDecoder sdk.TxDecoder + txEncoder sdk.TxEncoder prepareLanesHandler block.PrepareLanesHandler - processLanesHandler block.ProcessLanesHandler mempool block.Mempool } ) -// NewProposalHandler returns a new abci++ proposal handler. This proposal handler will +// NewProposalHandler returns a new ABCI++ proposal handler. This proposal handler will // iteratively call each of the lanes in the chain to prepare and process the proposal. -func NewProposalHandler(logger log.Logger, txDecoder sdk.TxDecoder, mempool block.Mempool) *ProposalHandler { +func NewProposalHandler( + logger log.Logger, + txDecoder sdk.TxDecoder, + txEncoder sdk.TxEncoder, + mempool block.Mempool, +) *ProposalHandler { return &ProposalHandler{ logger: logger, txDecoder: txDecoder, - prepareLanesHandler: ChainPrepareLanes(mempool.Registry()...), - processLanesHandler: ChainProcessLanes(mempool.Registry()...), + txEncoder: txEncoder, + prepareLanesHandler: ChainPrepareLanes(mempool.Registry()), mempool: mempool, } } // PrepareProposalHandler prepares the proposal by selecting transactions from each lane -// according to each lane's selection logic. We select transactions in a greedy fashion. Note that -// each lane has an boundary on the number of bytes that can be included in the proposal. By default, -// the default lane will not have a boundary on the number of bytes that can be included in the proposal and -// will include all valid transactions in the proposal (up to MaxTxBytes). +// according to each lane's selection logic. We select transactions in the order in which the +// lanes are configured on the chain. Note that each lane has an boundary on the number of +// bytes/gas that can be included in the proposal. By default, the default lane will not have +// a boundary on the number of bytes that can be included in the proposal and will include all +// valid transactions in the proposal (up to MaxBlockSize, MaxGasLimit). func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (resp *abci.ResponsePrepareProposal, err error) { // In the case where there is a panic, we recover here and return an empty proposal. defer func() { - if err := recover(); err != nil { + if rec := recover(); rec != nil { h.logger.Error("failed to prepare proposal", "err", err) + + // TODO: Should we attempt to return a empty proposal here with empty proposal info? resp = &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)} + err = fmt.Errorf("failed to prepare proposal: %v", rec) } }() h.logger.Info("mempool distribution before proposal creation", "distribution", h.mempool.GetTxDistribution()) - proposal, err := h.prepareLanesHandler(ctx, block.NewProposal(req.MaxTxBytes)) + // Build an empty placeholder proposal with the maximum block size and gas limit. + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(ctx) + emptyProposal := proposals.NewProposal(h.txEncoder, maxBlockSize, maxGasLimit) + + // Fill the proposal with transactions from each lane. + finalProposal, err := h.prepareLanesHandler(ctx, emptyProposal) if err != nil { h.logger.Error("failed to prepare proposal", "err", err) return &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err } + // Retrieve the proposal with metadata and transactions. + txs, err := finalProposal.GetProposalWithInfo() + if err != nil { + h.logger.Error("failed to get proposal with metadata", "err", err) + return &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err + } + h.logger.Info( "prepared proposal", - "num_txs", proposal.GetNumTxs(), - "total_tx_bytes", proposal.GetTotalTxBytes(), + "num_txs", len(txs), + "total_tx_bytes", finalProposal.Info.BlockSize, + "max_tx_bytes", maxBlockSize, + "total_gas_limit", finalProposal.Info.GasLimit, + "max_gas_limit", maxGasLimit, "height", req.Height, ) h.logger.Info("mempool distribution after proposal creation", "distribution", h.mempool.GetTxDistribution()) return &abci.ResponsePrepareProposal{ - Txs: proposal.GetProposal(), + Txs: txs, }, nil } } // ProcessProposalHandler processes the proposal by verifying all transactions in the proposal -// according to each lane's verification logic. We verify proposals in a greedy fashion. -// If a lane's portion of the proposal is invalid, we reject the proposal. After a lane's portion -// of the proposal is verified, we pass the remaining transactions to the next lane in the chain. +// according to each lane's verification logic. Proposals are verified similar to how they are +// constructed. After a proposal is processed, it should amount to the same proposal that was prepared. +// Each proposal will first be broken down by the lanes that prepared each partial proposal. Then, each +// lane will iteratively verify the transactions that it belong to it. If any lane fails to verify the +// transactions, then the proposal is rejected. func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { return func(ctx sdk.Context, req *abci.RequestProcessProposal) (resp *abci.ResponseProcessProposal, err error) { // In the case where any of the lanes panic, we recover here and return a reject status. @@ -90,126 +121,132 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { } }() - txs := req.Txs - if len(txs) == 0 { - h.logger.Info("accepted empty proposal") - return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil - } - - // Decode the transactions from the proposal. - decodedTxs, err := utils.GetDecodedTxs(h.txDecoder, txs) + // Extract all of the lanes and their corresponding transactions from the proposal. + proposalInfo, partialProposals, err := h.ExtractLanes(req.Txs) if err != nil { - h.logger.Error("failed to decode transactions", "err", err) + h.logger.Error("failed to validate proposal", "err", err) return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err } - // Verify the proposal using the verification logic from each lane. - if _, err := h.processLanesHandler(ctx, decodedTxs); err != nil { + // Build handler that will verify the partial proposals according to each lane's verification logic. + processLanesHandler := ChainProcessLanes(partialProposals, h.mempool.Registry()) + + // Build an empty placeholder proposal. + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(ctx) + emptyProposal := proposals.NewProposal(h.txEncoder, maxBlockSize, maxGasLimit) + + // Verify the proposal according to the verification logic from each lane. + finalProposal, err := processLanesHandler(ctx, emptyProposal) + if err != nil { h.logger.Error("failed to validate the proposal", "err", err) return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err } - h.logger.Info("validated proposal", "num_txs", len(txs)) + // Ensure block size and gas limit are correct. + if err := h.ValidateBlockLimits(finalProposal, proposalInfo); err != nil { + h.logger.Error("failed to validate the proposal", "err", err) + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err + } + + h.logger.Info( + "processed proposal", + "num_txs", len(req.Txs), + "total_tx_bytes", finalProposal.Info.BlockSize, + "max_tx_bytes", maxBlockSize, + "total_gas_limit", finalProposal.Info.GasLimit, + "max_gas_limit", maxGasLimit, + "height", req.Height, + ) return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil } } -// ChainPrepareLanes chains together the proposal preparation logic from each lane -// into a single function. The first lane in the chain is the first lane to be prepared and -// the last lane in the chain is the last lane to be prepared. -// -// In the case where any of the lanes fail to prepare the partial proposal, the lane that failed -// will be skipped and the next lane in the chain will be called to prepare the proposal. -func ChainPrepareLanes(chain ...block.Lane) block.PrepareLanesHandler { - if len(chain) == 0 { - return nil +// ExtractLanes validates the proposal against the basic invariants that are required +// for the proposal to be valid. This includes: +// 1. The proposal must contain the proposal information and must be valid. +// 2. The proposal must contain the correct number of transactions for each lane. +func (h *ProposalHandler) ExtractLanes(proposal [][]byte) (types.ProposalInfo, [][][]byte, error) { + // If the proposal is empty, then the metadata was not included. + if len(proposal) == 0 { + return types.ProposalInfo{}, nil, fmt.Errorf("proposal does not contain proposal metadata") } - // Handle non-terminated decorators chain - if (chain[len(chain)-1] != terminator.Terminator{}) { - chain = append(chain, terminator.Terminator{}) + metaDataBz, txs := proposal[ProposalInfoIndex], proposal[ProposalInfoIndex+1:] + + // Retrieve the metadata from the proposal. + var metaData types.ProposalInfo + if err := metaData.Unmarshal(metaDataBz); err != nil { + return types.ProposalInfo{}, nil, fmt.Errorf("failed to unmarshal proposal metadata: %w", err) } - return func(ctx sdk.Context, partialProposal block.BlockProposal) (finalProposal block.BlockProposal, err error) { - lane := chain[0] - lane.Logger().Info("preparing lane", "lane", lane.Name()) + lanes := h.mempool.Registry() + partialProposals := make([][][]byte, len(lanes)) - // Cache the context in the case where any of the lanes fail to prepare the proposal. - cacheCtx, write := ctx.CacheContext() - - // We utilize a recover to handle any panics or errors that occur during the preparation - // of a lane's transactions. This defer will first check if there was a panic or error - // thrown from the lane's preparation logic. If there was, we log the error, skip the lane, - // and call the next lane in the chain to the prepare the proposal. - defer func() { - if rec := recover(); rec != nil || err != nil { - lane.Logger().Error("failed to prepare lane", "lane", lane.Name(), "err", err, "recover_error", rec) - lane.Logger().Info("skipping lane", "lane", lane.Name()) - - if len(chain) <= 2 { - // If there are only two lanes remaining, then the first lane in the chain - // is the lane that failed to prepare the partial proposal and the second lane in the - // chain is the terminator lane. We return the proposal as is. - finalProposal, err = partialProposal, nil - } else { - // If there are more than two lanes remaining, then the first lane in the chain - // is the lane that failed to prepare the proposal but the second lane in the - // chain is not the terminator lane so there could potentially be more transactions - // added to the proposal - finalProposal, err = ChainPrepareLanes(chain[1:]...)(ctx, partialProposal) - } - } else { - // Write the cache to the context since we know that the lane successfully prepared - // the partial proposal. State is written to in a backwards, cascading fashion. This means - // that the final context will only be updated after all other lanes have successfully - // prepared the partial proposal. - write() - } - }() - - // Get the maximum number of bytes that can be included in the proposal for this lane. - maxTxBytesForLane := utils.GetMaxTxBytesForLane( - partialProposal.GetMaxTxBytes(), - partialProposal.GetTotalTxBytes(), - lane.GetMaxBlockSpace(), - ) - - return lane.PrepareLane( - cacheCtx, - partialProposal, - maxTxBytesForLane, - ChainPrepareLanes(chain[1:]...), - ) - } -} - -// ChainProcessLanes chains together the proposal verification logic from each lane -// into a single function. The first lane in the chain is the first lane to be verified and -// the last lane in the chain is the last lane to be verified. -func ChainProcessLanes(chain ...block.Lane) block.ProcessLanesHandler { - if len(chain) == 0 { - return nil - } - - // Handle non-terminated decorators chain - if (chain[len(chain)-1] != terminator.Terminator{}) { - chain = append(chain, terminator.Terminator{}) - } - - return func(ctx sdk.Context, proposalTxs []sdk.Tx) (sdk.Context, error) { - // Short circuit if there are no transactions to process. - if len(proposalTxs) == 0 { - return ctx, nil + if metaData.TxsByLane == nil { + if len(txs) > 0 { + return types.ProposalInfo{}, nil, fmt.Errorf("proposal contains invalid number of transactions") } - chain[0].Logger().Info("processing lane", "lane", chain[0].Name()) + return types.ProposalInfo{}, partialProposals, nil + } - if err := chain[0].CheckOrder(ctx, proposalTxs); err != nil { - chain[0].Logger().Error("failed to process lane", "lane", chain[0].Name(), "err", err) - return ctx, err + h.logger.Info( + "received proposal with metadata", + "max_block_size", metaData.MaxBlockSize, + "max_gas_limit", metaData.MaxGasLimit, + "gas_limit", metaData.GasLimit, + "block_size", metaData.BlockSize, + "lanes_with_txs", metaData.TxsByLane, + ) + + // Iterate through all of the lanes and match the corresponding transactions to the lane. + for index, lane := range lanes { + numTxs := metaData.TxsByLane[lane.Name()] + if numTxs > uint64(len(txs)) { + return types.ProposalInfo{}, nil, fmt.Errorf( + "proposal metadata contains invalid number of transactions for lane %s; got %d, expected %d", + lane.Name(), + len(txs), + numTxs, + ) } - return chain[0].ProcessLane(ctx, proposalTxs, ChainProcessLanes(chain[1:]...)) + partialProposals[index] = txs[:numTxs] + txs = txs[numTxs:] } + + // If there are any transactions remaining in the proposal, then the proposal is invalid. + if len(txs) > 0 { + return types.ProposalInfo{}, nil, fmt.Errorf("proposal contains invalid number of transactions") + } + + return metaData, partialProposals, nil +} + +// ValidateBlockLimits validates the block limits of the proposal against the block limits +// of the chain. +func (h *ProposalHandler) ValidateBlockLimits(finalProposal proposals.Proposal, proposalInfo types.ProposalInfo) error { + // Conduct final checks on block size and gas limit. + if finalProposal.Info.BlockSize != proposalInfo.BlockSize { + h.logger.Error( + "proposal block size does not match", + "expected", proposalInfo.BlockSize, + "got", finalProposal.Info.BlockSize, + ) + + return fmt.Errorf("proposal block size does not match") + } + + if finalProposal.Info.GasLimit != proposalInfo.GasLimit { + h.logger.Error( + "proposal gas limit does not match", + "expected", proposalInfo.GasLimit, + "got", finalProposal.Info.GasLimit, + ) + + return fmt.Errorf("proposal gas limit does not match") + } + + return nil } diff --git a/abci/abci_test.go b/abci/abci_test.go index 1c26c00..5a84a29 100644 --- a/abci/abci_test.go +++ b/abci/abci_test.go @@ -1,9 +1,6 @@ package abci_test import ( - "crypto/sha256" - "encoding/hex" - "fmt" "math/rand" "testing" @@ -16,12 +13,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/skip-mev/block-sdk/abci" - signer_extraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" "github.com/skip-mev/block-sdk/block" - "github.com/skip-mev/block-sdk/block/base" - defaultlane "github.com/skip-mev/block-sdk/lanes/base" - "github.com/skip-mev/block-sdk/lanes/free" - "github.com/skip-mev/block-sdk/lanes/mev" + "github.com/skip-mev/block-sdk/block/proposals" testutils "github.com/skip-mev/block-sdk/testutils" ) @@ -36,7 +29,7 @@ type ProposalsTestSuite struct { gasTokenDenom string } -func TestBlockBusterTestSuite(t *testing.T) { +func TestProposalsTestSuite(t *testing.T) { suite.Run(t, new(ProposalsTestSuite)) } @@ -54,6 +47,10 @@ func (s *ProposalsTestSuite) SetupTest() { s.ctx = testCtx.Ctx.WithIsCheckTx(true) } +func (s *ProposalsTestSuite) SetupSubTest() { + s.setBlockParams(1000000000000, 1000000000000) +} + func (s *ProposalsTestSuite) TestPrepareProposal() { s.Run("can prepare a proposal with no transactions", func() { // Set up the default lane with no transactions @@ -64,7 +61,18 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) - s.Require().Equal(0, len(resp.Txs)) + s.Require().Equal(1, len(resp.Txs)) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(0, len(info.TxsByLane)) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal with a single tx from the lane", func() { @@ -75,6 +83,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -84,13 +93,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) proposalHandler := s.setUpProposalHandlers([]block.Lane{defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NotNil(resp) s.Require().NoError(err) proposal := s.getTxBytes(tx) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal with multiple txs from the lane", func() { @@ -101,6 +122,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -112,6 +134,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 1, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), ) s.Require().NoError(err) @@ -122,13 +145,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) proposalHandler := s.setUpProposalHandlers([]block.Lane{defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NotNil(resp) s.Require().NoError(err) proposal := s.getTxBytes(tx2, tx1) - s.Require().Equal(2, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(3, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(2), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal with single tx with other that fails", func() { @@ -139,6 +174,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -150,6 +186,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 1, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(200000000)), ) s.Require().NoError(err) @@ -160,13 +197,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx2)) proposalHandler := s.setUpProposalHandlers([]block.Lane{defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NotNil(resp) s.Require().NoError(err) proposal := s.getTxBytes(tx1) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal an empty proposal with multiple lanes", func() { @@ -179,7 +228,18 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { s.Require().NoError(err) s.Require().NotNil(resp) - s.Require().Equal(0, len(resp.Txs)) + s.Require().Equal(1, len(resp.Txs)) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(0, len(info.TxsByLane)) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal with transactions from a single lane given multiple lanes", func() { @@ -191,6 +251,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, s.accounts[0:1], + 100, ) s.Require().NoError(err) @@ -205,13 +266,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx, bundleTxs[0]) - s.Require().Equal(2, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(3, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(2), info.TxsByLane[mevLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can ignore txs that are already included in a proposal", func() { @@ -223,6 +296,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, s.accounts[0:1], + 100, ) s.Require().NoError(err) @@ -243,16 +317,28 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx, bundleTxs[0]) - s.Require().Equal(2, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(3, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(2), info.TxsByLane[mevLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) - s.Run("can build a proposal where first lane has too large of a tx and second lane has a valid tx", func() { + s.Run("can build a proposal where first lane has failing tx and second lane has a valid tx", func() { // Create a bid tx that includes a single bundled tx tx, bundleTxs, err := testutils.CreateAuctionTx( s.encodingConfig.TxConfig, @@ -261,6 +347,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, s.accounts[0:1], + 100, ) s.Require().NoError(err) @@ -272,7 +359,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { s.Require().NoError(mevLane.Insert(sdk.Context{}, tx)) // Set up the default lane with the bid tx and the bundled tx - defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0"), map[sdk.Tx]bool{ // Even though this passes it should not include it in the proposal because it is in the ignore list tx: true, bundleTxs[0]: true, @@ -282,13 +369,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 10000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(bundleTxs[0]) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal where first lane cannot fit txs but second lane can", func() { @@ -300,6 +399,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, s.accounts[0:1], + 100, ) s.Require().NoError(err) @@ -323,12 +423,25 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposal := s.getTxBytes(tx, bundleTxs[0]) size := int64(len(proposal[0]) - 1) - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: size}) + s.setBlockParams(10000000, size) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal[1:], resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal[1:], resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal with single tx from middle lane", func() { @@ -359,12 +472,24 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposal := s.getTxBytes(freeTx) - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[freeLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("transaction from every lane", func() { @@ -376,6 +501,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, s.accounts[0:4], + 100, ) s.Require().NoError(err) @@ -386,6 +512,7 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { 0, 0, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -424,12 +551,90 @@ func (s *ProposalsTestSuite) TestPrepareProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, freeLane, defaultLane}).PrepareProposalHandler() proposal := s.getTxBytes(tx, bundleTxs[0], bundleTxs[1], bundleTxs[2], bundleTxs[3], freeTx, normalTx) - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) - s.Require().Equal(7, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(8, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(3, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[freeLane.Name()]) + s.Require().Equal(uint64(5), info.TxsByLane[mevLane.Name()]) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) + }) + + s.Run("can build a proposal where first lane does not have enough gas but second lane does", func() { + // set up the gas block limit for the proposal + s.setBlockParams(100, 1000000000) + + // Create a bid tx that includes a single bundled tx + tx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[0:1], + 51, + ) + s.Require().NoError(err) + + // Set up the TOB lane with the bid tx and the bundled tx + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + tx: true, + bundleTxs[0]: true, + }) + s.Require().NoError(mevLane.Insert(sdk.Context{}, tx)) + + // create a random tx to be included in the default lane + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + 100, // This should consume all of the block limit + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + normalTx: true, + }) + s.Require().NoError(defaultLane.Insert(sdk.Context{}, normalTx)) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).PrepareProposalHandler() + proposal := s.getTxBytes(tx, bundleTxs[0], normalTx) + + // Should be theoretically sufficient to fit the bid tx and the bundled tx + normal tx + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) + s.Require().NoError(err) + s.Require().NotNil(resp) + + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal[2:], resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) } @@ -443,6 +648,7 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { 0, 0, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -457,16 +663,29 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { proposalHandler := abci.NewProposalHandler( log.NewTestLogger(s.T()), s.encodingConfig.TxConfig.TxDecoder(), + s.encodingConfig.TxConfig.TxEncoder(), mempool, ).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal if second lane panics", func() { @@ -478,6 +697,7 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { 0, 0, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -492,16 +712,29 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { proposalHandler := abci.NewProposalHandler( log.NewTestLogger(s.T()), s.encodingConfig.TxConfig.TxDecoder(), + s.encodingConfig.TxConfig.TxEncoder(), mempool, ).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal if multiple consecutive lanes panic", func() { @@ -514,6 +747,7 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { 0, 0, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -528,16 +762,29 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { proposalHandler := abci.NewProposalHandler( log.NewTestLogger(s.T()), s.encodingConfig.TxConfig.TxDecoder(), + s.encodingConfig.TxConfig.TxEncoder(), mempool, ).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) s.Run("can build a proposal if the last few lanes panic", func() { @@ -550,6 +797,7 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { 0, 0, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), ) s.Require().NoError(err) @@ -564,16 +812,29 @@ func (s *ProposalsTestSuite) TestPrepareProposalEdgeCases() { proposalHandler := abci.NewProposalHandler( log.NewTestLogger(s.T()), s.encodingConfig.TxConfig.TxDecoder(), + s.encodingConfig.TxConfig.TxEncoder(), mempool, ).PrepareProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{MaxTxBytes: 1000000}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestPrepareProposal{}) s.Require().NoError(err) s.Require().NotNil(resp) proposal := s.getTxBytes(tx) - s.Require().Equal(1, len(resp.Txs)) - s.Require().Equal(proposal, resp.Txs) + s.Require().Equal(2, len(resp.Txs)) + s.Require().Equal(proposal, resp.Txs[1:]) + + info := s.getProposalInfo(resp.Txs[0]) + s.Require().NotNil(info) + s.Require().Equal(1, len(info.TxsByLane)) + s.Require().Equal(uint64(1), info.TxsByLane[defaultLane.Name()]) + + maxBlockSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + s.Require().Equal(maxBlockSize, info.MaxBlockSize) + s.Require().Equal(maxGasLimit, info.MaxGasLimit) + + s.Require().LessOrEqual(info.BlockSize, info.MaxBlockSize) + s.Require().LessOrEqual(info.GasLimit, info.MaxGasLimit) }) } @@ -585,12 +846,171 @@ func (s *ProposalsTestSuite) TestProcessProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, freeLane, defaultLane}).ProcessProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: nil}) + info := s.createProposalInfoBytes( + 0, + 0, + 0, + 0, + nil, + ) + proposal := [][]byte{info} + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) s.Require().NoError(err) s.Require().NotNil(resp) s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, resp) }) + s.Run("can process a valid proposal with a single tx", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Mev lane + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 1}, tx) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, resp) + }) + + s.Run("can process a valid proposal with txs from multiple lanes", func() { + // Create a random transaction that will be inserted into the default lane + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // create a bid tx that will be inserted into the mev lane + bidTx, bundleTxs, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[1], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 0, + s.accounts[1:2], + 100, + ) + s.Require().NoError(err) + + // create a free tx that will be inserted into the free lane + freeTx, err := testutils.CreateFreeTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + "test", + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Mev lane + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + bidTx: true, + bundleTxs[0]: true, + }) + freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{ + freeTx: true, + }) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 1, mevLane.Name(): 2, freeLane.Name(): 1}, bidTx, bundleTxs[0], freeTx, tx) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, freeLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_ACCEPT}, resp) + }) + + s.Run("rejects a proposal with mismatching block size", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 100, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 1, mevLane.Name(): 0}, tx) + + // modify the block size to be 1 + info := s.getProposalInfo(proposal[0]) + info.BlockSize-- + infoBz, err := info.Marshal() + s.Require().NoError(err) + proposal[0] = infoBz + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + + s.Run("rejects a proposal with mismatching gas limit", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 100, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{ + tx: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 1, mevLane.Name(): 0}, tx) + + // modify the block size to be 1 + info := s.getProposalInfo(proposal[0]) + info.GasLimit-- + infoBz, err := info.Marshal() + s.Require().NoError(err) + proposal[0] = infoBz + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().Error(err) + s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) + }) + s.Run("rejects a proposal with bad txs", func() { mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) freeLane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), map[sdk.Tx]bool{}) @@ -598,7 +1018,20 @@ func (s *ProposalsTestSuite) TestProcessProposal() { proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, freeLane, defaultLane}).ProcessProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{{0x01, 0x02, 0x03}}}) + info := s.createProposalInfoBytes( + 0, + 0, + 0, + 0, + map[string]uint64{ + mevLane.Name(): 0, + freeLane.Name(): 0, + defaultLane.Name(): 1, + }, + ) + proposal := [][]byte{info, {0x01, 0x02, 0x03}} + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) s.Require().Error(err) s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) }) @@ -613,24 +1046,37 @@ func (s *ProposalsTestSuite) TestProcessProposal() { 0, 0, 0, + 1, ) s.Require().NoError(err) proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, panicLane}).ProcessProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: [][]byte{txbz}}) + + info := s.createProposalInfoBytes( + 0, + 0, + 0, + 0, + map[string]uint64{ + panicLane.Name(): 1, + }, + ) + proposal := [][]byte{info, txbz} + + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) s.Require().Error(err) s.Require().Equal(&cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, resp) }) s.Run("can process a invalid proposal (out of order)", func() { // Create a random transaction that will be inserted into the default lane - tx, err := testutils.CreateRandomTx( + tx, err := testutils.CreateAuctionTxWithSigners( s.encodingConfig.TxConfig, s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), 0, 1, - 0, - sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + nil, ) s.Require().NoError(err) @@ -641,16 +1087,22 @@ func (s *ProposalsTestSuite) TestProcessProposal() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), ) s.Require().NoError(err) + // Mev lane + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{tx: true}) + // Set up the default lane - defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{tx: true}) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), map[sdk.Tx]bool{tx2: true}) s.Require().NoError(defaultLane.Insert(sdk.Context{}, tx)) - proposalHandler := s.setUpProposalHandlers([]block.Lane{defaultLane}).ProcessProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(tx, tx2)}) + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 1, mevLane.Name(): 1}, tx2, tx) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) s.Require().NotNil(resp) s.Require().Error(err) }) @@ -663,6 +1115,7 @@ func (s *ProposalsTestSuite) TestProcessProposal() { 0, 1, s.accounts[0:2], + 10, ) s.Require().NoError(err) @@ -672,7 +1125,8 @@ func (s *ProposalsTestSuite) TestProcessProposal() { 0, 1, 0, - sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), ) s.Require().NoError(err) @@ -682,136 +1136,340 @@ func (s *ProposalsTestSuite) TestProcessProposal() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), ) s.Require().NoError(err) // Set up the default lane - defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.5"), nil) - defaultLane.SetProcessLaneHandler(base.NoOpProcessLaneHandler()) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + normalTx: true, + normalTx2: false, + }) // Set up the TOB lane - mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), nil) - mevLane.SetProcessLaneHandler(base.NoOpProcessLaneHandler()) + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + bidTx: true, + bundle[0]: true, + bundle[1]: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 2, mevLane.Name(): 3}, bidTx, bundle[0], bundle[1], normalTx, normalTx2) proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() - resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: s.getTxBytes(bidTx, bundle[0], bundle[1], normalTx, normalTx2)}) + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) + + s.Run("can process a invalid proposal where a lane consumes too much gas", func() { + s.setBlockParams(1000, 10000000) + + bidTx, _, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 1, + s.accounts[0:0], + 10000000000, // This should consume too much gas for the lane + ) + s.Require().NoError(err) + + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), + ) + s.Require().NoError(err) + + normalTx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + // Set up the default lane + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0"), nil) + + // Set up the TOB lane + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.1"), nil) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 2, mevLane.Name(): 1}, bidTx, normalTx, normalTx2) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) + s.Require().NotNil(resp) + s.Require().Error(err) + }) + + s.Run("can process a invalid proposal where a lane consumes too much block space", func() { + bidTx, _, err := testutils.CreateAuctionTx( + s.encodingConfig.TxConfig, + s.accounts[0], + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + 0, + 1, + s.accounts[0:0], + 1, + ) + s.Require().NoError(err) + + normalTx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(3000000)), + ) + s.Require().NoError(err) + + normalTx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2000000)), + ) + s.Require().NoError(err) + + proposalTxs := s.getTxBytes(bidTx, normalTx, normalTx2) + + s.setBlockParams(1000, int64(len(proposalTxs[0])+len(proposalTxs[1])+len(proposalTxs[2])-1)) + + // Set up the default lane + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + normalTx: true, + normalTx2: true, + }) + + // Set up the TOB lane + mevLane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.5"), map[sdk.Tx]bool{ + bidTx: true, + }) + + proposal := s.createProposal(map[string]uint64{defaultLane.Name(): 2, mevLane.Name(): 1}, bidTx, normalTx, normalTx2) + + proposalHandler := s.setUpProposalHandlers([]block.Lane{mevLane, defaultLane}).ProcessProposalHandler() + resp, err := proposalHandler(s.ctx, &cometabci.RequestProcessProposal{Txs: proposal}) s.Require().NotNil(resp) s.Require().Error(err) }) } -func (s *ProposalsTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk.AnteHandler { - txCache := make(map[string]bool) - for tx, pass := range expectedExecution { - bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) +func (s *ProposalsTestSuite) TestValidateBasic() { + // Set up the default lane with no transactions + mevlane := s.setUpTOBLane(math.LegacyMustNewDecFromStr("0.25"), nil) + freelane := s.setUpFreeLane(math.LegacyMustNewDecFromStr("0.25"), nil) + defaultLane := s.setUpStandardLane(math.LegacyMustNewDecFromStr("0.0"), nil) + + proposalHandlers := s.setUpProposalHandlers([]block.Lane{ + mevlane, + freelane, + defaultLane, + }) + + s.Run("can validate an empty proposal", func() { + info := s.createProposalInfoBytes(0, 0, 0, 0, nil) + proposal := [][]byte{info} + + _, partialProposals, err := proposalHandlers.ExtractLanes(proposal) s.Require().NoError(err) + s.Require().Equal(3, len(partialProposals)) - hash := sha256.Sum256(bz) - hashStr := hex.EncodeToString(hash[:]) - txCache[hashStr] = pass - } - - anteHandler := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { - bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) - s.Require().NoError(err) - - hash := sha256.Sum256(bz) - hashStr := hex.EncodeToString(hash[:]) - - pass, found := txCache[hashStr] - if !found { - return ctx, fmt.Errorf("tx not found") + for _, partialProposal := range partialProposals { + s.Require().Equal(0, len(partialProposal)) } + }) - if pass { - return ctx, nil - } + s.Run("should invalidate proposal with mismatch in transactions and proposal info", func() { + info := s.createProposalInfoBytes(0, 0, 0, 0, nil) + proposal := [][]byte{info, {0x01, 0x02, 0x03}} - return ctx, fmt.Errorf("tx failed") - } + _, _, err := proposalHandlers.ExtractLanes(proposal) + s.Require().Error(err) + }) - return anteHandler -} + s.Run("should invalidate proposal without info", func() { + proposal := [][]byte{{0x01, 0x02, 0x03}} -func (s *ProposalsTestSuite) setUpStandardLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *defaultlane.DefaultLane { - cfg := base.LaneConfig{ - Logger: log.NewTestLogger(s.T()), - TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), - SignerExtractor: signer_extraction.NewDefaultAdapter(), - AnteHandler: s.setUpAnteHandler(expectedExecution), - MaxBlockSpace: maxBlockSpace, - } + _, _, err := proposalHandlers.ExtractLanes(proposal) + s.Require().Error(err) + }) - return defaultlane.NewDefaultLane(cfg) -} + s.Run("should invalidate completely empty proposal", func() { + proposal := [][]byte{} -func (s *ProposalsTestSuite) setUpTOBLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *mev.MEVLane { - cfg := base.LaneConfig{ - Logger: log.NewTestLogger(s.T()), - TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: s.setUpAnteHandler(expectedExecution), - SignerExtractor: signer_extraction.NewDefaultAdapter(), - MaxBlockSpace: maxBlockSpace, - } + _, _, err := proposalHandlers.ExtractLanes(proposal) + s.Require().Error(err) + }) - return mev.NewMEVLane(cfg, mev.NewDefaultAuctionFactory(cfg.TxDecoder, signer_extraction.NewDefaultAdapter())) -} + s.Run("should invalidate proposal with mismatch txs count with proposal info", func() { + info := s.createProposalInfoBytes(0, 0, 0, 0, nil) + proposal := [][]byte{info, {0x01, 0x02, 0x03}, {0x01, 0x02, 0x03}} -func (s *ProposalsTestSuite) setUpFreeLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *free.FreeLane { - cfg := base.LaneConfig{ - Logger: log.NewTestLogger(s.T()), - TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), - AnteHandler: s.setUpAnteHandler(expectedExecution), - SignerExtractor: signer_extraction.NewDefaultAdapter(), - MaxBlockSpace: maxBlockSpace, - } + _, _, err := proposalHandlers.ExtractLanes(proposal) + s.Require().Error(err) + }) - return free.NewFreeLane(cfg, base.DefaultTxPriority(), free.DefaultMatchHandler()) -} + s.Run("can validate a proposal with a single tx", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + proposal := s.getTxBytes(tx) -func (s *ProposalsTestSuite) setUpPanicLane(maxBlockSpace math.LegacyDec) *base.BaseLane { - cfg := base.LaneConfig{ - Logger: log.NewTestLogger(s.T()), - TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), - TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), - SignerExtractor: signer_extraction.NewDefaultAdapter(), - MaxBlockSpace: maxBlockSpace, - } + size, limit := s.getTxInfos(tx) + maxSize, maxLimit := proposals.GetBlockLimits(s.ctx) + info := s.createProposalInfoBytes( + maxLimit, + limit, + maxSize, + size, + map[string]uint64{ + defaultLane.Name(): 1, + }, + ) - lane := base.NewBaseLane( - cfg, - "panic", - base.NewMempool[string](base.DefaultTxPriority(), cfg.TxEncoder, signer_extraction.NewDefaultAdapter(), 0), - base.DefaultMatchHandler(), - ) + proposal = append([][]byte{info}, proposal...) - lane.SetPrepareLaneHandler(base.PanicPrepareLaneHandler()) - lane.SetProcessLaneHandler(base.PanicProcessLaneHandler()) - - return lane -} - -func (s *ProposalsTestSuite) setUpProposalHandlers(lanes []block.Lane) *abci.ProposalHandler { - mempool := block.NewLanedMempool(log.NewTestLogger(s.T()), true, lanes...) - - return abci.NewProposalHandler( - log.NewTestLogger(s.T()), - s.encodingConfig.TxConfig.TxDecoder(), - mempool, - ) -} - -func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte { - txBytes := make([][]byte, len(txs)) - for i, tx := range txs { - bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + _, partialProposals, err := proposalHandlers.ExtractLanes(proposal) s.Require().NoError(err) - txBytes[i] = bz - } - return txBytes + s.Require().Equal(3, len(partialProposals)) + s.Require().Equal(0, len(partialProposals[0])) + s.Require().Equal(0, len(partialProposals[1])) + s.Require().Equal(1, len(partialProposals[2])) + s.Require().Equal(proposal[1], partialProposals[2][0]) + }) + + s.Run("can validate a proposal with multiple txs from single lane", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + + proposal := s.getTxBytes(tx, tx2) + + size, limit := s.getTxInfos(tx, tx2) + maxSize, maxLimit := proposals.GetBlockLimits(s.ctx) + info := s.createProposalInfoBytes( + maxLimit, + limit, + maxSize, + size, + map[string]uint64{ + defaultLane.Name(): 2, + }, + ) + + proposal = append([][]byte{info}, proposal...) + + _, partialProposals, err := proposalHandlers.ExtractLanes(proposal) + s.Require().NoError(err) + + s.Require().Equal(3, len(partialProposals)) + s.Require().Equal(0, len(partialProposals[0])) + s.Require().Equal(0, len(partialProposals[1])) + s.Require().Equal(2, len(partialProposals[2])) + s.Require().Equal(proposal[1], partialProposals[2][0]) + s.Require().Equal(proposal[2], partialProposals[2][1]) + }) + + s.Run("can validate a proposal with 1 tx from each lane", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + + tx3, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + 0, + 1, + ) + s.Require().NoError(err) + + proposal := s.getTxBytes(tx, tx2, tx3) + + size, limit := s.getTxInfos(tx, tx2, tx3) + maxSize, maxLimit := proposals.GetBlockLimits(s.ctx) + + info := s.createProposalInfoBytes( + maxLimit, + limit, + maxSize, + size, + map[string]uint64{ + defaultLane.Name(): 1, + mevlane.Name(): 1, + freelane.Name(): 1, + }, + ) + + proposal = append([][]byte{info}, proposal...) + + _, partialProposals, err := proposalHandlers.ExtractLanes(proposal) + s.Require().NoError(err) + + s.Require().Equal(3, len(partialProposals)) + s.Require().Equal(1, len(partialProposals[0])) + s.Require().Equal(proposal[1], partialProposals[0][0]) + + s.Require().Equal(1, len(partialProposals[1])) + s.Require().Equal(proposal[2], partialProposals[1][0]) + + s.Require().Equal(1, len(partialProposals[2])) + s.Require().Equal(proposal[3], partialProposals[2][0]) + }) } diff --git a/abci/utils.go b/abci/utils.go new file mode 100644 index 0000000..be85990 --- /dev/null +++ b/abci/utils.go @@ -0,0 +1,93 @@ +package abci + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/lanes/terminator" +) + +// ChainPrepareLanes chains together the proposal preparation logic from each lane into a +// single function. The first lane in the chain is the first lane to be prepared and the +// last lane in the chain is the last lane to be prepared.In the case where any of the lanes +// fail to prepare the partial proposal, the lane that failed will be skipped and the next +// lane in the chain will be called to prepare the proposal. +func ChainPrepareLanes(chain []block.Lane) block.PrepareLanesHandler { + if len(chain) == 0 { + return nil + } + + // Handle non-terminated decorators chain + if (chain[len(chain)-1] != terminator.Terminator{}) { + chain = append(chain, terminator.Terminator{}) + } + + return func(ctx sdk.Context, partialProposal proposals.Proposal) (finalProposal proposals.Proposal, err error) { + lane := chain[0] + lane.Logger().Info("preparing lane", "lane", lane.Name()) + + // Cache the context in the case where any of the lanes fail to prepare the proposal. + cacheCtx, write := ctx.CacheContext() + + // We utilize a recover to handle any panics or errors that occur during the preparation + // of a lane's transactions. This defer will first check if there was a panic or error + // thrown from the lane's preparation logic. If there was, we log the error, skip the lane, + // and call the next lane in the chain to the prepare the proposal. + defer func() { + if rec := recover(); rec != nil || err != nil { + lane.Logger().Error("failed to prepare lane", "lane", lane.Name(), "err", err, "recover_error", rec) + lane.Logger().Info("skipping lane", "lane", lane.Name()) + + if len(chain) <= 2 { + // If there are only two lanes remaining, then the first lane in the chain + // is the lane that failed to prepare the partial proposal and the second lane in the + // chain is the terminator lane. We return the proposal as is. + finalProposal, err = partialProposal, nil + } else { + // If there are more than two lanes remaining, then the first lane in the chain + // is the lane that failed to prepare the proposal but the second lane in the + // chain is not the terminator lane so there could potentially be more transactions + // added to the proposal + finalProposal, err = ChainPrepareLanes(chain[1:])(ctx, partialProposal) + } + } else { + // Write the cache to the context since we know that the lane successfully prepared + // the partial proposal. State is written to in a backwards, cascading fashion. This means + // that the final context will only be updated after all other lanes have successfully + // prepared the partial proposal. + write() + } + }() + + return lane.PrepareLane( + cacheCtx, + partialProposal, + ChainPrepareLanes(chain[1:]), + ) + } +} + +// ChainProcessLanes chains together the proposal verification logic from each lane +// into a single function. The first lane in the chain is the first lane to be verified and +// the last lane in the chain is the last lane to be verified. Each lane will validate +// the transactions that it selected in the prepare phase. +func ChainProcessLanes(partialProposals [][][]byte, chain []block.Lane) block.ProcessLanesHandler { + if len(chain) == 0 { + return nil + } + + // Handle non-terminated decorators chain + if (chain[len(chain)-1] != terminator.Terminator{}) { + chain = append(chain, terminator.Terminator{}) + partialProposals = append(partialProposals, nil) + } + + return func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) { + lane := chain[0] + partialProposal := partialProposals[0] + + lane.Logger().Info("processing lane", "lane", chain[0].Name()) + + return lane.ProcessLane(ctx, proposal, partialProposal, ChainProcessLanes(partialProposals[1:], chain[1:])) + } +} diff --git a/abci/utils_test.go b/abci/utils_test.go new file mode 100644 index 0000000..e8157d2 --- /dev/null +++ b/abci/utils_test.go @@ -0,0 +1,212 @@ +package abci_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + tmprototypes "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/skip-mev/block-sdk/abci" + signeradaptors "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" + "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/base" + "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/proposals/types" + "github.com/skip-mev/block-sdk/block/utils" + defaultlane "github.com/skip-mev/block-sdk/lanes/base" + "github.com/skip-mev/block-sdk/lanes/free" + "github.com/skip-mev/block-sdk/lanes/mev" +) + +func (s *ProposalsTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk.AnteHandler { + txCache := make(map[string]bool) + for tx, pass := range expectedExecution { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + txCache[hashStr] = pass + } + + anteHandler := func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + hash := sha256.Sum256(bz) + hashStr := hex.EncodeToString(hash[:]) + + pass, found := txCache[hashStr] + if !found { + return ctx, fmt.Errorf("tx not found") + } + + if pass { + return ctx, nil + } + + return ctx, fmt.Errorf("tx failed") + } + + return anteHandler +} + +func (s *ProposalsTestSuite) setUpStandardLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *defaultlane.DefaultLane { + cfg := base.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + SignerExtractor: signeradaptors.NewDefaultAdapter(), + } + + return defaultlane.NewDefaultLane(cfg) +} + +func (s *ProposalsTestSuite) setUpTOBLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *mev.MEVLane { + cfg := base.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + SignerExtractor: signeradaptors.NewDefaultAdapter(), + } + + return mev.NewMEVLane(cfg, mev.NewDefaultAuctionFactory(cfg.TxDecoder, signeradaptors.NewDefaultAdapter())) +} + +func (s *ProposalsTestSuite) setUpFreeLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *free.FreeLane { + cfg := base.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + AnteHandler: s.setUpAnteHandler(expectedExecution), + MaxBlockSpace: maxBlockSpace, + SignerExtractor: signeradaptors.NewDefaultAdapter(), + } + + return free.NewFreeLane(cfg, base.DefaultTxPriority(), free.DefaultMatchHandler()) +} + +func (s *ProposalsTestSuite) setUpPanicLane(maxBlockSpace math.LegacyDec) *base.BaseLane { + cfg := base.LaneConfig{ + Logger: log.NewTestLogger(s.T()), + TxEncoder: s.encodingConfig.TxConfig.TxEncoder(), + TxDecoder: s.encodingConfig.TxConfig.TxDecoder(), + MaxBlockSpace: maxBlockSpace, + SignerExtractor: signeradaptors.NewDefaultAdapter(), + } + + lane := base.NewBaseLane( + cfg, + "panic", + base.NewMempool[string](base.DefaultTxPriority(), cfg.TxEncoder, cfg.SignerExtractor, 0), + base.DefaultMatchHandler(), + ) + + lane.SetPrepareLaneHandler(base.PanicPrepareLaneHandler()) + lane.SetProcessLaneHandler(base.PanicProcessLaneHandler()) + + return lane +} + +func (s *ProposalsTestSuite) setUpProposalHandlers(lanes []block.Lane) *abci.ProposalHandler { + mempool := block.NewLanedMempool(log.NewTestLogger(s.T()), true, lanes...) + + return abci.NewProposalHandler( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxDecoder(), + s.encodingConfig.TxConfig.TxEncoder(), + mempool, + ) +} + +func (s *ProposalsTestSuite) createProposal(distribution map[string]uint64, txs ...sdk.Tx) [][]byte { + maxSize, maxGasLimit := proposals.GetBlockLimits(s.ctx) + size, limit := s.getTxInfos(txs...) + + info := s.createProposalInfoBytes( + maxGasLimit, + limit, + maxSize, + size, + distribution, + ) + + proposal := s.getTxBytes(txs...) + return append([][]byte{info}, proposal...) +} + +func (s *ProposalsTestSuite) getProposalInfo(bz []byte) types.ProposalInfo { + var info types.ProposalInfo + s.Require().NoError(info.Unmarshal(bz)) + return info +} + +func (s *ProposalsTestSuite) createProposalInfo( + maxGasLimit, gasLimit uint64, + maxBlockSize, blockSize int64, + txsByLane map[string]uint64, +) types.ProposalInfo { + return types.ProposalInfo{ + MaxGasLimit: maxGasLimit, + GasLimit: gasLimit, + MaxBlockSize: maxBlockSize, + BlockSize: blockSize, + TxsByLane: txsByLane, + } +} + +func (s *ProposalsTestSuite) createProposalInfoBytes( + maxGasLimit, gasLimit uint64, + maxBlockSize, blockSize int64, + txsByLane map[string]uint64, +) []byte { + info := s.createProposalInfo(maxGasLimit, gasLimit, maxBlockSize, blockSize, txsByLane) + bz, err := info.Marshal() + s.Require().NoError(err) + return bz +} + +func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte { + txBytes := make([][]byte, len(txs)) + for i, tx := range txs { + bz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + txBytes[i] = bz + } + return txBytes +} + +func (s *ProposalsTestSuite) getTxInfos(txs ...sdk.Tx) (int64, uint64) { + totalSize := int64(0) + totalGasLimit := uint64(0) + + for _, tx := range txs { + info, err := utils.GetTxInfo(s.encodingConfig.TxConfig.TxEncoder(), tx) + s.Require().NoError(err) + + totalSize += info.Size + totalGasLimit += info.GasLimit + } + + return totalSize, totalGasLimit +} + +func (s *ProposalsTestSuite) setBlockParams(maxGasLimit, maxBlockSize int64) { + s.ctx = s.ctx.WithConsensusParams( + tmprototypes.ConsensusParams{ + Block: &tmprototypes.BlockParams{ + MaxBytes: maxBlockSize, + MaxGas: maxGasLimit, + }, + }, + ) +} diff --git a/block/utils/ante.go b/block/ante.go similarity index 79% rename from block/utils/ante.go rename to block/ante.go index fe9e12f..009bc38 100644 --- a/block/utils/ante.go +++ b/block/ante.go @@ -1,16 +1,10 @@ -package utils +package block import ( sdk "github.com/cosmos/cosmos-sdk/types" ) type ( - // Lane defines the required API dependencies for the IgnoreDecorator. The ignore decorator - // will check if a transaction belongs to a lane by calling the Match function. - Lane interface { - Match(ctx sdk.Context, tx sdk.Tx) bool - } - // IgnoreDecorator is an AnteDecorator that wraps an existing AnteDecorator. It allows // for the AnteDecorator to be ignored for specified lanes. IgnoreDecorator struct { diff --git a/block/base/abci.go b/block/base/abci.go index e52c6bf..49ee227 100644 --- a/block/base/abci.go +++ b/block/base/abci.go @@ -4,20 +4,24 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/proposals" "github.com/skip-mev/block-sdk/block/utils" ) // PrepareLane will prepare a partial proposal for the lane. It will select transactions from the // lane respecting the selection logic of the prepareLaneHandler. It will then update the partial // proposal with the selected transactions. If the proposal is unable to be updated, we return an -// error. The proposal will only be modified if it passes all of the invarient checks. +// error. The proposal will only be modified if it passes all of the invariant checks. func (l *BaseLane) PrepareLane( ctx sdk.Context, - proposal block.BlockProposal, - maxTxBytes int64, + proposal proposals.Proposal, next block.PrepareLanesHandler, -) (block.BlockProposal, error) { - txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes) +) (proposals.Proposal, error) { + limit := proposal.GetLaneLimits(l.cfg.MaxBlockSpace) + + // Select transactions from the lane respecting the selection logic of the lane and the + // max block space for the lane. + txsToInclude, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, limit) if err != nil { return proposal, err } @@ -31,31 +35,75 @@ func (l *BaseLane) PrepareLane( ) } - // Update the proposal with the selected transactions. - if err := proposal.UpdateProposal(l, txs); err != nil { + // Update the proposal with the selected transactions. This fails if the lane attempted to add + // more transactions than the allocated max block space for the lane. + if err := proposal.UpdateProposal(l, txsToInclude); err != nil { + l.Logger().Error( + "failed to update proposal", + "lane", l.Name(), + "err", err, + "num_txs_to_add", len(txsToInclude), + "num_txs_to_remove", len(txsToRemove), + ) + return proposal, err } + l.Logger().Info( + "lane prepared", + "lane", l.Name(), + "num_txs_added", len(txsToInclude), + "num_txs_removed", len(txsToRemove), + ) + return next(ctx, proposal) } -// CheckOrder checks that the ordering logic of the lane is respected given the set of transactions -// in the block proposal. If the ordering logic is not respected, we return an error. -func (l *BaseLane) CheckOrder(ctx sdk.Context, txs []sdk.Tx) error { - return l.checkOrderHandler(ctx, txs) -} - // ProcessLane verifies that the transactions included in the block proposal are valid respecting -// the verification logic of the lane (processLaneHandler). If the transactions are valid, we -// return the transactions that do not belong to this lane to the next lane. If the transactions -// are invalid, we return an error. -func (l *BaseLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next block.ProcessLanesHandler) (sdk.Context, error) { - remainingTxs, err := l.processLaneHandler(ctx, txs) +// the verification logic of the lane (processLaneHandler). If any of the transactions are invalid, +// we return an error. If all of the transactions are valid, we return the updated proposal. +func (l *BaseLane) ProcessLane( + ctx sdk.Context, + proposal proposals.Proposal, + txs [][]byte, + next block.ProcessLanesHandler, +) (proposals.Proposal, error) { + // Assume that this lane is processing sdk.Tx's and decode the transactions. + decodedTxs, err := utils.GetDecodedTxs(l.TxDecoder(), txs) if err != nil { - return ctx, err + l.Logger().Error( + "failed to decode transactions", + "lane", l.Name(), + "err", err, + ) + + return proposal, err } - return next(ctx, remainingTxs) + // Verify the transactions that belong to this lane according to the verification logic of the lane. + if err := l.processLaneHandler(ctx, decodedTxs); err != nil { + return proposal, err + } + + // Optimistically update the proposal with the partial proposal. + if err := proposal.UpdateProposal(l, decodedTxs); err != nil { + l.Logger().Error( + "failed to update proposal", + "lane", l.Name(), + "err", err, + "num_txs_to_verify", len(decodedTxs), + ) + + return proposal, err + } + + l.Logger().Info( + "lane processed", + "lane", l.Name(), + "num_txs_verified", len(decodedTxs), + ) + + return next(ctx, proposal) } // AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of diff --git a/block/base/handlers.go b/block/base/handlers.go index 6cc8e92..aa00829 100644 --- a/block/base/handlers.go +++ b/block/base/handlers.go @@ -5,20 +5,21 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/proposals" "github.com/skip-mev/block-sdk/block/utils" ) // DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It // selects all transactions in the mempool that are valid and not already in the partial -// proposal. It will continue to reap transactions until the maximum block space for this +// proposal. It will continue to reap transactions until the maximum blockspace/gas for this // lane has been reached. Additionally, any transactions that are invalid will be returned. func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { - return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + return func(ctx sdk.Context, proposal proposals.Proposal, limit proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) { var ( - totalSize int64 - txs [][]byte - txsToRemove []sdk.Tx + totalSize int64 + totalGas uint64 + txsToInclude []sdk.Tx + txsToRemove []sdk.Tx ) // Select all transactions in the mempool that are valid and not already in the @@ -26,7 +27,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { tx := iterator.Tx() - txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx) + txInfo, err := utils.GetTxInfo(l.TxEncoder(), tx) if err != nil { l.Logger().Info("failed to get hash of tx", "err", err) @@ -38,7 +39,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { if !l.Match(ctx, tx) { l.Logger().Info( "failed to select tx for lane; tx does not belong to lane", - "tx_hash", hash, + "tx_hash", txInfo.Hash, "lane", l.Name(), ) @@ -47,10 +48,10 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { } // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(txBytes) { + if proposal.Contains(txInfo.Hash) { l.Logger().Info( "failed to select tx for lane; tx is already in proposal", - "tx_hash", hash, + "tx_hash", txInfo.Hash, "lane", l.Name(), ) @@ -58,25 +59,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { } // If the transaction is too large, we break and do not attempt to include more txs. - txSize := int64(len(txBytes)) - if updatedSize := totalSize + txSize; updatedSize > maxTxBytes { + if updatedSize := totalSize + txInfo.Size; updatedSize > limit.MaxTxBytes { l.Logger().Info( - "tx bytes above the maximum allowed", + "failed to select tx for lane; tx bytes above the maximum allowed", "lane", l.Name(), - "tx_size", txSize, + "tx_size", txInfo.Size, "total_size", totalSize, - "max_tx_bytes", maxTxBytes, - "tx_hash", hash, + "max_tx_bytes", limit.MaxTxBytes, + "tx_hash", txInfo.Hash, ) - break + // TODO: Determine if there is any trade off with breaking or continuing here. + continue + } + + // If the gas limit of the transaction is too large, we break and do not attempt to include more txs. + if updatedGas := totalGas + txInfo.GasLimit; updatedGas > limit.MaxGasLimit { + l.Logger().Info( + "failed to select tx for lane; gas limit above the maximum allowed", + "lane", l.Name(), + "tx_gas", txInfo.GasLimit, + "total_gas", totalGas, + "max_gas", limit.MaxGasLimit, + "tx_hash", txInfo.Hash, + ) + + // TODO: Determine if there is any trade off with breaking or continuing here. + continue } // Verify the transaction. if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil { l.Logger().Info( "failed to verify tx", - "tx_hash", hash, + "tx_hash", txInfo.Hash, "err", err, ) @@ -84,66 +100,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { continue } - totalSize += txSize - txs = append(txs, txBytes) + totalSize += txInfo.Size + totalGas += txInfo.GasLimit + txsToInclude = append(txsToInclude, tx) } - return txs, txsToRemove, nil + return txsToInclude, txsToRemove, nil } } -// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It -// verifies all transactions in the lane that matches to the lane. If any transaction -// fails to verify, the entire proposal is rejected. If the handler comes across a transaction -// that does not match the lane's matcher, it will return the remaining transactions in the -// proposal. +// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It verifies +// the following invariants: +// 1. All transactions belong to this lane. +// 2. All transactions respect the priority defined by the mempool. +// 3. All transactions are valid respecting the verification logic of the lane. func (l *BaseLane) DefaultProcessLaneHandler() ProcessLaneHandler { - return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { - var err error - + return func(ctx sdk.Context, partialProposal []sdk.Tx) error { // Process all transactions that match the lane's matcher. - for index, tx := range txs { - if l.Match(ctx, tx) { - if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil { - return nil, fmt.Errorf("failed to verify tx: %w", err) - } - } else { - return txs[index:], nil - } - } - - // This means we have processed all transactions in the proposal. - return nil, nil - } -} - -// DefaultCheckOrderHandler returns a default implementation of the CheckOrderHandler. It -// ensures the following invariants: -// -// 1. All transactions that belong to this lane respect the ordering logic defined by the -// lane. -// 2. Transactions that belong to other lanes cannot be interleaved with transactions that -// belong to this lane. -func (l *BaseLane) DefaultCheckOrderHandler() CheckOrderHandler { - return func(ctx sdk.Context, txs []sdk.Tx) error { - seenOtherLaneTx := false - - for index, tx := range txs { - if l.Match(ctx, tx) { - if seenOtherLaneTx { - return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name()) - } - - // If the transactions do not respect the priority defined by the mempool, we consider the proposal - // to be invalid - if index > 0 && l.Compare(ctx, txs[index-1], tx) == -1 { - return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1) - } - } else { - seenOtherLaneTx = true + for index, tx := range partialProposal { + if !l.Match(ctx, tx) { + return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name()) + } + + // If the transactions do not respect the priority defined by the mempool, we consider the proposal + // to be invalid + if index > 0 && l.Compare(ctx, partialProposal[index-1], tx) == -1 { + return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1) + } + + if _, err := l.AnteVerifyTx(ctx, tx, false); err != nil { + return fmt.Errorf("failed to verify tx: %w", err) } } + // This means we have processed all transactions in the partial proposal. return nil } } diff --git a/block/base/lane.go b/block/base/lane.go index 693695c..ae0a06f 100644 --- a/block/base/lane.go +++ b/block/base/lane.go @@ -38,11 +38,6 @@ type BaseLane struct { //nolint // requested and the lane needs to submit transactions it wants included in the block. prepareLaneHandler PrepareLaneHandler - // checkOrderHandler is the function that is called when a new proposal is being - // verified and the lane needs to verify that the transactions included in the proposal - // respect the ordering rules of the lane and does not interleave transactions from other lanes. - checkOrderHandler CheckOrderHandler - // processLaneHandler is the function that is called when a new proposal is being // verified and the lane needs to verify that the transactions included in the proposal // are valid respecting the verification logic of the lane. @@ -95,10 +90,6 @@ func (l *BaseLane) ValidateBasic() error { l.processLaneHandler = l.DefaultProcessLaneHandler() } - if l.checkOrderHandler == nil { - l.checkOrderHandler = l.DefaultCheckOrderHandler() - } - return nil } @@ -125,18 +116,6 @@ func (l *BaseLane) SetProcessLaneHandler(processLaneHandler ProcessLaneHandler) l.processLaneHandler = processLaneHandler } -// SetCheckOrderHandler sets the check order handler for the lane. This handler -// is called when a new proposal is being verified and the lane needs to verify -// that the transactions included in the proposal respect the ordering rules of -// the lane and does not include transactions from other lanes. -func (l *BaseLane) SetCheckOrderHandler(checkOrderHandler CheckOrderHandler) { - if checkOrderHandler == nil { - panic("check order handler cannot be nil") - } - - l.checkOrderHandler = checkOrderHandler -} - // Match returns true if the transaction should be processed by this lane. This // function first determines if the transaction matches the lane and then checks // if the transaction is on the ignore list. If the transaction is on the ignore diff --git a/block/base/mempool.go b/block/base/mempool.go index 2c39ac6..5e1f30b 100644 --- a/block/base/mempool.go +++ b/block/base/mempool.go @@ -103,13 +103,13 @@ func (cm *Mempool[C]) Insert(ctx context.Context, tx sdk.Tx) error { return fmt.Errorf("failed to insert tx into auction index: %w", err) } - _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) if err != nil { cm.Remove(tx) return err } - cm.txCache[txHashStr] = struct{}{} + cm.txCache[txInfo.Hash] = struct{}{} return nil } @@ -120,12 +120,12 @@ func (cm *Mempool[C]) Remove(tx sdk.Tx) error { return fmt.Errorf("failed to remove transaction from the mempool: %w", err) } - _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) if err != nil { return fmt.Errorf("failed to get tx hash string: %w", err) } - delete(cm.txCache, txHashStr) + delete(cm.txCache, txInfo.Hash) return nil } @@ -145,12 +145,12 @@ func (cm *Mempool[C]) CountTx() int { // Contains returns true if the transaction is contained in the mempool. func (cm *Mempool[C]) Contains(tx sdk.Tx) bool { - _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) if err != nil { return false } - _, ok := cm.txCache[txHashStr] + _, ok := cm.txCache[txInfo.Hash] return ok } diff --git a/block/base/types.go b/block/base/types.go index 50a49de..c90d835 100644 --- a/block/base/types.go +++ b/block/base/types.go @@ -3,7 +3,7 @@ package base import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/proposals" ) type ( @@ -16,28 +16,20 @@ type ( // the transactions that must be removed from the lane, and an error if one occurred. PrepareLaneHandler func( ctx sdk.Context, - proposal block.BlockProposal, - maxTxBytes int64, - ) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) + proposal proposals.Proposal, + limit proposals.LaneLimits, + ) (txsToInclude []sdk.Tx, txsToRemove []sdk.Tx, err error) // ProcessLaneHandler is responsible for processing transactions that are included in a block and - // belong to a given lane. ProcessLaneHandler is executed after CheckOrderHandler so the transactions - // passed into this function SHOULD already be in order respecting the ordering rules of the lane and - // respecting the ordering rules of mempool relative to the lanes it has. - ProcessLaneHandler func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) - - // CheckOrderHandler is responsible for checking the order of transactions that belong to a given - // lane. This handler should be used to verify that the ordering of transactions passed into the - // function respect the ordering logic of the lane (if any transactions from the lane are included). - // This function should also ensure that transactions that belong to this lane are contiguous and do - // not have any transactions from other lanes in between them. - CheckOrderHandler func(ctx sdk.Context, txs []sdk.Tx) error + // belong to a given lane. This handler must return an error if the transactions are not correctly + // ordered, do not belong to this lane, or any other relevant error. + ProcessLaneHandler func(ctx sdk.Context, partialProposal []sdk.Tx) error ) // NoOpPrepareLaneHandler returns a no-op prepare lane handler. // This should only be used for testing. func NoOpPrepareLaneHandler() PrepareLaneHandler { - return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) { + return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) { return nil, nil, nil } } @@ -45,7 +37,7 @@ func NoOpPrepareLaneHandler() PrepareLaneHandler { // PanicPrepareLaneHandler returns a prepare lane handler that panics. // This should only be used for testing. func PanicPrepareLaneHandler() PrepareLaneHandler { - return func(sdk.Context, block.BlockProposal, int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) { + return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) { panic("panic prepare lanes handler") } } @@ -53,15 +45,15 @@ func PanicPrepareLaneHandler() PrepareLaneHandler { // NoOpProcessLaneHandler returns a no-op process lane handler. // This should only be used for testing. func NoOpProcessLaneHandler() ProcessLaneHandler { - return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { - return txs, nil + return func(sdk.Context, []sdk.Tx) error { + return nil } } // PanicProcessLanesHandler returns a process lanes handler that panics. // This should only be used for testing. func PanicProcessLaneHandler() ProcessLaneHandler { - return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, error) { + return func(sdk.Context, []sdk.Tx) error { panic("panic process lanes handler") } } diff --git a/block/lane.go b/block/lane.go index 0731948..b4dcd1f 100644 --- a/block/lane.go +++ b/block/lane.go @@ -5,13 +5,14 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + "github.com/skip-mev/block-sdk/block/proposals" ) // LaneMempool defines the interface a lane's mempool should implement. The basic API // is the same as the sdk.Mempool, but it also includes a Compare function that is used // to determine the relative priority of two transactions belonging in the same lane. // -//go:generate mockery --name LaneMempool --output ./utils/mocks --outpkg mocks --case underscore +//go:generate mockery --name LaneMempool --output ./mocks --outpkg mocks --case underscore type LaneMempool interface { sdkmempool.Mempool @@ -27,29 +28,31 @@ type LaneMempool interface { // Lane defines an interface used for matching transactions to lanes, storing transactions, // and constructing partial blocks. // -//go:generate mockery --name Lane --output ./utils/mocks --outpkg mocks --case underscore +//go:generate mockery --name Lane --output ./mocks --outpkg mocks --case underscore type Lane interface { LaneMempool - // PrepareLane builds a portion of the block. It inputs the maxTxBytes that can be - // included in the proposal for the given lane, the partial proposal, and a function - // to call the next lane in the chain. The next lane in the chain will be called with - // the updated proposal and context. + // PrepareLane builds a portion of the block. It inputs the current context, proposal, and a + // function to call the next lane in the chain. This handler should update the context as needed + // and add transactions to the proposal. Note, the lane should only add transactions up to the + // max block space for the lane. PrepareLane( ctx sdk.Context, - proposal BlockProposal, - maxTxBytes int64, + proposal proposals.Proposal, next PrepareLanesHandler, - ) (BlockProposal, error) + ) (proposals.Proposal, error) - // CheckOrder validates that transactions belonging to this lane are not misplaced - // in the block proposal and respect the ordering rules of the lane. - CheckOrder(ctx sdk.Context, txs []sdk.Tx) error - - // ProcessLane verifies this lane's portion of a proposed block. It inputs the transactions - // that may belong to this lane and a function to call the next lane in the chain. The next - // lane in the chain will be called with the updated context and filtered down transactions. - ProcessLane(ctx sdk.Context, proposalTxs []sdk.Tx, next ProcessLanesHandler) (sdk.Context, error) + // ProcessLane verifies this lane's portion of a proposed block. It inputs the current context, + // proposal, transactions that belong to this lane, and a function to call the next lane in the + // chain. This handler should update the context as needed and add transactions to the proposal. + // The entire process lane chain should end up constructing the same proposal as the prepare lane + // chain. + ProcessLane( + ctx sdk.Context, + proposal proposals.Proposal, + partialProposal [][]byte, + next ProcessLanesHandler, + ) (proposals.Proposal, error) // GetMaxBlockSpace returns the max block space for the lane as a relative percentage. GetMaxBlockSpace() math.LegacyDec diff --git a/block/utils/mocks/lane.go b/block/mocks/lane.go similarity index 70% rename from block/utils/mocks/lane.go rename to block/mocks/lane.go index bf08240..9fc39e2 100644 --- a/block/utils/mocks/lane.go +++ b/block/mocks/lane.go @@ -15,6 +15,8 @@ import ( mock "github.com/stretchr/testify/mock" + proposals "github.com/skip-mev/block-sdk/block/proposals" + types "github.com/cosmos/cosmos-sdk/types" ) @@ -23,20 +25,6 @@ type Lane struct { mock.Mock } -// CheckOrder provides a mock function with given fields: ctx, txs -func (_m *Lane) CheckOrder(ctx types.Context, txs []types.Tx) error { - ret := _m.Called(ctx, txs) - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, []types.Tx) error); ok { - r0 = rf(ctx, txs) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Compare provides a mock function with given fields: ctx, this, other func (_m *Lane) Compare(ctx types.Context, this types.Tx, other types.Tx) int { ret := _m.Called(ctx, this, other) @@ -151,25 +139,23 @@ func (_m *Lane) Name() string { return r0 } -// PrepareLane provides a mock function with given fields: ctx, proposal, maxTxBytes, next -func (_m *Lane) PrepareLane(ctx types.Context, proposal block.BlockProposal, maxTxBytes int64, next block.PrepareLanesHandler) (block.BlockProposal, error) { - ret := _m.Called(ctx, proposal, maxTxBytes, next) +// PrepareLane provides a mock function with given fields: ctx, proposal, next +func (_m *Lane) PrepareLane(ctx types.Context, proposal proposals.Proposal, next block.PrepareLanesHandler) (proposals.Proposal, error) { + ret := _m.Called(ctx, proposal, next) - var r0 block.BlockProposal + var r0 proposals.Proposal var r1 error - if rf, ok := ret.Get(0).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) (block.BlockProposal, error)); ok { - return rf(ctx, proposal, maxTxBytes, next) + if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) (proposals.Proposal, error)); ok { + return rf(ctx, proposal, next) } - if rf, ok := ret.Get(0).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) block.BlockProposal); ok { - r0 = rf(ctx, proposal, maxTxBytes, next) + if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) proposals.Proposal); ok { + r0 = rf(ctx, proposal, next) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(block.BlockProposal) - } + r0 = ret.Get(0).(proposals.Proposal) } - if rf, ok := ret.Get(1).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) error); ok { - r1 = rf(ctx, proposal, maxTxBytes, next) + if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) error); ok { + r1 = rf(ctx, proposal, next) } else { r1 = ret.Error(1) } @@ -177,23 +163,23 @@ func (_m *Lane) PrepareLane(ctx types.Context, proposal block.BlockProposal, max return r0, r1 } -// ProcessLane provides a mock function with given fields: ctx, proposalTxs, next -func (_m *Lane) ProcessLane(ctx types.Context, proposalTxs []types.Tx, next block.ProcessLanesHandler) (types.Context, error) { - ret := _m.Called(ctx, proposalTxs, next) +// ProcessLane provides a mock function with given fields: ctx, proposal, partialProposal, next +func (_m *Lane) ProcessLane(ctx types.Context, proposal proposals.Proposal, partialProposal [][]byte, next block.ProcessLanesHandler) (proposals.Proposal, error) { + ret := _m.Called(ctx, proposal, partialProposal, next) - var r0 types.Context + var r0 proposals.Proposal var r1 error - if rf, ok := ret.Get(0).(func(types.Context, []types.Tx, block.ProcessLanesHandler) (types.Context, error)); ok { - return rf(ctx, proposalTxs, next) + if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) (proposals.Proposal, error)); ok { + return rf(ctx, proposal, partialProposal, next) } - if rf, ok := ret.Get(0).(func(types.Context, []types.Tx, block.ProcessLanesHandler) types.Context); ok { - r0 = rf(ctx, proposalTxs, next) + if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) proposals.Proposal); ok { + r0 = rf(ctx, proposal, partialProposal, next) } else { - r0 = ret.Get(0).(types.Context) + r0 = ret.Get(0).(proposals.Proposal) } - if rf, ok := ret.Get(1).(func(types.Context, []types.Tx, block.ProcessLanesHandler) error); ok { - r1 = rf(ctx, proposalTxs, next) + if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) error); ok { + r1 = rf(ctx, proposal, partialProposal, next) } else { r1 = ret.Error(1) } @@ -246,8 +232,7 @@ func (_m *Lane) SetIgnoreList(ignoreList []block.Lane) { func NewLane(t interface { mock.TestingT Cleanup(func()) -}, -) *Lane { +}) *Lane { mock := &Lane{} mock.Mock.Test(t) diff --git a/block/utils/mocks/lane_mempool.go b/block/mocks/lane_mempool.go similarity index 99% rename from block/utils/mocks/lane_mempool.go rename to block/mocks/lane_mempool.go index d3892d3..d4116e0 100644 --- a/block/utils/mocks/lane_mempool.go +++ b/block/mocks/lane_mempool.go @@ -107,8 +107,7 @@ func (_m *LaneMempool) Select(_a0 context.Context, _a1 [][]byte) mempool.Iterato func NewLaneMempool(t interface { mock.TestingT Cleanup(func()) -}, -) *LaneMempool { +}) *LaneMempool { mock := &LaneMempool{} mock.Mock.Test(t) diff --git a/block/proposals.go b/block/proposals.go deleted file mode 100644 index 0234da9..0000000 --- a/block/proposals.go +++ /dev/null @@ -1,205 +0,0 @@ -package block - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - - "cosmossdk.io/log" - "cosmossdk.io/math" - - "github.com/skip-mev/block-sdk/block/utils" -) - -var _ BlockProposal = (*Proposal)(nil) - -type ( - // LaneProposal defines the interface/APIs that are required for the proposal to interact - // with a lane. - LaneProposal interface { - // Logger returns the lane's logger. - Logger() log.Logger - - // GetMaxBlockSpace returns the maximum block space for the lane as a relative percentage. - GetMaxBlockSpace() math.LegacyDec - - // Name returns the name of the lane. - Name() string - } - - // BlockProposal is the interface/APIs that are required for proposal creation + interacting with - // and updating proposals. BlockProposals are iteratively updated as each lane prepares its - // partial proposal. Each lane must call UpdateProposal with its partial proposal in PrepareLane. BlockProposals - // can also include vote extensions, which are included at the top of the proposal. - BlockProposal interface { //nolint - // UpdateProposal updates the proposal with the given transactions. There are a - // few invarients that are checked: - // 1. The total size of the proposal must be less than the maximum number of bytes allowed. - // 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for - // the lane. - UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte) error - - // GetMaxTxBytes returns the maximum number of bytes that can be included in the proposal. - GetMaxTxBytes() int64 - - // GetTotalTxBytes returns the total number of bytes currently included in the proposal. - GetTotalTxBytes() int64 - - // GetTxs returns the transactions in the proposal. - GetTxs() [][]byte - - // GetNumTxs returns the number of transactions in the proposal. - GetNumTxs() int - - // Contains returns true if the proposal contains the given transaction. - Contains(tx []byte) bool - - // AddVoteExtension adds a vote extension to the proposal. - AddVoteExtension(voteExtension []byte) - - // GetVoteExtensions returns the vote extensions in the proposal. - GetVoteExtensions() [][]byte - - // GetProposal returns all of the transactions in the proposal along with the vote extensions - // at the top of the proposal. - GetProposal() [][]byte - } - - // Proposal defines a block proposal type. - Proposal struct { - // txs is the list of transactions in the proposal. - txs [][]byte - - // voteExtensions is the list of vote extensions in the proposal. - voteExtensions [][]byte - - // cache is a cache of the selected transactions in the proposal. - cache map[string]struct{} - - // totalTxBytes is the total number of bytes currently included in the proposal. - totalTxBytes int64 - - // maxTxBytes is the maximum number of bytes that can be included in the proposal. - maxTxBytes int64 - } -) - -// NewProposal returns a new empty proposal. -func NewProposal(maxTxBytes int64) *Proposal { - return &Proposal{ - txs: make([][]byte, 0), - voteExtensions: make([][]byte, 0), - cache: make(map[string]struct{}), - maxTxBytes: maxTxBytes, - } -} - -// UpdateProposal updates the proposal with the given transactions and total size. There are a -// few invarients that are checked: -// 1. The total size of the proposal must be less than the maximum number of bytes allowed. -// 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for -// the lane. -func (p *Proposal) UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte) error { - if len(partialProposalTxs) == 0 { - return nil - } - - partialProposalSize := int64(0) - for _, tx := range partialProposalTxs { - partialProposalSize += int64(len(tx)) - } - - // Invarient check: Ensure that the lane did not prepare a partial proposal that is too large. - maxTxBytesForLane := utils.GetMaxTxBytesForLane(p.GetMaxTxBytes(), p.GetTotalTxBytes(), lane.GetMaxBlockSpace()) - if partialProposalSize > maxTxBytesForLane { - return fmt.Errorf( - "%s lane prepared a partial proposal that is too large: %d > %d", - lane.Name(), - partialProposalSize, - maxTxBytesForLane, - ) - } - - // Invarient check: Ensure that the lane did not prepare a block proposal that is too large. - updatedSize := p.totalTxBytes + partialProposalSize - if updatedSize > p.maxTxBytes { - return fmt.Errorf( - "lane %s prepared a block proposal that is too large: %d > %d", - lane.Name(), - p.totalTxBytes, - p.maxTxBytes, - ) - } - p.totalTxBytes = updatedSize - - p.txs = append(p.txs, partialProposalTxs...) - - for _, tx := range partialProposalTxs { - txHash := sha256.Sum256(tx) - txHashStr := hex.EncodeToString(txHash[:]) - - p.cache[txHashStr] = struct{}{} - - lane.Logger().Info( - "adding transaction to proposal", - "lane", lane.Name(), - "tx_hash", txHashStr, - "tx_bytes", len(tx), - ) - } - - lane.Logger().Info( - "lane successfully updated proposal", - "lane", lane.Name(), - "num_txs", len(partialProposalTxs), - "partial_proposal_size", partialProposalSize, - "cumulative_proposal_size", updatedSize, - ) - - return nil -} - -// GetProposal returns all of the transactions in the proposal along with the vote extensions -// at the top of the proposal. -func (p *Proposal) GetProposal() [][]byte { - return append(p.voteExtensions, p.txs...) -} - -// AddVoteExtension adds a vote extension to the proposal. -func (p *Proposal) AddVoteExtension(voteExtension []byte) { - p.voteExtensions = append(p.voteExtensions, voteExtension) -} - -// GetVoteExtensions returns the vote extensions in the proposal. -func (p *Proposal) GetVoteExtensions() [][]byte { - return p.voteExtensions -} - -// GetMaxTxBytes returns the maximum number of bytes that can be included in the proposal. -func (p *Proposal) GetMaxTxBytes() int64 { - return p.maxTxBytes -} - -// GetTotalTxBytes returns the total number of bytes currently included in the proposal. -func (p *Proposal) GetTotalTxBytes() int64 { - return p.totalTxBytes -} - -// GetTxs returns the transactions in the proposal. -func (p *Proposal) GetTxs() [][]byte { - return p.txs -} - -// GetNumTxs returns the number of transactions in the proposal. -func (p *Proposal) GetNumTxs() int { - return len(p.txs) -} - -// Contains returns true if the proposal contains the given transaction. -func (p *Proposal) Contains(tx []byte) bool { - txHash := sha256.Sum256(tx) - txHashStr := hex.EncodeToString(txHash[:]) - - _, ok := p.cache[txHashStr] - return ok -} diff --git a/block/proposals/proposals.go b/block/proposals/proposals.go new file mode 100644 index 0000000..4b3f5bf --- /dev/null +++ b/block/proposals/proposals.go @@ -0,0 +1,94 @@ +package proposals + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/proposals/types" +) + +type ( + // Proposal defines a block proposal type. + Proposal struct { + // Txs is the list of transactions in the proposal. + Txs [][]byte + // Cache is a cache of the selected transactions in the proposal. + Cache map[string]struct{} + // TxEncoder is the transaction encoder. + TxEncoder sdk.TxEncoder + // Info contains information about the state of the proposal. + Info types.ProposalInfo + } +) + +// NewProposal returns a new empty proposal. Any transactions added to the proposal +// will be subject to the given max block size and max gas limit. +func NewProposal(txEncoder sdk.TxEncoder, maxBlockSize int64, maxGasLimit uint64) Proposal { + return Proposal{ + TxEncoder: txEncoder, + Txs: make([][]byte, 0), + Cache: make(map[string]struct{}), + Info: types.ProposalInfo{ + TxsByLane: make(map[string]uint64), + MaxBlockSize: maxBlockSize, + MaxGasLimit: maxGasLimit, + }, + } +} + +// GetProposalWithInfo returns all of the transactions in the proposal along with information +// about the lanes that built the proposal. +func (p *Proposal) GetProposalWithInfo() ([][]byte, error) { + // Marshall the proposal info into the first slot of the proposal. + infoBz, err := p.Info.Marshal() + if err != nil { + return nil, err + } + + proposal := [][]byte{infoBz} + proposal = append(proposal, p.Txs...) + + return proposal, nil +} + +// GetLaneLimits returns the maximum number of bytes and gas limit that can be +// included/consumed in the proposal for the given block space ratio. Lane's +// must first call this function to determine the maximum number of bytes and +// gas limit they can include in the proposal before constructing a partial +// proposal. +func (p *Proposal) GetLaneLimits(ratio math.LegacyDec) LaneLimits { + var ( + txBytes int64 + gasLimit uint64 + ) + + // In the case where the ratio is zero, we return the max tx bytes remaining. + // Note, the only lane that should have a ratio of zero is the default lane. + if ratio.IsZero() { + txBytes = p.Info.MaxBlockSize - p.Info.BlockSize + if txBytes < 0 { + txBytes = 0 + } + + // Unsigned subtraction needs an additional check + if p.Info.GasLimit >= p.Info.MaxGasLimit { + gasLimit = 0 + } else { + gasLimit = p.Info.MaxGasLimit - p.Info.GasLimit + } + } else { + // Otherwise, we calculate the max tx bytes / gas limit for the lane based on the ratio. + txBytes = ratio.MulInt64(p.Info.MaxBlockSize).TruncateInt().Int64() + gasLimit = ratio.MulInt(math.NewIntFromUint64(p.Info.MaxGasLimit)).TruncateInt().Uint64() + } + + return LaneLimits{ + MaxTxBytes: txBytes, + MaxGasLimit: gasLimit, + } +} + +// Contains returns true if the proposal contains the given transaction. +func (p *Proposal) Contains(txHash string) bool { + _, ok := p.Cache[txHash] + return ok +} diff --git a/block/proposals/proposals_test.go b/block/proposals/proposals_test.go new file mode 100644 index 0000000..92c2331 --- /dev/null +++ b/block/proposals/proposals_test.go @@ -0,0 +1,540 @@ +package proposals_test + +import ( + "math/rand" + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/mocks" + "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/proposals/types" + "github.com/skip-mev/block-sdk/block/utils" + "github.com/skip-mev/block-sdk/testutils" + "github.com/stretchr/testify/require" +) + +func TestUpdateProposal(t *testing.T) { + encodingConfig := testutils.CreateTestEncodingConfig() + + // Create a few random accounts + random := rand.New(rand.NewSource(1)) + accounts := testutils.RandomAccounts(random, 5) + + lane := mocks.NewLane(t) + + lane.On("Name").Return("test").Maybe() + lane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe() + + t.Run("can update with no transactions", func(t *testing.T) { + proposal := proposals.NewProposal(nil, 100, 100) + + err := proposal.UpdateProposal(lane, nil) + require.NoError(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 0, len(proposal.Txs)) + require.Equal(t, int64(0), proposal.Info.BlockSize) + require.Equal(t, uint64(0), proposal.Info.GasLimit) + require.Equal(t, 0, len(proposal.Info.TxsByLane)) + + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 1, len(block)) + }) + + t.Run("can update with a single transaction", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := len(txBzs[0]) + gasLimit := 100 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.NoError(t, err) + + // Ensure that the proposal is not empty. + require.Equal(t, 1, len(proposal.Txs)) + require.Equal(t, int64(size), proposal.Info.BlockSize) + require.Equal(t, uint64(gasLimit), proposal.Info.GasLimit) + require.Equal(t, 1, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"]) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 2, len(block)) + require.Equal(t, txBzs[0], block[1]) + }) + + t.Run("can update with multiple transactions", func(t *testing.T) { + txs := make([]sdk.Tx, 0) + + for i := 0; i < 10; i++ { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + uint64(i), + 0, + 100, + ) + require.NoError(t, err) + + txs = append(txs, tx) + } + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), txs) + require.NoError(t, err) + + size := 0 + gasLimit := uint64(0) + for _, txBz := range txBzs { + size += len(txBz) + gasLimit += 100 + } + + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), gasLimit) + + err = proposal.UpdateProposal(lane, txs) + require.NoError(t, err) + + // Ensure that the proposal is not empty. + require.Equal(t, len(txs), len(proposal.Txs)) + require.Equal(t, int64(size), proposal.Info.BlockSize) + require.Equal(t, gasLimit, proposal.Info.GasLimit) + require.Equal(t, uint64(10), proposal.Info.TxsByLane["test"]) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 11, len(block)) + + for i := 0; i < 10; i++ { + require.Equal(t, txBzs[i], block[i+1]) + } + }) + + t.Run("rejects an update with duplicate transactions", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := int64(len(txBzs[0])) + gasLimit := uint64(100) + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), size, gasLimit) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.NoError(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 1, len(proposal.Txs)) + require.Equal(t, size, proposal.Info.BlockSize) + require.Equal(t, gasLimit, proposal.Info.GasLimit) + require.Equal(t, 1, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"]) + + otherlane := mocks.NewLane(t) + + otherlane.On("Name").Return("test").Maybe() + otherlane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe() + + // Attempt to add the same transaction again. + err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx}) + require.Error(t, err) + + require.Equal(t, 1, len(proposal.Txs)) + require.Equal(t, size, proposal.Info.BlockSize) + require.Equal(t, gasLimit, proposal.Info.GasLimit) + require.Equal(t, 1, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"]) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 2, len(block)) + require.Equal(t, txBzs[0], block[1]) + }) + + t.Run("rejects an update with duplicate lane updates", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + tx2, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[1], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx, tx2}) + require.NoError(t, err) + + size := len(txBzs[0]) + len(txBzs[1]) + gasLimit := 200 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx2}) + require.Error(t, err) + + // Ensure that the proposal is not empty. + require.Equal(t, 1, len(proposal.Txs)) + require.Equal(t, int64(len(txBzs[0])), proposal.Info.BlockSize) + require.Equal(t, uint64(100), proposal.Info.GasLimit) + require.Equal(t, 1, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"]) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 2, len(block)) + require.Equal(t, txBzs[0], block[1]) + }) + + t.Run("rejects an update where lane limit is smaller (block size)", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := len(txBzs[0]) + gasLimit := 100 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + + lane := mocks.NewLane(t) + + lane.On("Name").Return("test").Maybe() + lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.Error(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 0, len(proposal.Txs)) + require.Equal(t, int64(0), proposal.Info.BlockSize) + require.Equal(t, uint64(0), proposal.Info.GasLimit) + require.Equal(t, 0, len(proposal.Info.TxsByLane)) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 1, len(block)) + }) + + t.Run("rejects an update where the lane limit is smaller (gas limit)", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := len(txBzs[0]) + gasLimit := 100 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + + lane := mocks.NewLane(t) + + lane.On("Name").Return("test").Maybe() + lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.Error(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 0, len(proposal.Txs)) + require.Equal(t, int64(0), proposal.Info.BlockSize) + require.Equal(t, 0, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(0), proposal.Info.GasLimit) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 1, len(block)) + }) + + t.Run("rejects an update where the proposal exceeds max block size", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := len(txBzs[0]) + gasLimit := 100 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size)-1, uint64(gasLimit)) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.Error(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 0, len(proposal.Txs)) + require.Equal(t, int64(0), proposal.Info.BlockSize) + require.Equal(t, uint64(0), proposal.Info.GasLimit) + require.Equal(t, 0, len(proposal.Info.TxsByLane)) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 1, len(block)) + }) + + t.Run("rejects an update where the proposal exceeds max gas limit", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx}) + require.NoError(t, err) + + size := len(txBzs[0]) + gasLimit := 100 + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)-1) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.Error(t, err) + + // Ensure that the proposal is empty. + require.Equal(t, 0, len(proposal.Txs)) + require.Equal(t, int64(0), proposal.Info.BlockSize) + require.Equal(t, uint64(0), proposal.Info.GasLimit) + require.Equal(t, 0, len(proposal.Info.TxsByLane)) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 1, len(block)) + }) + + t.Run("can add transactions from multiple lanes", func(t *testing.T) { + tx, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[0], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + tx2, err := testutils.CreateRandomTx( + encodingConfig.TxConfig, + accounts[1], + 0, + 1, + 0, + 100, + ) + require.NoError(t, err) + + txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx, tx2}) + require.NoError(t, err) + + proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), 10000, 10000) + + err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + require.NoError(t, err) + + otherlane := mocks.NewLane(t) + otherlane.On("Name").Return("test2") + otherlane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("1.0")) + + err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx2}) + require.NoError(t, err) + + size := len(txBzs[0]) + len(txBzs[1]) + gasLimit := 200 + + // Ensure that the proposal is not empty. + require.Equal(t, 2, len(proposal.Txs)) + require.Equal(t, int64(size), proposal.Info.BlockSize) + require.Equal(t, uint64(gasLimit), proposal.Info.GasLimit) + require.Equal(t, 2, len(proposal.Info.TxsByLane)) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"]) + require.Equal(t, uint64(1), proposal.Info.TxsByLane["test2"]) + + // Ensure that the proposal can be marshalled. + block, err := proposal.GetProposalWithInfo() + require.NoError(t, err) + require.Equal(t, 3, len(block)) + require.Equal(t, txBzs[0], block[1]) + require.Equal(t, txBzs[1], block[2]) + }) +} + +func TestGetLaneLimits(t *testing.T) { + testCases := []struct { + name string + maxTxBytes int64 + totalTxBytesUsed int64 + maxGasLimit uint64 + totalGasLimitUsed uint64 + ratio math.LegacyDec + expectedTxBytes int64 + expectedGasLimit uint64 + }{ + { + "ratio is zero", + 100, + 50, + 100, + 50, + math.LegacyZeroDec(), + 50, + 50, + }, + { + "ratio is zero", + 100, + 100, + 50, + 25, + math.LegacyZeroDec(), + 0, + 25, + }, + { + "ratio is zero", + 100, + 150, + 100, + 150, + math.LegacyZeroDec(), + 0, + 0, + }, + { + "ratio is 1", + 100, + 0, + 75, + 0, + math.LegacyOneDec(), + 100, + 75, + }, + { + "ratio is 10%", + 100, + 0, + 75, + 0, + math.LegacyMustNewDecFromStr("0.1"), + 10, + 7, + }, + { + "ratio is 25%", + 100, + 0, + 80, + 0, + math.LegacyMustNewDecFromStr("0.25"), + 25, + 20, + }, + { + "ratio is 50%", + 101, + 0, + 75, + 0, + math.LegacyMustNewDecFromStr("0.5"), + 50, + 37, + }, + { + "ratio is 33%", + 100, + 0, + 75, + 0, + math.LegacyMustNewDecFromStr("0.33"), + 33, + 24, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + proposal := proposals.Proposal{ + Info: types.ProposalInfo{ + MaxBlockSize: tc.maxTxBytes, + BlockSize: tc.totalTxBytesUsed, + MaxGasLimit: tc.maxGasLimit, + GasLimit: tc.totalGasLimitUsed, + }, + } + + res := proposal.GetLaneLimits(tc.ratio) + + if res.MaxTxBytes != tc.expectedTxBytes { + t.Errorf("expected tx bytes %d, got %d", tc.expectedTxBytes, res.MaxTxBytes) + } + + if res.MaxGasLimit != tc.expectedGasLimit { + t.Errorf("expected gas limit %d, got %d", tc.expectedGasLimit, res.MaxGasLimit) + } + }) + } +} diff --git a/block/proposals/types/types.pb.go b/block/proposals/types/types.pb.go new file mode 100644 index 0000000..d45fbe3 --- /dev/null +++ b/block/proposals/types/types.pb.go @@ -0,0 +1,572 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: sdk/proposals/v1/types.proto + +package types + +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 + +// ProposalInfo contains the metadata about a given proposal that was built by +// the block-sdk. This is used to verify and consilidate proposal data across +// the network. +type ProposalInfo struct { + // TxsByLane contains information about how each partial proposal + // was constructed by the block-sdk lanes. + TxsByLane map[string]uint64 `protobuf:"bytes,1,rep,name=txs_by_lane,json=txsByLane,proto3" json:"txs_by_lane,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + // MaxBlockSize corresponds to the upper bound on the size of the + // block that was used to construct this block proposal. + MaxBlockSize int64 `protobuf:"varint,2,opt,name=max_block_size,json=maxBlockSize,proto3" json:"max_block_size,omitempty"` + // MaxGasLimit corresponds to the upper bound on the gas limit of the + // block that was used to construct this block proposal. + MaxGasLimit uint64 `protobuf:"varint,3,opt,name=max_gas_limit,json=maxGasLimit,proto3" json:"max_gas_limit,omitempty"` + // BlockSize corresponds to the size of this block proposal. + BlockSize int64 `protobuf:"varint,4,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"` + // GasLimit corresponds to the gas limit of this block proposal. + GasLimit uint64 `protobuf:"varint,5,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` +} + +func (m *ProposalInfo) Reset() { *m = ProposalInfo{} } +func (m *ProposalInfo) String() string { return proto.CompactTextString(m) } +func (*ProposalInfo) ProtoMessage() {} +func (*ProposalInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_b5d6b8540ee6bc1e, []int{0} +} +func (m *ProposalInfo) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ProposalInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ProposalInfo.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 *ProposalInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_ProposalInfo.Merge(m, src) +} +func (m *ProposalInfo) XXX_Size() int { + return m.Size() +} +func (m *ProposalInfo) XXX_DiscardUnknown() { + xxx_messageInfo_ProposalInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_ProposalInfo proto.InternalMessageInfo + +func (m *ProposalInfo) GetTxsByLane() map[string]uint64 { + if m != nil { + return m.TxsByLane + } + return nil +} + +func (m *ProposalInfo) GetMaxBlockSize() int64 { + if m != nil { + return m.MaxBlockSize + } + return 0 +} + +func (m *ProposalInfo) GetMaxGasLimit() uint64 { + if m != nil { + return m.MaxGasLimit + } + return 0 +} + +func (m *ProposalInfo) GetBlockSize() int64 { + if m != nil { + return m.BlockSize + } + return 0 +} + +func (m *ProposalInfo) GetGasLimit() uint64 { + if m != nil { + return m.GasLimit + } + return 0 +} + +func init() { + proto.RegisterType((*ProposalInfo)(nil), "sdk.proposals.v1.ProposalInfo") + proto.RegisterMapType((map[string]uint64)(nil), "sdk.proposals.v1.ProposalInfo.TxsByLaneEntry") +} + +func init() { proto.RegisterFile("sdk/proposals/v1/types.proto", fileDescriptor_b5d6b8540ee6bc1e) } + +var fileDescriptor_b5d6b8540ee6bc1e = []byte{ + // 325 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x91, 0xcf, 0x4b, 0xc3, 0x30, + 0x1c, 0xc5, 0x97, 0x75, 0x13, 0x9b, 0xcd, 0x31, 0x82, 0x87, 0xe2, 0x8f, 0x52, 0x86, 0x87, 0x5e, + 0x96, 0x32, 0x77, 0x11, 0xf1, 0x34, 0x10, 0x11, 0x36, 0x90, 0xea, 0xc9, 0x4b, 0x49, 0xb7, 0x38, + 0x43, 0x9b, 0xa6, 0x2c, 0x59, 0x69, 0xf7, 0x57, 0xf8, 0x2f, 0xf8, 0xdf, 0x78, 0xdc, 0xd1, 0xa3, + 0x6c, 0xff, 0x88, 0x34, 0x9b, 0x73, 0x7a, 0xfb, 0xbe, 0x97, 0x7c, 0x1e, 0x0f, 0x1e, 0x3c, 0x93, + 0x93, 0xc8, 0x4b, 0x67, 0x22, 0x15, 0x92, 0xc4, 0xd2, 0xcb, 0x7a, 0x9e, 0x2a, 0x52, 0x2a, 0x71, + 0x3a, 0x13, 0x4a, 0xa0, 0xb6, 0x9c, 0x44, 0x78, 0xf7, 0x8a, 0xb3, 0x5e, 0xe7, 0xbd, 0x0a, 0x9b, + 0x0f, 0x5b, 0xe3, 0x3e, 0x79, 0x11, 0x68, 0x04, 0x1b, 0x2a, 0x97, 0x41, 0x58, 0x04, 0x31, 0x49, + 0xa8, 0x05, 0x1c, 0xc3, 0x6d, 0x5c, 0x76, 0xf1, 0x7f, 0x10, 0xef, 0x43, 0xf8, 0x29, 0x97, 0x83, + 0x62, 0x48, 0x12, 0x7a, 0x9b, 0xa8, 0x59, 0xe1, 0x9b, 0xea, 0x47, 0xa3, 0x0b, 0xd8, 0xe2, 0x24, + 0x0f, 0xc2, 0x58, 0x8c, 0xa3, 0x40, 0xb2, 0x05, 0xb5, 0xaa, 0x0e, 0x70, 0x0d, 0xbf, 0xc9, 0x49, + 0x3e, 0x28, 0xcd, 0x47, 0xb6, 0xa0, 0xa8, 0x03, 0x8f, 0xca, 0x5f, 0x53, 0x22, 0x83, 0x98, 0x71, + 0xa6, 0x2c, 0xc3, 0x01, 0x6e, 0xcd, 0x6f, 0x70, 0x92, 0xdf, 0x11, 0x39, 0x2c, 0x2d, 0x74, 0x0e, + 0xe1, 0x5e, 0x4a, 0x4d, 0xa7, 0x98, 0xe1, 0x2e, 0xe2, 0x14, 0x9a, 0xbf, 0x78, 0x5d, 0xe3, 0x87, + 0xd3, 0x2d, 0x7b, 0x72, 0x03, 0x5b, 0x7f, 0x2b, 0xa2, 0x36, 0x34, 0x22, 0x5a, 0x58, 0xc0, 0x01, + 0xae, 0xe9, 0x97, 0x27, 0x3a, 0x86, 0xf5, 0x8c, 0xc4, 0xf3, 0x4d, 0xc1, 0x9a, 0xbf, 0x11, 0xd7, + 0xd5, 0x2b, 0x30, 0x18, 0x7d, 0xac, 0x6c, 0xb0, 0x5c, 0xd9, 0xe0, 0x6b, 0x65, 0x83, 0xb7, 0xb5, + 0x5d, 0x59, 0xae, 0xed, 0xca, 0xe7, 0xda, 0xae, 0x3c, 0xf7, 0xa7, 0x4c, 0xbd, 0xce, 0x43, 0x3c, + 0x16, 0xdc, 0x93, 0x11, 0x4b, 0xbb, 0x9c, 0x66, 0x9e, 0xee, 0xd4, 0x2d, 0x77, 0xd0, 0xd7, 0xde, + 0x1a, 0x7a, 0x8a, 0xf0, 0x40, 0x6f, 0xd1, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xfb, 0x40, + 0x73, 0xab, 0x01, 0x00, 0x00, +} + +func (m *ProposalInfo) 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 *ProposalInfo) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ProposalInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.GasLimit != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.GasLimit)) + i-- + dAtA[i] = 0x28 + } + if m.BlockSize != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.BlockSize)) + i-- + dAtA[i] = 0x20 + } + if m.MaxGasLimit != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.MaxGasLimit)) + i-- + dAtA[i] = 0x18 + } + if m.MaxBlockSize != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.MaxBlockSize)) + i-- + dAtA[i] = 0x10 + } + if len(m.TxsByLane) > 0 { + for k := range m.TxsByLane { + v := m.TxsByLane[k] + baseI := i + i = encodeVarintTypes(dAtA, i, uint64(v)) + i-- + dAtA[i] = 0x10 + i -= len(k) + copy(dAtA[i:], k) + i = encodeVarintTypes(dAtA, i, uint64(len(k))) + i-- + dAtA[i] = 0xa + i = encodeVarintTypes(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintTypes(dAtA []byte, offset int, v uint64) int { + offset -= sovTypes(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *ProposalInfo) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.TxsByLane) > 0 { + for k, v := range m.TxsByLane { + _ = k + _ = v + mapEntrySize := 1 + len(k) + sovTypes(uint64(len(k))) + 1 + sovTypes(uint64(v)) + n += mapEntrySize + 1 + sovTypes(uint64(mapEntrySize)) + } + } + if m.MaxBlockSize != 0 { + n += 1 + sovTypes(uint64(m.MaxBlockSize)) + } + if m.MaxGasLimit != 0 { + n += 1 + sovTypes(uint64(m.MaxGasLimit)) + } + if m.BlockSize != 0 { + n += 1 + sovTypes(uint64(m.BlockSize)) + } + if m.GasLimit != 0 { + n += 1 + sovTypes(uint64(m.GasLimit)) + } + return n +} + +func sovTypes(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTypes(x uint64) (n int) { + return sovTypes(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *ProposalInfo) 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 ErrIntOverflowTypes + } + 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: ProposalInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ProposalInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TxsByLane", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.TxsByLane == nil { + m.TxsByLane = make(map[string]uint64) + } + var mapkey string + var mapvalue uint64 + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthTypes + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return ErrInvalidLengthTypes + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapvalue |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + } else { + iNdEx = entryPreIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.TxsByLane[mapkey] = mapvalue + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxBlockSize", wireType) + } + m.MaxBlockSize = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxBlockSize |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxGasLimit", wireType) + } + m.MaxGasLimit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxGasLimit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockSize", wireType) + } + m.BlockSize = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.BlockSize |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasLimit", wireType) + } + m.GasLimit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasLimit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTypes(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, ErrIntOverflowTypes + } + 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, ErrIntOverflowTypes + } + 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, ErrIntOverflowTypes + } + 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, ErrInvalidLengthTypes + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTypes + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTypes + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTypes = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTypes = fmt.Errorf("proto: unexpected end of group") +) diff --git a/block/proposals/update.go b/block/proposals/update.go new file mode 100644 index 0000000..4d19897 --- /dev/null +++ b/block/proposals/update.go @@ -0,0 +1,113 @@ +package proposals + +import ( + "fmt" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/utils" +) + +// Lane defines the contract interface for a lane. +type Lane interface { + GetMaxBlockSpace() math.LegacyDec + Name() string +} + +// UpdateProposal updates the proposal with the given transactions and lane limits. There are a +// few invariants that are checked: +// 1. The total size of the proposal must be less than the maximum number of bytes allowed. +// 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for +// the lane. +// 3. The total gas limit of the proposal must be less than the maximum gas limit allowed. +// 4. The total gas limit of the partial proposal must be less than the maximum gas limit allowed for +// the lane. +// 5. The lane must not have already prepared a partial proposal. +// 6. The transaction must not already be in the proposal. +func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error { + if len(partialProposal) == 0 { + return nil + } + + // invariant check: Ensure we have not already prepared a partial proposal for this lane. + if _, ok := p.Info.TxsByLane[lane.Name()]; ok { + return fmt.Errorf("lane %s already prepared a partial proposal", lane) + } + + // Aggregate info from the transactions. + hashes := make(map[string]struct{}) + txs := make([][]byte, len(partialProposal)) + partialProposalSize := int64(0) + partialProposalGasLimit := uint64(0) + + for index, tx := range partialProposal { + txInfo, err := utils.GetTxInfo(p.TxEncoder, tx) + if err != nil { + return fmt.Errorf("err retrieving transaction info: %s", err) + } + + // invariant check: Ensure that the transaction is not already in the proposal. + if _, ok := p.Cache[txInfo.Hash]; ok { + return fmt.Errorf("transaction %s is already in the proposal", txInfo.Hash) + } + + hashes[txInfo.Hash] = struct{}{} + partialProposalSize += txInfo.Size + partialProposalGasLimit += txInfo.GasLimit + txs[index] = txInfo.TxBytes + } + + // invariant check: Ensure that the partial proposal is not too large. + limit := p.GetLaneLimits(lane.GetMaxBlockSpace()) + if partialProposalSize > limit.MaxTxBytes { + return fmt.Errorf( + "partial proposal is too large: %d > %d", + partialProposalSize, + limit.MaxTxBytes, + ) + } + + // invariant check: Ensure that the partial proposal does not consume too much gas. + if partialProposalGasLimit > limit.MaxGasLimit { + return fmt.Errorf( + "partial proposal consumes too much gas: %d > %d", + partialProposalGasLimit, + limit.MaxGasLimit, + ) + } + + // invariant check: Ensure that the lane did not prepare a block proposal that is too large. + updatedSize := p.Info.BlockSize + partialProposalSize + if updatedSize > p.Info.MaxBlockSize { + return fmt.Errorf( + "block proposal is too large: %d > %d", + updatedSize, + p.Info.MaxBlockSize, + ) + } + + // invariant check: Ensure that the lane did not prepare a block proposal that consumes too much gas. + updatedGasLimit := p.Info.GasLimit + partialProposalGasLimit + if updatedGasLimit > p.Info.MaxGasLimit { + return fmt.Errorf( + "block proposal consumes too much gas: %d > %d", + updatedGasLimit, + p.Info.MaxGasLimit, + ) + } + + // Update the proposal. + p.Info.BlockSize = updatedSize + p.Info.GasLimit = updatedGasLimit + + // Update the lane info. + p.Info.TxsByLane[lane.Name()] = uint64(len(partialProposal)) + + // Update the proposal. + p.Txs = append(p.Txs, txs...) + for hash := range hashes { + p.Cache[hash] = struct{}{} + } + + return nil +} diff --git a/block/proposals/utils.go b/block/proposals/utils.go new file mode 100644 index 0000000..1331645 --- /dev/null +++ b/block/proposals/utils.go @@ -0,0 +1,39 @@ +package proposals + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // MaxUint64 is the maximum value of a uint64. + MaxUint64 = 1<<64 - 1 +) + +type ( + // LaneLimits defines the constraints for a partial proposal. Each lane must only propose + // transactions that satisfy these constraints. Otherwise the partial proposal update will + // be rejected. + LaneLimits struct { + // MaxTxBytes is the maximum number of bytes allowed in the partial proposal. + MaxTxBytes int64 + // MaxGasLimit is the maximum gas limit allowed in the partial proposal. + MaxGasLimit uint64 + } +) + +// GetBlockLimits retrieves the maximum number of bytes and gas limit allowed in a block. +func GetBlockLimits(ctx sdk.Context) (int64, uint64) { + blockParams := ctx.ConsensusParams().Block + + // If the max gas is set to 0, then the max gas limit for the block can be infinite. + // Otherwise we use the max gas limit casted as a uint64 which is how gas limits are + // extracted from sdk.Tx's. + var maxGasLimit uint64 + if maxGas := blockParams.MaxGas; maxGas > 0 { + maxGasLimit = uint64(maxGas) + } else { + maxGasLimit = MaxUint64 + } + + return blockParams.MaxBytes, maxGasLimit +} diff --git a/block/types.go b/block/types.go index f8865be..2b19290 100644 --- a/block/types.go +++ b/block/types.go @@ -2,24 +2,25 @@ package block import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/proposals" ) type ( // PrepareLanesHandler wraps all of the lanes' PrepareLane function into a single chained // function. You can think of it like an AnteHandler, but for preparing proposals in the // context of lanes instead of modules. - PrepareLanesHandler func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) + PrepareLanesHandler func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) // ProcessLanesHandler wraps all of the lanes' ProcessLane functions into a single chained // function. You can think of it like an AnteHandler, but for processing proposals in the // context of lanes instead of modules. - ProcessLanesHandler func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) + ProcessLanesHandler func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) ) // NoOpPrepareLanesHandler returns a no-op prepare lanes handler. // This should only be used for testing. func NoOpPrepareLanesHandler() PrepareLanesHandler { - return func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) { + return func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) { return proposal, nil } } @@ -27,7 +28,7 @@ func NoOpPrepareLanesHandler() PrepareLanesHandler { // NoOpProcessLanesHandler returns a no-op process lanes handler. // This should only be used for testing. func NoOpProcessLanesHandler() ProcessLanesHandler { - return func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) { - return ctx, nil + return func(_ sdk.Context, p proposals.Proposal) (proposals.Proposal, error) { + return p, nil } } diff --git a/block/utils/utils.go b/block/utils/utils.go index a4b2ae0..9f2881d 100644 --- a/block/utils/utils.go +++ b/block/utils/utils.go @@ -5,23 +5,47 @@ import ( "encoding/hex" "fmt" - "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" ) -// GetTxHashStr returns the hex-encoded hash of the transaction alongside the -// transaction bytes. -func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) ([]byte, string, error) { +type ( + // TxInfo contains the information required for a transaction to be + // included in a proposal. + TxInfo struct { + // Hash is the hex-encoded hash of the transaction. + Hash string + // Size is the size of the transaction in bytes. + Size int64 + // GasLimit is the gas limit of the transaction. + GasLimit uint64 + // TxBytes is the bytes of the transaction. + TxBytes []byte + } +) + +// GetTxHashStr returns the TxInfo of a given transaction. +func GetTxInfo(txEncoder sdk.TxEncoder, tx sdk.Tx) (TxInfo, error) { txBz, err := txEncoder(tx) if err != nil { - return nil, "", fmt.Errorf("failed to encode transaction: %w", err) + return TxInfo{}, fmt.Errorf("failed to encode transaction: %w", err) } txHash := sha256.Sum256(txBz) txHashStr := hex.EncodeToString(txHash[:]) - return txBz, txHashStr, nil + // TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc. + gasTx, ok := tx.(sdk.FeeTx) + if !ok { + return TxInfo{}, fmt.Errorf("failed to cast transaction to GasTx") + } + + return TxInfo{ + Hash: txHashStr, + Size: int64(len(txBz)), + GasLimit: gasTx.GetGas(), + TxBytes: txBz, + }, nil } // GetDecodedTxs returns the decoded transactions from the given bytes. @@ -39,6 +63,21 @@ func GetDecodedTxs(txDecoder sdk.TxDecoder, txs [][]byte) ([]sdk.Tx, error) { return decodedTxs, nil } +// GetEncodedTxs returns the encoded transactions from the given bytes. +func GetEncodedTxs(txEncoder sdk.TxEncoder, txs []sdk.Tx) ([][]byte, error) { + var encodedTxs [][]byte + for _, tx := range txs { + txBz, err := txEncoder(tx) + if err != nil { + return nil, fmt.Errorf("failed to encode transaction: %w", err) + } + + encodedTxs = append(encodedTxs, txBz) + } + + return encodedTxs, nil +} + // RemoveTxsFromLane removes the transactions from the given lane's mempool. func RemoveTxsFromLane(txs []sdk.Tx, mempool sdkmempool.Mempool) error { for _, tx := range txs { @@ -49,23 +88,3 @@ func RemoveTxsFromLane(txs []sdk.Tx, mempool sdkmempool.Mempool) error { return nil } - -// GetMaxTxBytesForLane returns the maximum number of bytes that can be included in the proposal -// for the given lane. -func GetMaxTxBytesForLane(maxTxBytes, totalTxBytes int64, ratio math.LegacyDec) int64 { - // In the case where the ratio is zero, we return the max tx bytes remaining. Note, the only - // lane that should have a ratio of zero is the default lane. This means the default lane - // will have no limit on the number of transactions it can include in a block and is only - // limited by the maxTxBytes included in the PrepareProposalRequest. - if ratio.IsZero() { - remainder := maxTxBytes - totalTxBytes - if remainder < 0 { - return 0 - } - - return remainder - } - - // Otherwise, we calculate the max tx bytes for the lane based on the ratio. - return ratio.MulInt64(maxTxBytes).TruncateInt().Int64() -} diff --git a/block/utils/utils_test.go b/block/utils/utils_test.go deleted file mode 100644 index 3e6d85b..0000000 --- a/block/utils/utils_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package utils_test - -import ( - "testing" - - "cosmossdk.io/math" - - "github.com/skip-mev/block-sdk/block/utils" -) - -func TestGetMaxTxBytesForLane(t *testing.T) { - testCases := []struct { - name string - maxTxBytes int64 - totalTxBytes int64 - ratio math.LegacyDec - expected int64 - }{ - { - "ratio is zero", - 100, - 50, - math.LegacyZeroDec(), - 50, - }, - { - "ratio is zero", - 100, - 100, - math.LegacyZeroDec(), - 0, - }, - { - "ratio is zero", - 100, - 150, - math.LegacyZeroDec(), - 0, - }, - { - "ratio is 1", - 100, - 50, - math.LegacyOneDec(), - 100, - }, - { - "ratio is 10%", - 100, - 50, - math.LegacyMustNewDecFromStr("0.1"), - 10, - }, - { - "ratio is 25%", - 100, - 50, - math.LegacyMustNewDecFromStr("0.25"), - 25, - }, - { - "ratio is 50%", - 101, - 50, - math.LegacyMustNewDecFromStr("0.5"), - 50, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := utils.GetMaxTxBytesForLane(tc.maxTxBytes, tc.totalTxBytes, tc.ratio) - if actual != tc.expected { - t.Errorf("expected %d, got %d", tc.expected, actual) - } - }) - } -} diff --git a/lanes/base/abci_test.go b/lanes/base/abci_test.go index c577fb6..cae064b 100644 --- a/lanes/base/abci_test.go +++ b/lanes/base/abci_test.go @@ -12,7 +12,9 @@ import ( signer_extraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" "github.com/skip-mev/block-sdk/block" "github.com/skip-mev/block-sdk/block/base" - "github.com/skip-mev/block-sdk/block/utils/mocks" + "github.com/skip-mev/block-sdk/block/mocks" + "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/utils" defaultlane "github.com/skip-mev/block-sdk/lanes/base" testutils "github.com/skip-mev/block-sdk/testutils" ) @@ -26,33 +28,36 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx expectedExecution := map[sdk.Tx]bool{ tx: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) - - // Insert the transaction into the lane + lane := s.initLane(math.LegacyMustNewDecFromStr("0.5"), expectedExecution) s.Require().NoError(lane.Insert(sdk.Context{}, tx)) txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) s.Require().NoError(err) - // Create a proposal - maxTxBytes := int64(len(txBz) - 1) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + int64(len(txBz)), + 1, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is empty - s.Require().Equal(0, proposal.GetNumTxs()) - s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + s.Require().Equal(0, len(finalProposal.Txs)) + s.Require().Equal(int64(0), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(0), finalProposal.Info.GasLimit) }) - s.Run("should not build a proposal when box space configured to lane is too small", func() { + s.Run("should not build a proposal when gas configured to lane is too small", func() { // Create a basic transaction that should not in the proposal tx, err := testutils.CreateRandomTx( s.encodingConfig.TxConfig, @@ -60,15 +65,58 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 10, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx expectedExecution := map[sdk.Tx]bool{ tx: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("0.000001"), expectedExecution) + lane := s.initLane(math.LegacyMustNewDecFromStr("0.5"), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + limit := proposals.LaneLimits{ + MaxTxBytes: int64(len(txBz)) * 10, + MaxGasLimit: 10, + } + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + limit.MaxTxBytes, + limit.MaxGasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is empty + s.Require().Equal(0, len(finalProposal.Txs)) + s.Require().Equal(int64(0), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(0), finalProposal.Info.GasLimit) + }) + + s.Run("should not build a proposal when gas configured to lane is too small p2", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 10, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + expectedExecution := map[sdk.Tx]bool{ + tx: true, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("0.5"), expectedExecution) // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}, tx)) @@ -77,48 +125,67 @@ func (s *BaseTestSuite) TestPrepareLane() { s.Require().NoError(err) // Create a proposal - maxTxBytes := int64(len(txBz)) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) - s.Require().Error(err) + limit := proposals.LaneLimits{ + MaxTxBytes: int64(len(txBz)) * 10, // have enough space for 10 of these txs + MaxGasLimit: 10, + } + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + limit.MaxTxBytes, + limit.MaxGasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) + s.Require().NoError(err) // Ensure the proposal is empty - s.Require().Equal(0, proposal.GetNumTxs()) - s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + s.Require().Equal(0, len(finalProposal.Txs)) + s.Require().Equal(int64(0), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(0), finalProposal.Info.GasLimit) }) s.Run("should be able to build a proposal with a tx that just fits in", func() { - // Create a basic transaction that should not in the proposal + // Create a basic transaction that should just fit in with the gas limit + // and max size tx, err := testutils.CreateRandomTx( s.encodingConfig.TxConfig, s.accounts[0], 0, 1, 0, + 10, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx expectedExecution := map[sdk.Tx]bool{ tx: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + lane := s.initLane(math.LegacyOneDec(), expectedExecution) - // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}, tx)) txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) s.Require().NoError(err) - // Create a proposal - maxTxBytes := int64(len(txBz)) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + limit := proposals.LaneLimits{ + MaxTxBytes: int64(len(txBz)), + MaxGasLimit: 10, + } + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + limit.MaxTxBytes, + limit.MaxGasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is not empty and contains the transaction - s.Require().Equal(1, proposal.GetNumTxs()) - s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) - s.Require().Equal(txBz, proposal.GetTxs()[0]) + s.Require().Equal(1, len(finalProposal.Txs)) + s.Require().Equal(limit.MaxTxBytes, finalProposal.Info.BlockSize) + s.Require().Equal(uint64(10), finalProposal.Info.GasLimit) + s.Require().Equal(txBz, finalProposal.Txs[0]) }) s.Run("should not build a proposal with a that fails verify tx", func() { @@ -129,30 +196,35 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 10, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + // We expect the transaction to fail verify tx expectedExecution := map[sdk.Tx]bool{ tx: false, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + lane := s.initLane(math.LegacyOneDec(), expectedExecution) - // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}, tx)) - // Create a proposal txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) s.Require().NoError(err) - maxTxBytes := int64(len(txBz)) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + int64(len(txBz)), + 10, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is empty - s.Require().Equal(0, proposal.GetNumTxs()) - s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + s.Require().Equal(0, len(finalProposal.Txs)) + s.Require().Equal(int64(0), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(0), finalProposal.Info.GasLimit) // Ensure the transaction is removed from the lane s.Require().False(lane.Contains(tx)) @@ -160,13 +232,13 @@ func (s *BaseTestSuite) TestPrepareLane() { }) s.Run("should order transactions correctly in the proposal", func() { - // Create a basic transaction that should not in the proposal tx1, err := testutils.CreateRandomTx( s.encodingConfig.TxConfig, s.accounts[0], 0, 1, 0, + 10, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), ) s.Require().NoError(err) @@ -177,18 +249,17 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 10, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx expectedExecution := map[sdk.Tx]bool{ tx1: true, tx2: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + lane := s.initLane(math.LegacyOneDec(), expectedExecution) - // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}, tx1)) s.Require().NoError(lane.Insert(sdk.Context{}, tx2)) @@ -198,24 +269,32 @@ func (s *BaseTestSuite) TestPrepareLane() { txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) s.Require().NoError(err) - maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + size := int64(len(txBz1)) + int64(len(txBz2)) + gasLimit := uint64(20) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + size, + gasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is ordered correctly - s.Require().Equal(2, proposal.GetNumTxs()) - s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) - s.Require().Equal([][]byte{txBz1, txBz2}, proposal.GetTxs()) + s.Require().Equal(2, len(finalProposal.Txs)) + s.Require().Equal(size, finalProposal.Info.BlockSize) + s.Require().Equal(gasLimit, finalProposal.Info.GasLimit) + s.Require().Equal([][]byte{txBz1, txBz2}, finalProposal.Txs) }) s.Run("should order transactions correctly in the proposal (with different insertion)", func() { - // Create a basic transaction that should not in the proposal tx1, err := testutils.CreateRandomTx( s.encodingConfig.TxConfig, s.accounts[0], 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -226,18 +305,17 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), ) s.Require().NoError(err) - // Create a lane with a max block space of 1 but a proposal that is smaller than the tx expectedExecution := map[sdk.Tx]bool{ tx1: true, tx2: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + lane := s.initLane(math.LegacyOneDec(), expectedExecution) - // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}, tx1)) s.Require().NoError(lane.Insert(sdk.Context{}, tx2)) @@ -247,14 +325,22 @@ func (s *BaseTestSuite) TestPrepareLane() { txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) s.Require().NoError(err) - maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + size := int64(len(txBz1)) + int64(len(txBz2)) + gasLimit := uint64(2) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + size, + gasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is ordered correctly - s.Require().Equal(2, proposal.GetNumTxs()) - s.Require().Equal(maxTxBytes, proposal.GetTotalTxBytes()) - s.Require().Equal([][]byte{txBz2, txBz1}, proposal.GetTxs()) + s.Require().Equal(2, len(finalProposal.Txs)) + s.Require().Equal(size, finalProposal.Info.BlockSize) + s.Require().Equal(gasLimit, finalProposal.Info.GasLimit) + s.Require().Equal([][]byte{txBz2, txBz1}, finalProposal.Txs) }) s.Run("should include tx that fits in proposal when other does not", func() { @@ -265,6 +351,7 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 1, 0, + 2, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -275,6 +362,7 @@ func (s *BaseTestSuite) TestPrepareLane() { 0, 10, // This tx is too large to fit in the proposal 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -284,7 +372,7 @@ func (s *BaseTestSuite) TestPrepareLane() { tx1: true, tx2: true, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), expectedExecution) + lane := s.initLane(math.LegacyOneDec(), expectedExecution) // Insert the transaction into the lane s.Require().NoError(lane.Insert(sdk.Context{}.WithPriority(10), tx1)) @@ -296,14 +384,130 @@ func (s *BaseTestSuite) TestPrepareLane() { txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) s.Require().NoError(err) - maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) - 1 - proposal, err := lane.PrepareLane(sdk.Context{}, block.NewProposal(maxTxBytes), maxTxBytes, block.NoOpPrepareLanesHandler()) + size := int64(len(txBz1)) + int64(len(txBz2)) - 1 + gasLimit := uint64(3) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + size, + gasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) s.Require().NoError(err) // Ensure the proposal is ordered correctly - s.Require().Equal(1, proposal.GetNumTxs()) - s.Require().Equal(int64(len(txBz1)), proposal.GetTotalTxBytes()) - s.Require().Equal([][]byte{txBz1}, proposal.GetTxs()) + s.Require().Equal(1, len(finalProposal.Txs)) + s.Require().Equal(int64(len(txBz1)), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(2), finalProposal.Info.GasLimit) + s.Require().Equal([][]byte{txBz1}, finalProposal.Txs) + }) + + s.Run("should include tx that consumes all gas in proposal while other cannot", func() { + // Create a basic transaction that should not in the proposal + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 2, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 10, // This tx is too large to fit in the proposal + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx1: true, + tx2: true, + } + lane := s.initLane(math.LegacyOneDec(), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx1)) + s.Require().NoError(lane.Insert(sdk.Context{}, tx2)) + + txBz1, err := s.encodingConfig.TxConfig.TxEncoder()(tx1) + s.Require().NoError(err) + + txBz2, err := s.encodingConfig.TxConfig.TxEncoder()(tx2) + s.Require().NoError(err) + + size := int64(len(txBz1)) + int64(len(txBz2)) - 1 + gasLimit := uint64(1) + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + size, + gasLimit, + ) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is ordered correctly + s.Require().Equal(1, len(finalProposal.Txs)) + s.Require().Equal(int64(len(txBz2)), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(1), finalProposal.Info.GasLimit) + s.Require().Equal([][]byte{txBz2}, finalProposal.Txs) + }) + + s.Run("should not attempt to include transaction already included in the proposal", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 2, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + // Create a lane with a max block space of 1 but a proposal that is smaller than the tx + expectedExecution := map[sdk.Tx]bool{ + tx: true, + } + lane := s.initLane(math.LegacyOneDec(), expectedExecution) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}, tx)) + + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + int64(len(txBz))*10, + 1000000, + ) + + mockLane := mocks.NewLane(s.T()) + + mockLane.On("Name").Return("test") + mockLane.On("GetMaxBlockSpace").Return(math.LegacyOneDec()) + + err = emptyProposal.UpdateProposal(mockLane, []sdk.Tx{tx}) + s.Require().NoError(err) + + finalProposal, err := lane.PrepareLane(sdk.Context{}, emptyProposal, block.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is ordered correctly + s.Require().Equal(1, len(finalProposal.Txs)) + s.Require().Equal(int64(len(txBz)), finalProposal.Info.BlockSize) + s.Require().Equal(uint64(2), finalProposal.Info.GasLimit) + s.Require().Equal([][]byte{txBz}, finalProposal.Txs) }) } @@ -315,6 +519,7 @@ func (s *BaseTestSuite) TestProcessLane() { 0, 1, 0, + 1, ) s.Require().NoError(err) @@ -322,11 +527,23 @@ func (s *BaseTestSuite) TestProcessLane() { tx1, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ - tx1: true, - }) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + }, + ) - _, err = lane.ProcessLane(sdk.Context{}, proposal, block.NoOpProcessLanesHandler()) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) s.Require().NoError(err) }) @@ -337,6 +554,7 @@ func (s *BaseTestSuite) TestProcessLane() { 0, 1, 0, + 1, ) s.Require().NoError(err) @@ -344,11 +562,23 @@ func (s *BaseTestSuite) TestProcessLane() { tx1, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ - tx1: false, - }) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: false, + }, + ) - _, err = lane.ProcessLane(sdk.Context{}, proposal, block.NoOpProcessLanesHandler()) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) s.Require().Error(err) }) @@ -359,6 +589,7 @@ func (s *BaseTestSuite) TestProcessLane() { 0, 1, 0, + 1, ) s.Require().NoError(err) @@ -368,6 +599,7 @@ func (s *BaseTestSuite) TestProcessLane() { 0, 1, 0, + 1, ) s.Require().NoError(err) @@ -377,6 +609,7 @@ func (s *BaseTestSuite) TestProcessLane() { 0, 1, 0, + 1, ) s.Require().NoError(err) @@ -386,18 +619,27 @@ func (s *BaseTestSuite) TestProcessLane() { tx3, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ - tx1: true, - tx2: false, - tx3: true, - }) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: false, + tx3: true, + }) - _, err = lane.ProcessLane(sdk.Context{}, proposal, block.NoOpProcessLanesHandler()) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) s.Require().Error(err) }) -} -func (s *BaseTestSuite) TestCheckOrder() { s.Run("should accept proposal with transactions in correct order", func() { tx1, err := testutils.CreateRandomTx( s.encodingConfig.TxConfig, @@ -405,6 +647,7 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), ) s.Require().NoError(err) @@ -415,6 +658,7 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -424,11 +668,24 @@ func (s *BaseTestSuite) TestCheckOrder() { tx2, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ - tx1: true, - tx2: true, - }) - s.Require().NoError(lane.CheckOrder(sdk.Context{}, proposal)) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().NoError(err) }) s.Run("should not accept a proposal with transactions that are not in the correct order", func() { @@ -438,6 +695,7 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -448,6 +706,7 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), ) s.Require().NoError(err) @@ -457,11 +716,24 @@ func (s *BaseTestSuite) TestCheckOrder() { tx2, } - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ - tx1: true, - tx2: true, - }) - s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) }) s.Run("should not accept a proposal where transactions are out of order relative to other lanes", func() { @@ -471,6 +743,7 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 2, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), ) s.Require().NoError(err) @@ -481,23 +754,203 @@ func (s *BaseTestSuite) TestCheckOrder() { 0, 1, 0, + 1, sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), ) s.Require().NoError(err) - mocklane := mocks.NewLane(s.T()) - mocklane.On("Match", sdk.Context{}, tx1).Return(true) - mocklane.On("Match", sdk.Context{}, tx2).Return(false) + otherLane := s.initLane(math.LegacyOneDec(), nil) - lane := s.initLane(math.LegacyMustNewDecFromStr("1"), nil) - lane.SetIgnoreList([]block.Lane{mocklane}) + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: false, + }) + lane.SetIgnoreList([]block.Lane{otherLane}) proposal := []sdk.Tx{ tx1, tx2, } - s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + 100000, + 100000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal that builds too large of a partial block", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 1, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + }) + + maxSize := s.getTxSize(tx1) - 1 + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + maxSize, + 1000000, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal that builds a partial block that is too gas consumptive", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 10, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + }, + ) + + maxSize := s.getTxSize(tx1) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + maxSize, + 9, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal that builds a partial block that is too gas consumptive p2", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 10, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 10, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + + maxSize := s.getTxSize(tx1) + s.getTxSize(tx2) + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + maxSize, + 19, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal that builds a partial block that is too large p2", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 10, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 10, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane( + math.LegacyOneDec(), + map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }, + ) + + maxSize := s.getTxSize(tx1) + s.getTxSize(tx2) - 1 + partialProposal, err := utils.GetEncodedTxs(s.encodingConfig.TxConfig.TxEncoder(), proposal) + s.Require().NoError(err) + + emptyProposal := proposals.NewProposal( + s.encodingConfig.TxConfig.TxEncoder(), + maxSize, + 20, + ) + + _, err = lane.ProcessLane(sdk.Context{}, emptyProposal, partialProposal, block.NoOpProcessLanesHandler()) + s.Require().Error(err) }) } @@ -549,3 +1002,10 @@ func (s *BaseTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool) sdk. return anteHandler } + +func (s *BaseTestSuite) getTxSize(tx sdk.Tx) int64 { + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + + return int64(len(txBz)) +} diff --git a/lanes/base/mempool_test.go b/lanes/base/mempool_test.go index c21120c..341b289 100644 --- a/lanes/base/mempool_test.go +++ b/lanes/base/mempool_test.go @@ -19,6 +19,7 @@ func (s *BaseTestSuite) TestGetTxPriority() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -34,6 +35,7 @@ func (s *BaseTestSuite) TestGetTxPriority() { 0, 0, 0, + 0, ) s.Require().NoError(err) @@ -48,6 +50,7 @@ func (s *BaseTestSuite) TestGetTxPriority() { 0, 0, 0, + 0, sdk.NewCoin("random", math.NewInt(100)), ) s.Require().NoError(err) @@ -95,6 +98,7 @@ func (s *BaseTestSuite) TestInsert() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -112,6 +116,7 @@ func (s *BaseTestSuite) TestInsert() { uint64(i), 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(int64(100*i))), ) s.Require().NoError(err) @@ -127,6 +132,7 @@ func (s *BaseTestSuite) TestInsert() { 10, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -147,6 +153,7 @@ func (s *BaseTestSuite) TestRemove() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -166,6 +173,7 @@ func (s *BaseTestSuite) TestRemove() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -184,6 +192,7 @@ func (s *BaseTestSuite) TestSelect() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) @@ -194,6 +203,7 @@ func (s *BaseTestSuite) TestSelect() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(200)), ) s.Require().NoError(err) @@ -223,6 +233,7 @@ func (s *BaseTestSuite) TestSelect() { 0, 0, 0, + 0, sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), ) s.Require().NoError(err) diff --git a/lanes/build-your-own/README.md b/lanes/build-your-own/README.md index 63800a9..5eef831 100644 --- a/lanes/build-your-own/README.md +++ b/lanes/build-your-own/README.md @@ -339,7 +339,7 @@ the lane either. ProcessLaneHandler func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) ``` -Given the invarients above, the default implementation is simple. It will +Given the invariants above, the default implementation is simple. It will continue to verify transactions in the block proposal under the following criteria: 1. If a transaction matches to this lane, verify it and continue. If it is not diff --git a/lanes/mev/abci.go b/lanes/mev/abci.go index 86fc864..2084960 100644 --- a/lanes/mev/abci.go +++ b/lanes/mev/abci.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/skip-mev/block-sdk/block" "github.com/skip-mev/block-sdk/block/base" + "github.com/skip-mev/block-sdk/block/proposals" "github.com/skip-mev/block-sdk/block/utils" "github.com/skip-mev/block-sdk/x/auction/types" ) @@ -17,11 +17,11 @@ import ( // will return no transactions if no valid bids are found. If any of the bids are invalid, // it will return them and will only remove the bids and not the bundled transactions. func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { - return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + return func(ctx sdk.Context, proposal proposals.Proposal, limit proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) { // Define all of the info we need to select transactions for the partial proposal. var ( - txs [][]byte - txsToRemove []sdk.Tx + txsToInclude []sdk.Tx + txsToRemove []sdk.Tx ) // Attempt to select the highest bid transaction that is valid and whose @@ -30,209 +30,228 @@ func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { selectBidTxLoop: for ; bidTxIterator != nil; bidTxIterator = bidTxIterator.Next() { cacheCtx, write := ctx.CacheContext() - tmpBidTx := bidTxIterator.Tx() + bidTx := bidTxIterator.Tx() - bidTxBz, hash, err := utils.GetTxHashStr(l.TxEncoder(), tmpBidTx) + txInfo, err := utils.GetTxInfo(l.TxEncoder(), bidTx) if err != nil { l.Logger().Info("failed to get hash of auction bid tx", "err", err) - txsToRemove = append(txsToRemove, tmpBidTx) + txsToRemove = append(txsToRemove, bidTx) continue selectBidTxLoop } // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(bidTxBz) { + // + // TODO: Should we really be panic'ing here? + if proposal.Contains(txInfo.Hash) { l.Logger().Info( "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, + "tx_hash", txInfo.Hash, ) continue selectBidTxLoop } - bidTxSize := int64(len(bidTxBz)) - if bidTxSize <= maxTxBytes { - // Build the partial proposal by selecting the bid transaction and all of - // its bundled transactions. - bidInfo, err := l.GetAuctionBidInfo(tmpBidTx) - if err != nil { - l.Logger().Info( - "failed to get auction bid info", - "tx_hash", hash, - "err", err, - ) + if txInfo.Size > limit.MaxTxBytes { + l.Logger().Info( + "failed to select auction bid tx for lane; tx size is too large", + "tx_size", txInfo.Size, + "max_size", limit.MaxTxBytes, + "tx_hash", txInfo.Hash, + ) - // Some transactions in the bundle may be malformed or invalid, so we - // remove the bid transaction and try the next top bid. - txsToRemove = append(txsToRemove, tmpBidTx) - continue selectBidTxLoop - } - - // Verify the bid transaction and all of its bundled transactions. - if err := l.VerifyTx(cacheCtx, tmpBidTx, bidInfo); err != nil { - l.Logger().Info( - "failed to verify auction bid tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove = append(txsToRemove, tmpBidTx) - continue selectBidTxLoop - } - - // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal - bundledTxBz := make([][]byte, len(bidInfo.Transactions)) - for index, rawRefTx := range bidInfo.Transactions { - sdkTx, err := l.WrapBundleTransaction(rawRefTx) - if err != nil { - l.Logger().Info( - "failed to wrap bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove = append(txsToRemove, tmpBidTx) - continue selectBidTxLoop - } - - sdkTxBz, _, err := utils.GetTxHashStr(l.TxEncoder(), sdkTx) - if err != nil { - l.Logger().Info( - "failed to get hash of bundled tx", - "tx_hash", hash, - "err", err, - ) - - txsToRemove = append(txsToRemove, tmpBidTx) - continue selectBidTxLoop - } - - // if the transaction is already in the (partial) block proposal, we skip it. - if proposal.Contains(sdkTxBz) { - l.Logger().Info( - "failed to select auction bid tx for lane; tx is already in proposal", - "tx_hash", hash, - ) - - continue selectBidTxLoop - } - - bundleTxBz := make([]byte, len(sdkTxBz)) - copy(bundleTxBz, sdkTxBz) - bundledTxBz[index] = sdkTxBz - } - - // At this point, both the bid transaction itself and all the bundled - // transactions are valid. So we select the bid transaction along with - // all the bundled transactions. We also mark these transactions as seen and - // update the total size selected thus far. - txs = append(txs, bidTxBz) - txs = append(txs, bundledTxBz...) - - // Write the cache context to the original context when we know we have a - // valid bundle. - write() - - break selectBidTxLoop + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop } - l.Logger().Info( - "failed to select auction bid tx for lane; tx size is too large", - "tx_size", bidTxSize, - "max_size", maxTxBytes, - ) + if txInfo.GasLimit > limit.MaxGasLimit { + l.Logger().Info( + "failed to select auction bid tx for lane; tx gas limit is too large", + "tx_gas_limit", txInfo.GasLimit, + "max_gas_limit", limit.MaxGasLimit, + "tx_hash", txInfo.Hash, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + // Build the partial proposal by selecting the bid transaction and all of + // its bundled transactions. + bidInfo, err := l.GetAuctionBidInfo(bidTx) + if err != nil { + l.Logger().Info( + "failed to get auction bid info", + "tx_hash", txInfo.Hash, + "err", err, + ) + + // Some transactions in the bundle may be malformed or invalid, so we + // remove the bid transaction and try the next top bid. + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + // Verify the bid transaction and all of its bundled transactions. + if err := l.VerifyTx(cacheCtx, bidTx, bidInfo); err != nil { + l.Logger().Info( + "failed to verify auction bid tx", + "tx_hash", txInfo.Hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + // store the bytes of each ref tx as sdk.Tx bytes in order to build a valid proposal + gasLimitSum := txInfo.GasLimit + bundledTxs := make([]sdk.Tx, len(bidInfo.Transactions)) + for index, bundledTxBz := range bidInfo.Transactions { + bundleTx, err := l.WrapBundleTransaction(bundledTxBz) + if err != nil { + l.Logger().Info( + "failed to wrap bundled tx", + "tx_hash", txInfo.Hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + bundledTxInfo, err := utils.GetTxInfo(l.TxEncoder(), bundleTx) + if err != nil { + l.Logger().Info( + "failed to get hash of bundled tx", + "tx_hash", txInfo.Hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(bundledTxInfo.Hash) { + l.Logger().Info( + "failed to select auction bid tx for lane; tx is already in proposal", + "tx_hash", bundledTxInfo.Hash, + ) + + continue selectBidTxLoop + } + + // If the bundled transaction is a bid transaction, we skip it. + if l.Match(ctx, bundleTx) { + l.Logger().Info( + "failed to select auction bid tx for lane; bundled tx is another bid transaction", + "tx_hash", bundledTxInfo.Hash, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + if gasLimitSum += bundledTxInfo.GasLimit; gasLimitSum > limit.MaxGasLimit { + l.Logger().Info( + "failed to select auction bid tx for lane; tx gas limit is too large", + "tx_gas_limit", gasLimitSum, + "max_gas_limit", limit.MaxGasLimit, + "tx_hash", txInfo.Hash, + ) + + txsToRemove = append(txsToRemove, bidTx) + continue selectBidTxLoop + } + + bundleTxBz := make([]byte, bundledTxInfo.Size) + copy(bundleTxBz, bundledTxInfo.TxBytes) + bundledTxs[index] = bundleTx + } + + // At this point, both the bid transaction itself and all the bundled + // transactions are valid. So we select the bid transaction along with + // all the bundled transactions. We also mark these transactions as seen and + // update the total size selected thus far. + txsToInclude = append(txsToInclude, bidTx) + txsToInclude = append(txsToInclude, bundledTxs...) + + // Write the cache context to the original context when we know we have a + // valid bundle. + write() + + break selectBidTxLoop } - return txs, txsToRemove, nil + return txsToInclude, txsToRemove, nil } } // ProcessLaneHandler will ensure that block proposals that include transactions from -// the mev lane are valid. +// the mev lane are valid. In particular, the invariant checks that we perform are: +// 1. The first transaction in the partial block proposal must be a bid transaction. +// 2. The bid transaction must be valid. +// 3. The bundled transactions must be valid. +// 4. The bundled transactions must match the transactions in the block proposal in the +// same order they were defined in the bid transaction. +// 5. The bundled transactions must not be bid transactions. func (l *MEVLane) ProcessLaneHandler() base.ProcessLaneHandler { - return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { - if len(txs) == 0 { - return txs, nil + return func(ctx sdk.Context, partialProposal []sdk.Tx) error { + if len(partialProposal) == 0 { + return nil } - bidTx := txs[0] + // If the first transaction does not match the lane, then we return an error. + bidTx := partialProposal[0] if !l.Match(ctx, bidTx) { - return txs, nil + return fmt.Errorf("expected first transaction in lane %s to be a bid transaction", l.Name()) } bidInfo, err := l.GetAuctionBidInfo(bidTx) if err != nil { - return nil, fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) + return fmt.Errorf("failed to get bid info from auction bid tx for lane %s: %w", l.Name(), err) } - if err := l.VerifyTx(ctx, bidTx, bidInfo); err != nil { - return nil, fmt.Errorf("invalid bid tx: %w", err) - } - - return txs[len(bidInfo.Transactions)+1:], nil - } -} - -// CheckOrderHandler ensures that if a bid transaction is present in a proposal, -// - it is the first transaction in the partial proposal -// - all of the bundled transactions are included after the bid transaction in the order -// they were included in the bid transaction. -// - there are no other bid transactions in the proposal -// - transactions from other lanes are not interleaved with transactions from the bid -// transaction. -func (l *MEVLane) CheckOrderHandler() base.CheckOrderHandler { - return func(ctx sdk.Context, txs []sdk.Tx) error { - if len(txs) == 0 { - return nil - } - - bidTx := txs[0] - - // If there is a bid transaction, it must be the first transaction in the block proposal. - if !l.Match(ctx, bidTx) { - for _, tx := range txs[1:] { - if l.Match(ctx, tx) { - return fmt.Errorf("misplaced bid transactions in lane %s", l.Name()) - } - } - - return nil - } - - bidInfo, err := l.GetAuctionBidInfo(bidTx) - if err != nil { - return fmt.Errorf("failed to get bid info for lane %s: %w", l.Name(), err) - } - - if len(txs) < len(bidInfo.Transactions)+1 { + // Check that all bundled transactions were included. + if len(bidInfo.Transactions)+1 != len(partialProposal) { return fmt.Errorf( - "invalid number of transactions in lane %s; expected at least %d, got %d", - l.Name(), + "expected %d transactions in lane %s but got %d", len(bidInfo.Transactions)+1, - len(txs), + l.Name(), + len(partialProposal), ) } - // Ensure that the order of transactions in the bundle is preserved. - for i, bundleTx := range txs[1 : len(bidInfo.Transactions)+1] { - if l.Match(ctx, bundleTx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) - } - - txBz, err := l.TxEncoder()(bundleTx) - if err != nil { - return fmt.Errorf("failed to encode bundled tx in lane %s: %w", l.Name(), err) - } - - if !bytes.Equal(txBz, bidInfo.Transactions[i]) { - return fmt.Errorf("invalid order of transactions in lane %s", l.Name()) - } + // Verify the top-level bid transaction. + if ctx, err = l.AnteVerifyTx(ctx, bidTx, false); err != nil { + return fmt.Errorf("invalid bid tx; failed to execute ante handler: %w", err) } - // Ensure that there are no more bid transactions in the block proposal. - for _, tx := range txs[len(bidInfo.Transactions)+1:] { - if l.Match(ctx, tx) { - return fmt.Errorf("multiple bid transactions in lane %s", l.Name()) + // Verify all of the bundled transactions. + for index, bundledTxBz := range bidInfo.Transactions { + bundledTx, err := l.WrapBundleTransaction(bundledTxBz) + if err != nil { + return fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) + } + + txBz, err := l.TxEncoder()(partialProposal[index+1]) + if err != nil { + return fmt.Errorf("invalid bid tx; failed to encode tx: %w", err) + } + + // Verify that the bundled transaction matches the transaction in the block proposal. + if !bytes.Equal(bundledTxBz, txBz) { + return fmt.Errorf("invalid bid tx; bundled tx does not match tx in block proposal") + } + + // Verify this is not another bid transaction. + if l.Match(ctx, bundledTx) { + return fmt.Errorf("invalid bid tx; bundled tx is another bid transaction") + } + + if ctx, err = l.AnteVerifyTx(ctx, bundledTx, false); err != nil { + return fmt.Errorf("invalid bid tx; failed to execute bundled transaction: %w", err) } } diff --git a/lanes/mev/factory_test.go b/lanes/mev/factory_test.go index f05d0f4..2957002 100644 --- a/lanes/mev/factory_test.go +++ b/lanes/mev/factory_test.go @@ -19,7 +19,7 @@ func (suite *MEVTestSuite) TestIsAuctionTx() { { "normal sdk tx", func() sdk.Tx { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 2, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 2, 0, 0) suite.Require().NoError(err) return tx }, @@ -125,7 +125,7 @@ func (suite *MEVTestSuite) TestGetTransactionSigners() { { "normal sdk tx", func() sdk.Tx { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 10, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 10, 0, 0) suite.Require().NoError(err) return tx @@ -186,7 +186,7 @@ func (suite *MEVTestSuite) TestWrapBundleTransaction() { { "normal sdk tx", func() (sdk.Tx, []byte) { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0, 0) suite.Require().NoError(err) bz, err := suite.encCfg.TxConfig.TxEncoder()(tx) @@ -241,7 +241,7 @@ func (suite *MEVTestSuite) TestGetBidder() { { "normal sdk tx", func() sdk.Tx { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0, 0) suite.Require().NoError(err) return tx @@ -316,7 +316,7 @@ func (suite *MEVTestSuite) TestGetBid() { { "normal sdk tx", func() sdk.Tx { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0, 0) suite.Require().NoError(err) return tx @@ -390,7 +390,7 @@ func (suite *MEVTestSuite) TestGetBundledTransactions() { { "normal sdk tx", func() (sdk.Tx, [][]byte) { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 0, 0) suite.Require().NoError(err) return tx, nil @@ -462,7 +462,7 @@ func (suite *MEVTestSuite) TestGetTimeout() { { "normal sdk tx", func() sdk.Tx { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 1) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, suite.accounts[0], 0, 1, 1, 0) suite.Require().NoError(err) return tx diff --git a/lanes/mev/lane.go b/lanes/mev/lane.go index 7b6d6da..63ed327 100644 --- a/lanes/mev/lane.go +++ b/lanes/mev/lane.go @@ -67,9 +67,6 @@ func NewMEVLane( // Set the process lane handler to the TOB one lane.SetProcessLaneHandler(lane.ProcessLaneHandler()) - // Set the check order handler to the TOB one - lane.SetCheckOrderHandler(lane.CheckOrderHandler()) - if err := lane.ValidateBasic(); err != nil { panic(err) } diff --git a/lanes/terminator/lane.go b/lanes/terminator/lane.go index c340008..c2ec64d 100644 --- a/lanes/terminator/lane.go +++ b/lanes/terminator/lane.go @@ -9,6 +9,7 @@ import ( sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/block-sdk/block" + "github.com/skip-mev/block-sdk/block/proposals" ) const ( @@ -40,18 +41,13 @@ type Terminator struct{} var _ block.Lane = (*Terminator)(nil) // PrepareLane is a no-op -func (t Terminator) PrepareLane(_ sdk.Context, proposal block.BlockProposal, _ int64, _ block.PrepareLanesHandler) (block.BlockProposal, error) { +func (t Terminator) PrepareLane(_ sdk.Context, proposal proposals.Proposal, _ block.PrepareLanesHandler) (proposals.Proposal, error) { return proposal, nil } -// ValidateLaneBasic is a no-op -func (t Terminator) CheckOrder(sdk.Context, []sdk.Tx) error { - return nil -} - // ProcessLane is a no-op -func (t Terminator) ProcessLane(ctx sdk.Context, _ []sdk.Tx, _ block.ProcessLanesHandler) (sdk.Context, error) { - return ctx, nil +func (t Terminator) ProcessLane(_ sdk.Context, p proposals.Proposal, _ [][]byte, _ block.ProcessLanesHandler) (proposals.Proposal, error) { + return p, nil } // GetMaxBlockSpace is a no-op diff --git a/proto/sdk/proposals/v1/types.proto b/proto/sdk/proposals/v1/types.proto new file mode 100644 index 0000000..b526c0b --- /dev/null +++ b/proto/sdk/proposals/v1/types.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package sdk.proposals.v1; + +option go_package = "github.com/skip-mev/block-sdk/block/proposals/types"; + +// ProposalInfo contains the metadata about a given proposal that was built by +// the block-sdk. This is used to verify and consilidate proposal data across +// the network. +message ProposalInfo { + // TxsByLane contains information about how each partial proposal + // was constructed by the block-sdk lanes. + map txs_by_lane = 1; + // MaxBlockSize corresponds to the upper bound on the size of the + // block that was used to construct this block proposal. + int64 max_block_size = 2; + // MaxGasLimit corresponds to the upper bound on the gas limit of the + // block that was used to construct this block proposal. + uint64 max_gas_limit = 3; + // BlockSize corresponds to the size of this block proposal. + int64 block_size = 4; + // GasLimit corresponds to the gas limit of this block proposal. + uint64 gas_limit = 5; +} \ No newline at end of file diff --git a/tests/app/ante.go b/tests/app/ante.go index 4436f33..a48d09c 100644 --- a/tests/app/ante.go +++ b/tests/app/ante.go @@ -5,12 +5,11 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/skip-mev/block-sdk/block" - "github.com/skip-mev/block-sdk/block/utils" auctionante "github.com/skip-mev/block-sdk/x/auction/ante" auctionkeeper "github.com/skip-mev/block-sdk/x/auction/keeper" ) -type POBHandlerOptions struct { +type BSDKHandlerOptions struct { BaseOptions ante.HandlerOptions Mempool block.Mempool MEVLane auctionante.MEVLane @@ -20,8 +19,9 @@ type POBHandlerOptions struct { FreeLane block.Lane } -// NewPOBAnteHandler wraps all of the default Cosmos SDK AnteDecorators with the POB AnteHandler. -func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler { +// NewBSDKAnteHandler wraps all of the default Cosmos SDK AnteDecorators with the custom +// block-sdk AnteHandlers. +func NewBSDKAnteHandler(options BSDKHandlerOptions) sdk.AnteHandler { if options.BaseOptions.AccountKeeper == nil { panic("account keeper is required for ante builder") } @@ -41,7 +41,7 @@ func NewPOBAnteHandler(options POBHandlerOptions) sdk.AnteHandler { ante.NewTxTimeoutHeightDecorator(), ante.NewValidateMemoDecorator(options.BaseOptions.AccountKeeper), ante.NewConsumeGasForTxSizeDecorator(options.BaseOptions.AccountKeeper), - utils.NewIgnoreDecorator( + block.NewIgnoreDecorator( ante.NewDeductFeeDecorator( options.BaseOptions.AccountKeeper, options.BaseOptions.BankKeeper, diff --git a/tests/app/app.go b/tests/app/app.go index 29a8039..b32d1f5 100644 --- a/tests/app/app.go +++ b/tests/app/app.go @@ -321,7 +321,7 @@ func New( SigGasConsumer: ante.DefaultSigVerificationGasConsumer, SignModeHandler: app.txConfig.SignModeHandler(), } - options := POBHandlerOptions{ + options := BSDKHandlerOptions{ BaseOptions: handlerOptions, auctionkeeper: app.auctionkeeper, TxDecoder: app.txConfig.TxDecoder(), @@ -330,7 +330,7 @@ func New( MEVLane: mevLane, Mempool: mempool, } - anteHandler := NewPOBAnteHandler(options) + anteHandler := NewBSDKAnteHandler(options) // Set the lane config on the lanes. for _, lane := range lanes { @@ -342,6 +342,7 @@ func New( proposalHandler := abci.NewProposalHandler( app.Logger(), app.TxConfig().TxDecoder(), + app.TxConfig().TxEncoder(), mempool, ) app.App.SetPrepareProposal(proposalHandler.PrepareProposalHandler()) diff --git a/tests/integration/block_sdk_suite.go b/tests/integration/block_sdk_suite.go index 88c278c..f1d9678 100644 --- a/tests/integration/block_sdk_suite.go +++ b/tests/integration/block_sdk_suite.go @@ -54,7 +54,7 @@ func (s *IntegrationTestSuite) WithDenom(denom string) *IntegrationTestSuite { s.denom = denom // update the bech32 prefixes - sdk.GetConfig().SetBech32PrefixForAccount(s.denom, s.denom+ sdk.PrefixPublic) + sdk.GetConfig().SetBech32PrefixForAccount(s.denom, s.denom+sdk.PrefixPublic) sdk.GetConfig().SetBech32PrefixForValidator(s.denom+sdk.PrefixValidator, s.denom+sdk.PrefixValidator+sdk.PrefixPublic) sdk.GetConfig().Seal() return s diff --git a/tests/integration/chain_setup.go b/tests/integration/chain_setup.go index 2fe1b64..7b10bf6 100644 --- a/tests/integration/chain_setup.go +++ b/tests/integration/chain_setup.go @@ -1,17 +1,18 @@ package integration import ( + "archive/tar" + "bytes" "context" "encoding/hex" "encoding/json" + "io" + "os" + "path" "strings" "testing" "time" - "os" - "io" - "path" - "archive/tar" - "bytes" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -39,7 +40,7 @@ import ( type KeyringOverride struct { keyringOptions keyring.Option - cdc codec.Codec + cdc codec.Codec } // ChainBuilderFromChainSpec creates an interchaintest chain builder factory given a ChainSpec @@ -189,8 +190,8 @@ func (s *IntegrationTestSuite) BroadcastTxs(ctx context.Context, chain *cosmos.C // will not block on the tx's inclusion in a block, otherwise this method will block on the tx's inclusion. The callback // function is called for each tx that is included in a block. func (s *IntegrationTestSuite) BroadcastTxsWithCallback( - ctx context.Context, - chain *cosmos.CosmosChain, + ctx context.Context, + chain *cosmos.CosmosChain, msgsPerUser []Tx, cb func(tx []byte, resp *rpctypes.ResultTx), ) [][]byte { @@ -204,6 +205,11 @@ func (s *IntegrationTestSuite) BroadcastTxsWithCallback( s.Require().True(len(chain.Nodes()) > 0) client := chain.Nodes()[0].Client + statusResp, err := client.Status(context.Background()) + s.Require().NoError(err) + + s.T().Logf("broadcasting transactions at latest height of %d", statusResp.SyncInfo.LatestBlockHeight) + for i, tx := range txs { // broadcast tx resp, err := client.BroadcastTxSync(ctx, tx) @@ -343,7 +349,7 @@ func Block(t *testing.T, chain *cosmos.CosmosChain, height int64) *rpctypes.Resu // 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, 100 * time.Millisecond, func() (bool, error) { + err := testutil.WaitForCondition(30*time.Second, 100*time.Millisecond, func() (bool, error) { pollHeight, err := chain.Height(context.Background()) if err != nil { return false, err @@ -357,13 +363,13 @@ func WaitForHeight(t *testing.T, chain *cosmos.CosmosChain, height uint64) { 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])) + require.Equal(t, bidTxHash, TxHash(block.Block.Data.Txs[offset+1])) offset += 1 } // verify the txs in sequence for i, tx := range txs { - require.Equal(t, TxHash(tx), TxHash(block.Block.Data.Txs[i+offset])) + require.Equal(t, TxHash(tx), TxHash(block.Block.Data.Txs[i+offset+1])) } } @@ -403,7 +409,7 @@ func (s *IntegrationTestSuite) setupBroadcaster() { } // sniped from here: https://github.com/strangelove-ventures/interchaintest ref: 9341b001214d26be420f1ca1ab0f15bad17faee6 -func (s *IntegrationTestSuite) keyringDirFromNode() (string) { +func (s *IntegrationTestSuite) keyringDirFromNode() string { node := s.chain.(*cosmos.CosmosChain).Nodes()[0] // create a temp-dir diff --git a/testutils/mocks.go b/testutils/mocks.go index 515fd40..9aa36dc 100644 --- a/testutils/mocks.go +++ b/testutils/mocks.go @@ -1,4 +1,4 @@ -package test +package testutils import ( "context" diff --git a/testutils/utils.go b/testutils/utils.go index 2676337..506bf62 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -1,4 +1,4 @@ -package test +package testutils import ( "math/rand" @@ -122,7 +122,7 @@ func CreateFreeTx(txCfg client.TxConfig, account Account, nonce, timeout uint64, return CreateTx(txCfg, account, nonce, timeout, msgs, fees...) } -func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64, fees ...sdk.Coin) (authsigning.Tx, error) { +func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64, gasLimit uint64, fees ...sdk.Coin) (authsigning.Tx, error) { msgs := make([]sdk.Msg, numberMsgs) for i := 0; i < int(numberMsgs); i++ { msgs[i] = &banktypes.MsgSend{ @@ -152,11 +152,13 @@ func CreateRandomTx(txCfg client.TxConfig, account Account, nonce, numberMsgs, t txBuilder.SetFeeAmount(fees) + txBuilder.SetGasLimit(gasLimit) + return txBuilder.GetTx(), nil } -func CreateRandomTxBz(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout uint64) ([]byte, error) { - tx, err := CreateRandomTx(txCfg, account, nonce, numberMsgs, timeout) +func CreateRandomTxBz(txCfg client.TxConfig, account Account, nonce, numberMsgs, timeout, gasLimit uint64) ([]byte, error) { + tx, err := CreateRandomTx(txCfg, account, nonce, numberMsgs, timeout, gasLimit) if err != nil { return nil, err } @@ -194,7 +196,7 @@ func CreateTxWithSigners(txCfg client.TxConfig, nonce, timeout uint64, signers [ return txBuilder.GetTx(), nil } -func CreateAuctionTx(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, timeout uint64, signers []Account) (authsigning.Tx, []authsigning.Tx, error) { +func CreateAuctionTx(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, timeout uint64, signers []Account, gasLimit uint64) (authsigning.Tx, []authsigning.Tx, error) { bidMsg := &auctiontypes.MsgAuctionBid{ Bidder: bidder.Address.String(), Bid: bid, @@ -238,6 +240,8 @@ func CreateAuctionTx(txCfg client.TxConfig, bidder Account, bid sdk.Coin, nonce, txBuilder.SetTimeoutHeight(timeout) + txBuilder.SetGasLimit(gasLimit) + return txBuilder.GetTx(), txs, nil } diff --git a/x/auction/keeper/auction_test.go b/x/auction/keeper/auction_test.go index 4ac472d..b100401 100644 --- a/x/auction/keeper/auction_test.go +++ b/x/auction/keeper/auction_test.go @@ -184,7 +184,7 @@ func (suite *KeeperTestSuite) TestValidateBidInfo() { // Create the bundle of transactions ordered by accounts bundle := make([][]byte, 0) for _, acc := range accounts { - tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, acc, 0, 1, 100) + tx, err := testutils.CreateRandomTx(suite.encCfg.TxConfig, acc, 0, 1, 100, 0) suite.Require().NoError(err) txBz, err := suite.encCfg.TxConfig.TxEncoder()(tx)