Merge branch 'main' into terpay/rename-to-block-sdk

This commit is contained in:
David Terpay 2023-08-15 16:38:51 -04:00
commit a9022fd3ca
No known key found for this signature in database
GPG Key ID: 627EFB00DADF0CD1
12 changed files with 1476 additions and 4 deletions

View File

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

67
block/lane_abci.go Normal file
View File

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

198
block/lane_constructor.go Normal file
View File

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

155
block/lane_handlers.go Normal file
View File

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

71
block/lane_interface.go Normal file
View File

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

159
block/lane_mempool.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -189,7 +189,6 @@ func BroadcastTxs(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain,
require.NoError(t, err)
} else {
require.Error(t, err)
}
}

View File

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