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
This commit is contained in:
David Terpay 2023-09-28 11:10:13 -04:00 committed by GitHub
parent 3abfde4f34
commit b9d6761776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 3705 additions and 1099 deletions

View File

@ -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 ##

View File

@ -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
}

File diff suppressed because it is too large Load Diff

93
abci/utils.go Normal file
View File

@ -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:]))
}
}

212
abci/utils_test.go Normal file
View File

@ -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,
},
},
)
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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")
)

113
block/proposals/update.go Normal file
View File

@ -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
}

39
block/proposals/utils.go Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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)
}
})
}
}

View File

@ -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))
}

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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<string, uint64> 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;
}

View File

@ -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,

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
package test
package testutils
import (
"context"

View File

@ -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
}

View File

@ -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)