diff --git a/abci/abci.go b/abci/abci.go index 2bb6b16..1e203af 100644 --- a/abci/abci.go +++ b/abci/abci.go @@ -131,7 +131,7 @@ func ChainPrepareLanes(chain ...block.Lane) block.PrepareLanesHandler { // 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 handler any panics or errors that occur during the preparation + // 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. diff --git a/block/lane_abci.go b/block/lane_abci.go new file mode 100644 index 0000000..a70b102 --- /dev/null +++ b/block/lane_abci.go @@ -0,0 +1,67 @@ +package blockbuster + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster/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. +func (l *LaneConstructor) PrepareLane( + ctx sdk.Context, + proposal BlockProposal, + maxTxBytes int64, + next PrepareLanesHandler, +) (BlockProposal, error) { + txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes) + if err != nil { + return proposal, err + } + + // Remove all transactions that were invalid during the creation of the partial proposal. + if err := utils.RemoveTxsFromLane(txsToRemove, l); err != nil { + l.Logger().Error( + "failed to remove transactions from lane", + "lane", l.Name(), + "err", err, + ) + } + + // Update the proposal with the selected transactions. + if err := proposal.UpdateProposal(l, txs); err != nil { + return proposal, err + } + + 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 *LaneConstructor) 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 *LaneConstructor) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next ProcessLanesHandler) (sdk.Context, error) { + remainingTxs, err := l.processLaneHandler(ctx, txs) + if err != nil { + return ctx, err + } + + return next(ctx, remainingTxs) +} + +// AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of +// of the antehandler chain. +func (l *LaneConstructor) AnteVerifyTx(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + if l.cfg.AnteHandler != nil { + return l.cfg.AnteHandler(ctx, tx, simulate) + } + + return ctx, nil +} diff --git a/block/lane_constructor.go b/block/lane_constructor.go new file mode 100644 index 0000000..7107814 --- /dev/null +++ b/block/lane_constructor.go @@ -0,0 +1,198 @@ +package blockbuster + +import ( + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ Lane = (*LaneConstructor)(nil) + +// LaneConstructor is a generic implementation of a lane. It is meant to be used +// as a base for other lanes to be built on top of. It provides a default +// implementation of the MatchHandler, PrepareLaneHandler, ProcessLaneHandler, +// and CheckOrderHandler. To extend this lane, you must either utilize the default +// handlers or construct your own that you pass into the constructor/setters. +type LaneConstructor struct { + // cfg stores functionality required to encode/decode transactions, maintains how + // many transactions are allowed in this lane's mempool, and the amount of block + // space this lane is allowed to consume. + cfg LaneConfig + + // laneName is the name of the lane. + laneName string + + // LaneMempool is the mempool that is responsible for storing transactions + // that are waiting to be processed. + LaneMempool + + // matchHandler is the function that determines whether or not a transaction + // should be processed by this lane. + matchHandler MatchHandler + + // prepareLaneHandler is the function that is called when a new proposal is being + // 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. + processLaneHandler ProcessLaneHandler +} + +// NewLaneConstructor returns a new lane constructor. When creating this lane, the type +// of the lane must be specified. The type of the lane is directly associated with the +// type of the mempool that is used to store transactions that are waiting to be processed. +func NewLaneConstructor( + cfg LaneConfig, + laneName string, + laneMempool LaneMempool, + matchHandlerFn MatchHandler, +) *LaneConstructor { + lane := &LaneConstructor{ + cfg: cfg, + laneName: laneName, + LaneMempool: laneMempool, + matchHandler: matchHandlerFn, + } + + if err := lane.ValidateBasic(); err != nil { + panic(err) + } + + return lane +} + +// ValidateBasic ensures that the lane was constructed properly. In the case that +// the lane was not constructed with proper handlers, default handlers are set. +func (l *LaneConstructor) ValidateBasic() error { + if err := l.cfg.ValidateBasic(); err != nil { + return err + } + + if l.laneName == "" { + return fmt.Errorf("lane name cannot be empty") + } + + if l.LaneMempool == nil { + return fmt.Errorf("lane mempool cannot be nil") + } + + if l.matchHandler == nil { + return fmt.Errorf("match handler cannot be nil") + } + + if l.prepareLaneHandler == nil { + l.prepareLaneHandler = l.DefaultPrepareLaneHandler() + } + + if l.processLaneHandler == nil { + l.processLaneHandler = l.DefaultProcessLaneHandler() + } + + if l.checkOrderHandler == nil { + l.checkOrderHandler = l.DefaultCheckOrderHandler() + } + + return nil +} + +// SetPrepareLaneHandler sets the prepare lane handler for the lane. This handler +// is called when a new proposal is being requested and the lane needs to submit +// transactions it wants included in the block. +func (l *LaneConstructor) SetPrepareLaneHandler(prepareLaneHandler PrepareLaneHandler) { + if prepareLaneHandler == nil { + panic("prepare lane handler cannot be nil") + } + + l.prepareLaneHandler = prepareLaneHandler +} + +// SetProcessLaneHandler sets the process lane 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 are valid respecting the verification +// logic of the lane. +func (l *LaneConstructor) SetProcessLaneHandler(processLaneHandler ProcessLaneHandler) { + if processLaneHandler == nil { + panic("process lane handler cannot be nil") + } + + 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 *LaneConstructor) 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 +// list, it returns false. +func (l *LaneConstructor) Match(ctx sdk.Context, tx sdk.Tx) bool { + return l.matchHandler(ctx, tx) && !l.CheckIgnoreList(ctx, tx) +} + +// CheckIgnoreList returns true if the transaction is on the ignore list. The ignore +// list is utilized to prevent transactions that should be considered in other lanes +// from being considered from this lane. +func (l *LaneConstructor) CheckIgnoreList(ctx sdk.Context, tx sdk.Tx) bool { + for _, lane := range l.cfg.IgnoreList { + if lane.Match(ctx, tx) { + return true + } + } + + return false +} + +// Name returns the name of the lane. +func (l *LaneConstructor) Name() string { + return l.laneName +} + +// SetIgnoreList sets the ignore list for the lane. The ignore list is a list +// of lanes that the lane should ignore when processing transactions. +func (l *LaneConstructor) SetIgnoreList(lanes []Lane) { + l.cfg.IgnoreList = lanes +} + +// SetAnteHandler sets the ante handler for the lane. +func (l *LaneConstructor) SetAnteHandler(anteHandler sdk.AnteHandler) { + l.cfg.AnteHandler = anteHandler +} + +// Logger returns the logger for the lane. +func (l *LaneConstructor) Logger() log.Logger { + return l.cfg.Logger +} + +// TxDecoder returns the tx decoder for the lane. +func (l *LaneConstructor) TxDecoder() sdk.TxDecoder { + return l.cfg.TxDecoder +} + +// TxEncoder returns the tx encoder for the lane. +func (l *LaneConstructor) TxEncoder() sdk.TxEncoder { + return l.cfg.TxEncoder +} + +// GetMaxBlockSpace returns the maximum amount of block space that the lane is +// allowed to consume as a percentage of the total block space. +func (l *LaneConstructor) GetMaxBlockSpace() math.LegacyDec { + return l.cfg.MaxBlockSpace +} diff --git a/block/lane_handlers.go b/block/lane_handlers.go new file mode 100644 index 0000000..0a87a5d --- /dev/null +++ b/block/lane_handlers.go @@ -0,0 +1,155 @@ +package blockbuster + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster/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 +// lane has been reached. Additionally, any transactions that are invalid will be returned. +func (l *LaneConstructor) DefaultPrepareLaneHandler() PrepareLaneHandler { + return func(ctx sdk.Context, proposal BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) { + var ( + totalSize int64 + txs [][]byte + txsToRemove []sdk.Tx + ) + + // Select all transactions in the mempool that are valid and not already in the + // partial proposal. + for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { + tx := iterator.Tx() + + txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx) + if err != nil { + l.Logger().Info("failed to get hash of tx", "err", err) + + txsToRemove = append(txsToRemove, tx) + continue + } + + // Double check that the transaction belongs to this lane. + if !l.Match(ctx, tx) { + l.Logger().Info( + "failed to select tx for lane; tx does not belong to lane", + "tx_hash", hash, + "lane", l.Name(), + ) + + txsToRemove = append(txsToRemove, tx) + continue + } + + // if the transaction is already in the (partial) block proposal, we skip it. + if proposal.Contains(txBytes) { + l.Logger().Info( + "failed to select tx for lane; tx is already in proposal", + "tx_hash", hash, + "lane", l.Name(), + ) + + continue + } + + // 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 { + l.Logger().Info( + "tx bytes above the maximum allowed", + "lane", l.Name(), + "tx_size", txSize, + "total_size", totalSize, + "max_tx_bytes", maxTxBytes, + "tx_hash", hash, + ) + + break + } + + // Verify the transaction. + if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil { + l.Logger().Info( + "failed to verify tx", + "tx_hash", hash, + "err", err, + ) + + txsToRemove = append(txsToRemove, tx) + continue + } + + totalSize += txSize + txs = append(txs, txBytes) + } + + return txs, 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. +func (l *LaneConstructor) DefaultProcessLaneHandler() ProcessLaneHandler { + return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) { + var err 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 *LaneConstructor) 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 + } + } + + return nil + } +} + +// DefaultMatchHandler returns a default implementation of the MatchHandler. It matches all +// transactions. +func DefaultMatchHandler() MatchHandler { + return func(ctx sdk.Context, tx sdk.Tx) bool { + return true + } +} diff --git a/block/lane_interface.go b/block/lane_interface.go new file mode 100644 index 0000000..59ee35e --- /dev/null +++ b/block/lane_interface.go @@ -0,0 +1,71 @@ +package blockbuster + +import ( + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" +) + +// 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 +type LaneMempool interface { + sdkmempool.Mempool + + // Compare determines the relative priority of two transactions belonging in the same lane. Compare + // will return -1 if this transaction has a lower priority than the other transaction, 0 if they have + // the same priority, and 1 if this transaction has a higher priority than the other transaction. + Compare(ctx sdk.Context, this, other sdk.Tx) int + + // Contains returns true if the transaction is contained in the mempool. + Contains(tx sdk.Tx) bool +} + +// 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 +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( + ctx sdk.Context, + proposal BlockProposal, + maxTxBytes int64, + next PrepareLanesHandler, + ) (BlockProposal, 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) + + // GetMaxBlockSpace returns the max block space for the lane as a relative percentage. + GetMaxBlockSpace() math.LegacyDec + + // Logger returns the lane's logger. + Logger() log.Logger + + // Name returns the name of the lane. + Name() string + + // SetAnteHandler sets the lane's antehandler. + SetAnteHandler(antehander sdk.AnteHandler) + + // SetIgnoreList sets the lanes that should be ignored by this lane. + SetIgnoreList(ignoreList []Lane) + + // Match determines if a transaction belongs to this lane. + Match(ctx sdk.Context, tx sdk.Tx) bool +} diff --git a/block/lane_mempool.go b/block/lane_mempool.go new file mode 100644 index 0000000..e102ee8 --- /dev/null +++ b/block/lane_mempool.go @@ -0,0 +1,159 @@ +package blockbuster + +import ( + "context" + "errors" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + "github.com/skip-mev/pob/blockbuster/utils" +) + +type ( + // ConstructorMempool defines a mempool that orders transactions based on the + // txPriority. The mempool is a wrapper on top of the SDK's Priority Nonce mempool. + // It include's additional helper functions that allow users to determine if a + // transaction is already in the mempool and to compare the priority of two + // transactions. + ConstructorMempool[C comparable] struct { + // index defines an index of transactions. + index sdkmempool.Mempool + + // txPriority defines the transaction priority function. It is used to + // retrieve the priority of a given transaction and to compare the priority + // of two transactions. The index utilizes this struct to order transactions + // in the mempool. + txPriority TxPriority[C] + + // txEncoder defines the sdk.Tx encoder that allows us to encode transactions + // to bytes. + txEncoder sdk.TxEncoder + + // txCache is a map of all transactions in the mempool. It is used + // to quickly check if a transaction is already in the mempool. + txCache map[string]struct{} + } +) + +// DefaultTxPriority returns a default implementation of the TxPriority. It prioritizes +// transactions by their fee. +func DefaultTxPriority() TxPriority[string] { + return TxPriority[string]{ + GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return "" + } + + return feeTx.GetFee().String() + }, + Compare: func(a, b string) int { + aCoins, _ := sdk.ParseCoinsNormalized(a) + bCoins, _ := sdk.ParseCoinsNormalized(b) + + switch { + case aCoins == nil && bCoins == nil: + return 0 + + case aCoins == nil: + return -1 + + case bCoins == nil: + return 1 + + default: + switch { + case aCoins.IsAllGT(bCoins): + return 1 + + case aCoins.IsAllLT(bCoins): + return -1 + + default: + return 0 + } + } + }, + MinValue: "", + } +} + +// NewConstructorMempool returns a new ConstructorMempool. +func NewConstructorMempool[C comparable](txPriority TxPriority[C], txEncoder sdk.TxEncoder, maxTx int) *ConstructorMempool[C] { + return &ConstructorMempool[C]{ + index: NewPriorityMempool( + PriorityNonceMempoolConfig[C]{ + TxPriority: txPriority, + MaxTx: maxTx, + }, + ), + txPriority: txPriority, + txEncoder: txEncoder, + txCache: make(map[string]struct{}), + } +} + +// Insert inserts a transaction into the mempool. +func (cm *ConstructorMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error { + if err := cm.index.Insert(ctx, tx); err != nil { + return fmt.Errorf("failed to insert tx into auction index: %w", err) + } + + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + cm.Remove(tx) + return err + } + + cm.txCache[txHashStr] = struct{}{} + + return nil +} + +// Remove removes a transaction from the mempool. +func (cm *ConstructorMempool[C]) Remove(tx sdk.Tx) error { + if err := cm.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) { + return fmt.Errorf("failed to remove transaction from the mempool: %w", err) + } + + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + return fmt.Errorf("failed to get tx hash string: %w", err) + } + + delete(cm.txCache, txHashStr) + + return nil +} + +// Select returns an iterator of all transactions in the mempool. NOTE: If you +// remove a transaction from the mempool while iterating over the transactions, +// the iterator will not be aware of the removal and will continue to iterate +// over the removed transaction. Be sure to reset the iterator if you remove a transaction. +func (cm *ConstructorMempool[C]) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { + return cm.index.Select(ctx, txs) +} + +// CountTx returns the number of transactions in the mempool. +func (cm *ConstructorMempool[C]) CountTx() int { + return cm.index.CountTx() +} + +// Contains returns true if the transaction is contained in the mempool. +func (cm *ConstructorMempool[C]) Contains(tx sdk.Tx) bool { + _, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx) + if err != nil { + return false + } + + _, ok := cm.txCache[txHashStr] + return ok +} + +// Compare determines the relative priority of two transactions belonging in the same lane. +func (cm *ConstructorMempool[C]) Compare(ctx sdk.Context, this sdk.Tx, other sdk.Tx) int { + firstPriority := cm.txPriority.GetTxPriority(ctx, this) + secondPriority := cm.txPriority.GetTxPriority(ctx, other) + return cm.txPriority.Compare(firstPriority, secondPriority) +} diff --git a/block/lanes/base/abci_test.go b/block/lanes/base/abci_test.go new file mode 100644 index 0000000..d5c054d --- /dev/null +++ b/block/lanes/base/abci_test.go @@ -0,0 +1,547 @@ +package base_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "cosmossdk.io/log" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + "github.com/skip-mev/pob/blockbuster/lanes/base" + "github.com/skip-mev/pob/blockbuster/utils/mocks" + testutils "github.com/skip-mev/pob/testutils" +) + +func (s *BaseTestSuite) TestPrepareLane() { + s.Run("should not build a proposal when amount configured to lane is too small", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 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 + 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{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + }) + + s.Run("should not build a proposal when box space configured to lane is too small", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 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) + + // 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{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().Error(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + }) + + 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 + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 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 + 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{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.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.Run("should not build a proposal with a that fails verify tx", func() { + // Create a basic transaction that should not in the proposal + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 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: false, + } + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), 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{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.NoOpPrepareLanesHandler()) + s.Require().NoError(err) + + // Ensure the proposal is empty + s.Require().Equal(0, proposal.GetNumTxs()) + s.Require().Equal(int64(0), proposal.GetTotalTxBytes()) + + // Ensure the transaction is removed from the lane + s.Require().False(lane.Contains(tx)) + s.Require().Equal(0, lane.CountTx()) + }) + + 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, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 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) + + // 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) + + maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.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.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, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 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) + + // 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) + + maxTxBytes := int64(len(txBz1)) + int64(len(txBz2)) + proposal, err := lane.PrepareLane(sdk.Context{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.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.Run("should include tx that fits in proposal when other does not", func() { + // Create a basic transaction that should not in the proposal + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + 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, + 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) + + // Insert the transaction into the lane + s.Require().NoError(lane.Insert(sdk.Context{}.WithPriority(10), tx1)) + s.Require().NoError(lane.Insert(sdk.Context{}.WithPriority(5), tx2)) + + txBz1, err := s.encodingConfig.TxConfig.TxEncoder()(tx1) + s.Require().NoError(err) + + 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{}, blockbuster.NewProposal(maxTxBytes), maxTxBytes, blockbuster.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()) + }) +} + +func (s *BaseTestSuite) TestProcessLane() { + s.Run("should accept a proposal with valid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.NoOpProcessLanesHandler()) + s.Require().NoError(err) + }) + + s.Run("should not accept a proposal with invalid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: false, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.NoOpProcessLanesHandler()) + s.Require().Error(err) + }) + + s.Run("should not accept a proposal with some invalid transactions", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + tx3, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 1, + 0, + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + tx3, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: false, + tx3: true, + }) + + _, err = lane.ProcessLane(sdk.Context{}, proposal, blockbuster.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, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + s.Require().NoError(lane.CheckOrder(sdk.Context{}, proposal)) + }) + + s.Run("should not accept a proposal with transactions that are not in the correct order", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(2)), + ) + s.Require().NoError(err) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), map[sdk.Tx]bool{ + tx1: true, + tx2: true, + }) + s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + }) + + s.Run("should not accept a proposal where transactions are out of order relative to other lanes", func() { + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 2, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 1, + 0, + 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) + + lane := s.initLane(math.LegacyMustNewDecFromStr("1"), nil) + lane.SetIgnoreList([]blockbuster.Lane{mocklane}) + + proposal := []sdk.Tx{ + tx1, + tx2, + } + + s.Require().Error(lane.CheckOrder(sdk.Context{}, proposal)) + }) +} + +func (s *BaseTestSuite) initLane( + maxBlockSpace math.LegacyDec, + expectedExecution map[sdk.Tx]bool, +) *base.DefaultLane { + config := blockbuster.NewBaseLaneConfig( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + s.encodingConfig.TxConfig.TxDecoder(), + s.setUpAnteHandler(expectedExecution), + maxBlockSpace, + ) + + return base.NewDefaultLane(config) +} + +func (s *BaseTestSuite) 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 +} diff --git a/block/lanes/base/base_test.go b/block/lanes/base/base_test.go new file mode 100644 index 0000000..7852dac --- /dev/null +++ b/block/lanes/base/base_test.go @@ -0,0 +1,32 @@ +package base_test + +import ( + "math/rand" + "testing" + + testutils "github.com/skip-mev/pob/testutils" + "github.com/stretchr/testify/suite" +) + +type BaseTestSuite struct { + suite.Suite + + encodingConfig testutils.EncodingConfig + random *rand.Rand + accounts []testutils.Account + gasTokenDenom string +} + +func TestBaseTestSuite(t *testing.T) { + suite.Run(t, new(BaseTestSuite)) +} + +func (s *BaseTestSuite) SetupTest() { + // Set up basic TX encoding config. + s.encodingConfig = testutils.CreateTestEncodingConfig() + + // Create a few random accounts + s.random = rand.New(rand.NewSource(1)) + s.accounts = testutils.RandomAccounts(s.random, 5) + s.gasTokenDenom = "stake" +} diff --git a/block/lanes/base/mempool_test.go b/block/lanes/base/mempool_test.go new file mode 100644 index 0000000..4f12e73 --- /dev/null +++ b/block/lanes/base/mempool_test.go @@ -0,0 +1,240 @@ +package base_test + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/pob/blockbuster" + testutils "github.com/skip-mev/pob/testutils" +) + +func (s *BaseTestSuite) TestGetTxPriority() { + txPriority := blockbuster.DefaultTxPriority() + + s.Run("should be able to get the priority off a normal transaction with fees", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal(sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String(), priority) + }) + + s.Run("should not get a priority when the transaction does not have a fee", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal("", priority) + }) + + s.Run("should get a priority when the gas token is different", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin("random", math.NewInt(100)), + ) + s.Require().NoError(err) + + priority := txPriority.GetTxPriority(sdk.Context{}, tx) + s.Require().Equal(sdk.NewCoin("random", math.NewInt(100)).String(), priority) + }) +} + +func (s *BaseTestSuite) TestCompareTxPriority() { + txPriority := blockbuster.DefaultTxPriority() + + s.Run("should return 0 when both priorities are nil", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(0)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(0)).String() + s.Require().Equal(0, txPriority.Compare(a, b)) + }) + + s.Run("should return 1 when the first priority is greater", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)).String() + s.Require().Equal(1, txPriority.Compare(a, b)) + }) + + s.Run("should return -1 when the second priority is greater", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(1)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + s.Require().Equal(-1, txPriority.Compare(a, b)) + }) + + s.Run("should return 0 when both priorities are equal", func() { + a := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + b := sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)).String() + s.Require().Equal(0, txPriority.Compare(a, b)) + }) +} + +func (s *BaseTestSuite) TestInsert() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + s.Run("should be able to insert a transaction", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + }) + + s.Run("cannot insert more transactions than the max", func() { + for i := 0; i < 3; i++ { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + uint64(i), + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(int64(100*i))), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + } + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 10, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().Error(err) + s.Require().False(mempool.Contains(tx)) + }) +} + +func (s *BaseTestSuite) TestRemove() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + s.Run("should be able to remove a transaction", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + err = mempool.Insert(sdk.Context{}, tx) + s.Require().NoError(err) + s.Require().True(mempool.Contains(tx)) + + mempool.Remove(tx) + s.Require().False(mempool.Contains(tx)) + }) + + s.Run("should not error when removing a transaction that does not exist", func() { + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + mempool.Remove(tx) + }) +} + +func (s *BaseTestSuite) TestSelect() { + s.Run("should be able to select transactions in the correct order", func() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + tx2, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(200)), + ) + s.Require().NoError(err) + + // Insert the transactions into the mempool + s.Require().NoError(mempool.Insert(sdk.Context{}, tx1)) + s.Require().NoError(mempool.Insert(sdk.Context{}, tx2)) + s.Require().Equal(2, mempool.CountTx()) + + // Check that the transactions are in the correct order + iterator := mempool.Select(sdk.Context{}, nil) + s.Require().NotNil(iterator) + s.Require().Equal(tx2, iterator.Tx()) + + // Check the second transaction + iterator = iterator.Next() + s.Require().NotNil(iterator) + s.Require().Equal(tx1, iterator.Tx()) + }) + + s.Run("should be able to select a single transaction", func() { + mempool := blockbuster.NewConstructorMempool[string](blockbuster.DefaultTxPriority(), s.encodingConfig.TxConfig.TxEncoder(), 3) + + tx1, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 0, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(100)), + ) + s.Require().NoError(err) + + // Insert the transactions into the mempool + s.Require().NoError(mempool.Insert(sdk.Context{}, tx1)) + s.Require().Equal(1, mempool.CountTx()) + + // Check that the transactions are in the correct order + iterator := mempool.Select(sdk.Context{}, nil) + s.Require().NotNil(iterator) + s.Require().Equal(tx1, iterator.Tx()) + + iterator = iterator.Next() + s.Require().Nil(iterator) + }) +} diff --git a/lanes/terminator/lane.go b/lanes/terminator/lane.go index 3fce765..c828792 100644 --- a/lanes/terminator/lane.go +++ b/lanes/terminator/lane.go @@ -14,6 +14,10 @@ const ( LaneName = "Terminator" ) +const ( + LaneName = "Terminator" +) + // Terminator Lane will get added to the chain to simplify chaining code so that we // don't need to check if next == nil further up the chain // diff --git a/tests/integration/chain_setup.go b/tests/integration/chain_setup.go index 5c94024..ba753a9 100644 --- a/tests/integration/chain_setup.go +++ b/tests/integration/chain_setup.go @@ -189,7 +189,6 @@ func BroadcastTxs(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, require.NoError(t, err) } else { require.Error(t, err) - } } diff --git a/tests/integration/pob_integration_test.go b/tests/integration/pob_integration_test.go index 4766666..e5b775d 100644 --- a/tests/integration/pob_integration_test.go +++ b/tests/integration/pob_integration_test.go @@ -5,9 +5,9 @@ import ( "testing" testutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/skip-mev/pob/tests/integration" buildertypes "github.com/skip-mev/pob/x/builder/types" "github.com/strangelove-ventures/interchaintest/v7" - "github.com/skip-mev/pob/tests/integration" "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v7/ibc" "github.com/stretchr/testify/suite" @@ -16,7 +16,7 @@ import ( var ( // config params numValidators = 4 - numFullNodes = 0 + numFullNodes = 0 denom = "stake" image = ibc.DockerImage{