diff --git a/app/ante.go b/app/ante.go index 24b1b5911..4d5466268 100644 --- a/app/ante.go +++ b/app/ante.go @@ -11,6 +11,7 @@ import ( authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/bandprotocol/chain/v3/app/mempool" bandtsskeeper "github.com/bandprotocol/chain/v3/x/bandtss/keeper" feedskeeper "github.com/bandprotocol/chain/v3/x/feeds/keeper" "github.com/bandprotocol/chain/v3/x/globalfee/feechecker" @@ -23,15 +24,16 @@ import ( // channel keeper. type HandlerOptions struct { ante.HandlerOptions - Cdc codec.Codec - AuthzKeeper *authzkeeper.Keeper - OracleKeeper *oraclekeeper.Keeper - IBCKeeper *ibckeeper.Keeper - StakingKeeper *stakingkeeper.Keeper - GlobalfeeKeeper *globalfeekeeper.Keeper - TSSKeeper *tsskeeper.Keeper - BandtssKeeper *bandtsskeeper.Keeper - FeedsKeeper *feedskeeper.Keeper + Cdc codec.Codec + AuthzKeeper *authzkeeper.Keeper + OracleKeeper *oraclekeeper.Keeper + IBCKeeper *ibckeeper.Keeper + StakingKeeper *stakingkeeper.Keeper + GlobalfeeKeeper *globalfeekeeper.Keeper + TSSKeeper *tsskeeper.Keeper + BandtssKeeper *bandtsskeeper.Keeper + FeedsKeeper *feedskeeper.Keeper + IgnoreDecoratorMatchFns []mempool.TxMatchFn } func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { @@ -79,14 +81,8 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { if options.TxFeeChecker == nil { feeChecker := feechecker.NewFeeChecker( - options.Cdc, - options.AuthzKeeper, - options.OracleKeeper, options.GlobalfeeKeeper, options.StakingKeeper, - options.TSSKeeper, - options.BandtssKeeper, - options.FeedsKeeper, ) options.TxFeeChecker = feeChecker.CheckTxFee } @@ -98,11 +94,14 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ante.NewTxTimeoutHeightDecorator(), ante.NewValidateMemoDecorator(options.AccountKeeper), ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator( - options.AccountKeeper, - options.BankKeeper, - options.FeegrantKeeper, - options.TxFeeChecker, + NewIgnoreDecorator( + ante.NewDeductFeeDecorator( + options.AccountKeeper, + options.BankKeeper, + options.FeegrantKeeper, + options.TxFeeChecker, + ), + options.IgnoreDecoratorMatchFns..., ), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewSetPubKeyDecorator(options.AccountKeeper), @@ -115,3 +114,41 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { return sdk.ChainAnteDecorators(anteDecorators...), nil } + +// IgnoreDecorator is an AnteDecorator that wraps an existing AnteDecorator. It allows +// for the AnteDecorator to be ignored for specified lanes. +type IgnoreDecorator struct { + decorator sdk.AnteDecorator + matchFns []mempool.TxMatchFn +} + +// NewIgnoreDecorator returns a new IgnoreDecorator instance. +func NewIgnoreDecorator(decorator sdk.AnteDecorator, matchFns ...mempool.TxMatchFn) *IgnoreDecorator { + return &IgnoreDecorator{ + decorator: decorator, + matchFns: matchFns, + } +} + +// NewIgnoreDecorator is a wrapper that implements the sdk.AnteDecorator interface, +// providing two execution paths for processing transactions: +// - If a transaction matches one of the designated bypass lanes, it is forwarded +// directly to the next AnteHandler. +// - Otherwise, the transaction is processed using the embedded decorator’s AnteHandler. +func (ig IgnoreDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + // IgnoreDecorator is only used for check tx and re-check tx. + if !ctx.IsCheckTx() && !ctx.IsReCheckTx() { + return ig.decorator.AnteHandle(ctx, tx, simulate, next) + } + + cacheCtx, _ := ctx.CacheContext() + for _, matchFn := range ig.matchFns { + if matchFn(cacheCtx, tx) { + return next(ctx, tx, simulate) + } + } + + return ig.decorator.AnteHandle(ctx, tx, simulate, next) +} diff --git a/app/app.go b/app/app.go index f7f8f9909..64c79889e 100644 --- a/app/app.go +++ b/app/app.go @@ -53,12 +53,15 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/bandprotocol/chain/v3/app/keepers" + "github.com/bandprotocol/chain/v3/app/mempool" "github.com/bandprotocol/chain/v3/app/upgrades" v3 "github.com/bandprotocol/chain/v3/app/upgrades/v3" v3_rc4 "github.com/bandprotocol/chain/v3/app/upgrades/v3_rc4" nodeservice "github.com/bandprotocol/chain/v3/client/grpc/node" proofservice "github.com/bandprotocol/chain/v3/client/grpc/oracle/proof" + feedskeeper "github.com/bandprotocol/chain/v3/x/feeds/keeper" oraclekeeper "github.com/bandprotocol/chain/v3/x/oracle/keeper" + tsskeeper "github.com/bandprotocol/chain/v3/x/tss/keeper" ) var ( @@ -254,11 +257,22 @@ func NewBandApp( app.sm.RegisterStoreDecoders() + lanes := CreateLanes(app) + + // create Band mempool + bandMempool := mempool.NewMempool(app.Logger(), lanes) + // set the mempool + app.SetMempool(bandMempool) + // Initialize stores. app.MountKVStores(app.GetKVStoreKey()) app.MountTransientStores(app.GetTransientStoreKey()) app.MountMemoryStores(app.GetMemoryStoreKey()) + feedsMsgServer := feedskeeper.NewMsgServerImpl(app.FeedsKeeper) + tssMsgServer := tsskeeper.NewMsgServerImpl(app.TSSKeeper) + oracleMsgServer := oraclekeeper.NewMsgServerImpl(app.OracleKeeper) + anteHandler, err := NewAnteHandler( HandlerOptions{ HandlerOptions: ante.HandlerOptions{ @@ -277,12 +291,24 @@ func NewBandApp( IBCKeeper: app.IBCKeeper, StakingKeeper: app.StakingKeeper, GlobalfeeKeeper: &app.GlobalFeeKeeper, + IgnoreDecoratorMatchFns: []mempool.TxMatchFn{ + feedsSubmitSignalPriceTxMatchHandler(app.appCodec, &app.AuthzKeeper, feedsMsgServer), + tssTxMatchHandler(app.appCodec, &app.AuthzKeeper, &app.BandtssKeeper, tssMsgServer), + oracleReportTxMatchHandler(app.appCodec, &app.AuthzKeeper, oracleMsgServer), + }, }, ) if err != nil { panic(fmt.Errorf("failed to create ante handler: %s", err)) } + // proposal handler + proposalHandler := mempool.NewProposalHandler(app.Logger(), txConfig.TxDecoder(), bandMempool) + + // set the Prepare / ProcessProposal Handlers on the app to be the `LanedMempool`'s + app.SetPrepareProposal(proposalHandler.PrepareProposalHandler()) + app.SetProcessProposal(proposalHandler.ProcessProposalHandler()) + postHandler, err := NewPostHandler( PostHandlerOptions{}, ) diff --git a/app/lanes.go b/app/lanes.go new file mode 100644 index 000000000..22256a13e --- /dev/null +++ b/app/lanes.go @@ -0,0 +1,113 @@ +package band + +import ( + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + + "github.com/bandprotocol/chain/v3/app/mempool" + feedstypes "github.com/bandprotocol/chain/v3/x/feeds/types" + oracletypes "github.com/bandprotocol/chain/v3/x/oracle/types" + tsstypes "github.com/bandprotocol/chain/v3/x/tss/types" +) + +// DefaultLaneMatchHandler is a function that returns the match function for the default lane. +func DefaultLaneMatchHandler() mempool.TxMatchFn { + return func(_ sdk.Context, _ sdk.Tx) bool { + return true + } +} + +// CreateLanes creates the lanes for the Band mempool. +func CreateLanes(app *BandApp) []*mempool.Lane { + // feedsLane handles feeds submit signal price transactions. + // Each transaction has a gas limit of 2%, and the total gas limit for the lane is 50%. + // It uses SenderNonceMempool to ensure transactions are ordered by sender and nonce, with no per-sender tx limit. + feedsLane := mempool.NewLane( + app.Logger(), + app.txConfig.TxEncoder(), + "feedsLane", + mempool.NewLaneTxMatchFn([]sdk.Msg{&feedstypes.MsgSubmitSignalPrices{}}, true), + math.LegacyMustNewDecFromStr("0.02"), + math.LegacyMustNewDecFromStr("0.5"), + sdkmempool.NewSenderNonceMempool(sdkmempool.SenderNonceMaxTxOpt(0)), + nil, + ) + + // tssLane handles TSS transactions. + // Each transaction has a gas limit of 10%, and the total gas limit for the lane is 20%. + tssLane := mempool.NewLane( + app.Logger(), + app.txConfig.TxEncoder(), + "tssLane", + mempool.NewLaneTxMatchFn( + []sdk.Msg{ + &tsstypes.MsgSubmitDKGRound1{}, + &tsstypes.MsgSubmitDKGRound2{}, + &tsstypes.MsgConfirm{}, + &tsstypes.MsgComplain{}, + &tsstypes.MsgSubmitDEs{}, + &tsstypes.MsgSubmitSignature{}, + }, + true, + ), + math.LegacyMustNewDecFromStr("0.1"), + math.LegacyMustNewDecFromStr("0.2"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + // oracleRequestLane handles oracle request data transactions. + // Each transaction has a gas limit of 10%, and the total gas limit for the lane is 10%. + // It is blocked if the oracle report lane exceeds its limit. + oracleRequestLane := mempool.NewLane( + app.Logger(), + app.txConfig.TxEncoder(), + "oracleRequestLane", + mempool.NewLaneTxMatchFn( + []sdk.Msg{ + &oracletypes.MsgRequestData{}, + &channeltypes.MsgRecvPacket{}, // TODO: Only match oracle request packet + }, + false, + ), + math.LegacyMustNewDecFromStr("0.1"), + math.LegacyMustNewDecFromStr("0.1"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + // oracleReportLane handles oracle report data transactions. + // Each transaction has a gas limit of 10%, and the total gas limit for the lane is 20%. + // It block the oracle request lane if it exceeds its limit. + oracleReportLane := mempool.NewLane( + app.Logger(), + app.txConfig.TxEncoder(), + "oracleReportLane", + mempool.NewLaneTxMatchFn([]sdk.Msg{&oracletypes.MsgReportData{}}, true), + math.LegacyMustNewDecFromStr("0.1"), + math.LegacyMustNewDecFromStr("0.2"), + sdkmempool.DefaultPriorityMempool(), + func(isLaneLimitExceeded bool) { + oracleRequestLane.SetBlocked(isLaneLimitExceeded) + }, + ) + + // defaultLane handles all other transactions. + // Each transaction has a gas limit of 10%, and the total gas limit for the lane is 10%. + defaultLane := mempool.NewLane( + app.Logger(), + app.txConfig.TxEncoder(), + "defaultLane", + DefaultLaneMatchHandler(), + math.LegacyMustNewDecFromStr("0.1"), + math.LegacyMustNewDecFromStr("0.1"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + return []*mempool.Lane{feedsLane, tssLane, oracleReportLane, oracleRequestLane, defaultLane} +} diff --git a/app/match.go b/app/match.go new file mode 100644 index 000000000..7895c6d30 --- /dev/null +++ b/app/match.go @@ -0,0 +1,250 @@ +package band + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + + "github.com/bandprotocol/chain/v3/app/mempool" + bandtsskeeper "github.com/bandprotocol/chain/v3/x/bandtss/keeper" + feedstypes "github.com/bandprotocol/chain/v3/x/feeds/types" + oracletypes "github.com/bandprotocol/chain/v3/x/oracle/types" + tsstypes "github.com/bandprotocol/chain/v3/x/tss/types" +) + +// feedsSubmitSignalPriceTxMatchHandler is a function that returns the match function for the Feeds SubmitSignalPriceTx. +func feedsSubmitSignalPriceTxMatchHandler( + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + feedsMsgServer feedstypes.MsgServer, +) mempool.TxMatchFn { + return func(ctx sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if !isValidMsgSubmitSignalPrices(ctx, msg, cdc, authzKeeper, feedsMsgServer) { + return false + } + } + return true + } +} + +// isValidMsgSubmitSignalPrices return true if the message is a valid feeds' MsgSubmitSignalPrices. +func isValidMsgSubmitSignalPrices( + ctx sdk.Context, + msg sdk.Msg, + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + feedsMsgServer feedstypes.MsgServer, +) bool { + switch msg := msg.(type) { + case *feedstypes.MsgSubmitSignalPrices: + return isSuccess(feedsMsgServer.SubmitSignalPrices(ctx, msg)) + case *authz.MsgExec: + msgs, err := msg.GetMessages() + if err != nil { + return false + } + + grantee, err := sdk.AccAddressFromBech32(msg.Grantee) + if err != nil { + return false + } + + for _, m := range msgs { + signers, _, err := cdc.GetMsgV1Signers(m) + if err != nil { + return false + } + // Check if this grantee have authorization for the message. + cap, _ := authzKeeper.GetAuthorization( + ctx, + grantee, + sdk.AccAddress(signers[0]), + sdk.MsgTypeURL(m), + ) + if cap == nil { + return false + } + + // Check if this message should be free or not. + if !isValidMsgSubmitSignalPrices(ctx, m, cdc, authzKeeper, feedsMsgServer) { + return false + } + } + return true + default: + return false + } +} + +// tssTxMatchHandler is a function that returns the match function for the TSS Tx. +func tssTxMatchHandler( + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + bandtssKeeper *bandtsskeeper.Keeper, + tssMsgServer tsstypes.MsgServer, +) mempool.TxMatchFn { + return func(ctx sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if !isValidTSSTxMsg(ctx, msg, cdc, authzKeeper, bandtssKeeper, tssMsgServer) { + return false + } + } + return true + } +} + +// isValidTSSTxMsg return true if the message is a valid for TSS Tx. +func isValidTSSTxMsg( + ctx sdk.Context, + msg sdk.Msg, + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + bandtssKeeper *bandtsskeeper.Keeper, + tssMsgServer tsstypes.MsgServer, +) bool { + switch msg := msg.(type) { + case *tsstypes.MsgSubmitDKGRound1: + return isSuccess(tssMsgServer.SubmitDKGRound1(ctx, msg)) + case *tsstypes.MsgSubmitDKGRound2: + return isSuccess(tssMsgServer.SubmitDKGRound2(ctx, msg)) + case *tsstypes.MsgConfirm: + return isSuccess(tssMsgServer.Confirm(ctx, msg)) + case *tsstypes.MsgComplain: + return isSuccess(tssMsgServer.Complain(ctx, msg)) + case *tsstypes.MsgSubmitDEs: + acc, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return false + } + + currentGroupID := bandtssKeeper.GetCurrentGroup(ctx).GroupID + incomingGroupID := bandtssKeeper.GetIncomingGroupID(ctx) + if !bandtssKeeper.HasMember(ctx, acc, currentGroupID) && + !bandtssKeeper.HasMember(ctx, acc, incomingGroupID) { + return false + } + + return isSuccess(tssMsgServer.SubmitDEs(ctx, msg)) + case *tsstypes.MsgSubmitSignature: + return isSuccess(tssMsgServer.SubmitSignature(ctx, msg)) + case *authz.MsgExec: + msgs, err := msg.GetMessages() + if err != nil { + return false + } + + grantee, err := sdk.AccAddressFromBech32(msg.Grantee) + if err != nil { + return false + } + + for _, m := range msgs { + signers, _, err := cdc.GetMsgV1Signers(m) + if err != nil { + return false + } + // Check if this grantee have authorization for the message. + cap, _ := authzKeeper.GetAuthorization( + ctx, + grantee, + sdk.AccAddress(signers[0]), + sdk.MsgTypeURL(m), + ) + if cap == nil { + return false + } + + // Check if this message should be free or not. + if !isValidTSSTxMsg(ctx, m, cdc, authzKeeper, bandtssKeeper, tssMsgServer) { + return false + } + } + return true + default: + return false + } +} + +// oracleReportTxMatchHandler is a function that returns the match function for the oracle report tx. +func oracleReportTxMatchHandler( + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + oracleMsgServer oracletypes.MsgServer, +) mempool.TxMatchFn { + return func(ctx sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if !isValidMsgReportData(ctx, msg, cdc, authzKeeper, oracleMsgServer) { + return false + } + } + return true + } +} + +// isValidMsgReportData return true if the message is a valid oracle's MsgReportData. +func isValidMsgReportData( + ctx sdk.Context, + msg sdk.Msg, + cdc codec.Codec, + authzKeeper *authzkeeper.Keeper, + oracleMsgServer oracletypes.MsgServer, +) bool { + switch msg := msg.(type) { + case *oracletypes.MsgReportData: + return isSuccess(oracleMsgServer.ReportData(ctx, msg)) + case *authz.MsgExec: + msgs, err := msg.GetMessages() + if err != nil { + return false + } + + grantee, err := sdk.AccAddressFromBech32(msg.Grantee) + if err != nil { + return false + } + + for _, m := range msgs { + signers, _, err := cdc.GetMsgV1Signers(m) + if err != nil { + return false + } + // Check if this grantee have authorization for the message. + cap, _ := authzKeeper.GetAuthorization( + ctx, + grantee, + sdk.AccAddress(signers[0]), + sdk.MsgTypeURL(m), + ) + if cap == nil { + return false + } + + // Check if this message should be free or not. + if !isValidMsgReportData(ctx, m, cdc, authzKeeper, oracleMsgServer) { + return false + } + } + + return true + default: + return false + } +} + +func isSuccess(_ any, err error) bool { + return err == nil +} diff --git a/app/mempool/README.md b/app/mempool/README.md new file mode 100644 index 000000000..73f9b0e4d --- /dev/null +++ b/app/mempool/README.md @@ -0,0 +1,151 @@ +# Mempool Package + +The mempool package implements a transaction mempool for the Cosmos SDK blockchain. It provides a sophisticated transaction management system that organizes transactions into different lanes based on their types and priorities. + +## Overview + +The mempool is designed to handle transaction processing and block proposal preparation in a Cosmos SDK-based blockchain. It implements the `sdkmempool.Mempool` interface and uses a lane-based architecture to manage different types of transactions. + +## Architecture + +### Core Components + +1. **Mempool**: The main structure that manages multiple transaction lanes and implements the core mempool interface. +2. **Lane**: A logical grouping of transactions with specific matching criteria and space allocation. +3. **Proposal**: Represents a block proposal under construction, managing transaction inclusion and space limits. +4. **BlockSpace**: Manages block space constraints including transaction bytes and gas limits. + +### Key Features + +- **Lane-based Organization**: Transactions are organized into different lanes based on their matching functions +- **Space Management**: Efficient management of block space and gas limits +- **Transaction Prioritization**: Support for different transaction priorities among and within lanes +- **Thread Safety**: Built-in mutex protection for concurrent access +- **Error Recovery**: Panic recovery mechanisms for robust operation + +## Usage + +### Creating a Lane + +A lane is created with specific matching criteria and space allocation. Here's an example of creating a lane for bank send transactions: + +```go +bankSendLane := NewLane( + logger, + txEncoder, + "bankSend", // Lane name + isBankSendTx, // Matching function + math.LegacyMustNewDecFromStr("0.2"), // Max transaction space ratio + math.LegacyMustNewDecFromStr("0.3"), // Max lane space ratio + sdkmempool.DefaultPriorityMempool(), // Underlying mempool implementation + nil, // Lane limit check handler +) + +// Example matching function for bank send transactions +func isBankSendTx(_ sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if _, ok := msg.(*banktypes.MsgSend); !ok { + return false + } + } + return true +} +``` + +Key parameters for lane creation: +- `logger`: Logger instance for lane operations +- `txEncoder`: Function to encode transactions +- `name`: Unique identifier for the lane +- `matchFn`: Function to determine if a transaction belongs in this lane +- `maxTransactionBlockRatio`: Maximum space ratio for individual transactions relative to total block space more details on [Space management](#space-management) +- `maxLaneBlockRatio`: Maximum space ratio for the entire lane relative to total block space more details on [Space management](#space-management) +- `mempool`: Underlying Cosmos SDK mempool implementation that handles transaction storage and ordering within the lane. This determines how transactions are stored and selected within the lane +- `callbackAfterFillProposal`: Optional callback function that is called when the lane fills its space limit. This can be used to implement inter-lane dependencies, such as blocking other lanes when a lane fills its limit. + +#### Inter-Lane Dependencies + +Lanes can be configured to interact with each other through the `callbackAfterFillProposal` callback. This is useful for implementing priority systems or dependencies between different types of transactions. For example, you might want to block certain lanes when a high-priority lane fills its limit: + +```go +// Create a dependent lane that will be blocked when the dependency lane fills its limit +dependentLane := NewLane( + logger, + txEncoder, + signerExtractor, + "dependent", + isOtherTx, + math.LegacyMustNewDecFromStr("0.5"), + math.LegacyMustNewDecFromStr("0.5"), + sdkmempool.DefaultPriorityMempool(), + nil, +) + +// Create a dependency lane that controls the dependent lane +dependencyLane := NewLane( + logger, + txEncoder, + signerExtractor, + "dependency", + isBankSendTx, + math.LegacyMustNewDecFromStr("0.5"), + math.LegacyMustNewDecFromStr("0.5"), + sdkmempool.DefaultPriorityMempool(), + func(isLaneLimitExceeded bool) { + dependentLane.SetBlocked(isLaneLimitExceeded) + }, +) +``` + +In this example, when the dependency lane fills its space limit, it will block the dependent lane from processing transactions. This mechanism allows for sophisticated transaction prioritization and coordination between different types of transactions. + +### Creating a Mempool + +```go +mempool := NewMempool( + logger, + []*Lane{ + BankSendLane, + DelegationLane, + // Lane order is critical - first lane in the slice matches and processes the transaction first + }, +) + +// set the mempool in Chain application +app.SetMempool(mempool) +``` + +Key parameters for mempool creation: +- `logger`: Logger instance for mempool operations +- `lanes`: Array of lane configurations, where each lane is responsible for a specific type of transaction + - The order of lanes determines the priority of transaction types + - The sum of all lane space ratios can exceed 100% as it represents the maximum potential allocation for each lane, not a strict partition of the block space + +### Space Management + +#### Space Allocation +Both `maxTransactionBlockRatio` and `maxLaneBlockRatio` are expressed as ratios of the total block space and are used specifically during proposal preparation. For example, a `maxTransactionBlockRatio` of 0.2 means a single transaction can use up to 20% of the total block space in a proposal, while a `maxLaneBlockRatio` of 0.3 means the entire lane can use up to 30% of the total block space in a proposal. These ratios are used to ensure fair distribution of block space among different transaction types during proposal construction. + +#### Space Cap Behavior +- **Transaction Space Cap (Hard Cap)**: The `maxTransactionBlockRatio` ratio enforces a strict limit on individual transaction sizes of each lane. For example, with a `maxTransactionBlockRatio` of 0.2, a transaction requiring more than 20% of the total block space will not be accepted. +- **Lane Space Cap (Soft Cap)**: The `maxLaneBlockRatio` ratio serves as a guideline for space allocation during the first round of proposal construction. If a lane's `maxLaneBlockRatio` is 0.3, it can still include one last transaction that would cause it to exceed this limit in the proposal, provided each individual transaction respects the `maxTransactionBlockRatio` limit. For instance, a lane with a 0.3 `maxLaneBlockRatio` could include two transactions each using 20% of the block space (totaling 40%) in the proposal, as long as both transactions individually respect the `maxTransactionBlockRatio` limit. + +#### Proposal Preparation +The lane space cap (`maxLaneBlockRatio`) is only enforced during the first round of proposal preparation. In subsequent rounds, when filling the remaining block space, the lane cap is not considered, allowing lanes to potentially use more space than their initial allocation if space is available. This two-phase approach ensures both fair initial distribution and efficient use of remaining block space. + +### Block Proposal Preparation + +The mempool provides functionality to prepare block proposals by: +1. Filling proposals with transactions from each lane with `maxLaneBlockRatio` +2. Filling remaining proposal space with transactions from each lane in the same order without `maxLaneBlockRatio` + +## Best Practices + +1. Configure appropriate lane ratios based on your application's needs +2. Implement proper transaction matching functions for each lane +3. Always place the default lane as the last lane to ensure every transaction type can be matched + +Note: Lane order is critical as it determines transaction processing priority. This is why the default lane should always be last, to ensure it only catches transactions that don't match any other lane. diff --git a/app/mempool/block_space.go b/app/mempool/block_space.go new file mode 100644 index 000000000..78715832a --- /dev/null +++ b/app/mempool/block_space.go @@ -0,0 +1,116 @@ +package mempool + +import ( + "fmt" + "math" + + ethmath "github.com/ethereum/go-ethereum/common/math" + + sdkmath "cosmossdk.io/math" +) + +// BlockSpace defines the block space. +type BlockSpace struct { + txBytes uint64 + gas uint64 +} + +// NewBlockSpace returns a new block space. +func NewBlockSpace(txBytes uint64, gas uint64) BlockSpace { + return BlockSpace{ + txBytes: txBytes, + gas: gas, + } +} + +// --- Getters --- +func (bs BlockSpace) TxBytes() uint64 { + return bs.txBytes +} + +func (bs BlockSpace) Gas() uint64 { + return bs.gas +} + +// --- Comparison Methods --- + +// IsReachedBy returns true if 'other' usage has reached this BlockSpace's limits. +func (bs BlockSpace) IsReachedBy(other BlockSpace) bool { + return other.txBytes >= bs.txBytes || other.gas >= bs.gas +} + +// IsExceededBy returns true if 'other' usage has exceeded this BlockSpace's limits. +func (bs BlockSpace) IsExceededBy(other BlockSpace) bool { + return other.txBytes > bs.txBytes || other.gas > bs.gas +} + +// --- Math Methods --- + +// Sub returns the difference between this BlockSpace and another BlockSpace. +// Ensures txBytes and gas never go below zero. +func (bs BlockSpace) Sub(other BlockSpace) BlockSpace { + var txBytes uint64 + var gas uint64 + + // Calculate txBytes + txBytes, borrowOut := ethmath.SafeSub(bs.txBytes, other.txBytes) + if borrowOut { + txBytes = 0 + } + + // Calculate gas + gas, borrowOut = ethmath.SafeSub(bs.gas, other.gas) + if borrowOut { + gas = 0 + } + + return BlockSpace{ + txBytes: txBytes, + gas: gas, + } +} + +// Add returns the sum of this BlockSpace and another BlockSpace. +func (bs BlockSpace) Add(other BlockSpace) BlockSpace { + var txBytes uint64 + var gas uint64 + + // Calculate txBytes + txBytes, carry := ethmath.SafeAdd(bs.txBytes, other.txBytes) + if carry { + txBytes = math.MaxUint64 + } + + // Calculate gas + gas, carry = ethmath.SafeAdd(bs.gas, other.gas) + if carry { + gas = math.MaxUint64 + } + + return BlockSpace{ + txBytes: txBytes, + gas: gas, + } +} + +// Scale returns a new BlockSpace with txBytes and gas multiplied by a decimal. +func (bs BlockSpace) Scale(dec sdkmath.LegacyDec) (BlockSpace, error) { + txBytes := dec.MulInt(sdkmath.NewIntFromUint64(bs.txBytes)).TruncateInt() + gas := dec.MulInt(sdkmath.NewIntFromUint64(bs.gas)).TruncateInt() + + if !txBytes.IsUint64() || !gas.IsUint64() { + return BlockSpace{}, fmt.Errorf("block space scaling overflow: block_space %s, dec %s", bs, dec) + } + + return BlockSpace{ + txBytes: txBytes.Uint64(), + gas: gas.Uint64(), + }, nil +} + +// --- Stringer --- + +// String returns a string representation of the BlockSpace. +func (bs BlockSpace) String() string { + return fmt.Sprintf("BlockSpace{txBytes: %d, gas: %d}", bs.txBytes, bs.gas) +} diff --git a/app/mempool/lane.go b/app/mempool/lane.go new file mode 100644 index 000000000..1bee955f4 --- /dev/null +++ b/app/mempool/lane.go @@ -0,0 +1,291 @@ +package mempool + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + "sync" + + comettypes "github.com/cometbft/cometbft/types" + + "cosmossdk.io/log" + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" +) + +// Lane defines a logical grouping of transactions within the mempool. +type Lane struct { + logger log.Logger + txEncoder sdk.TxEncoder + name string + txMatchFn TxMatchFn + + maxTransactionBlockRatio math.LegacyDec + maxLaneBlockRatio math.LegacyDec + + mempool sdkmempool.Mempool + + // txIndex holds the uppercase hex-encoded hash for every transaction + // currently in this lane's mempool. + txIndex map[string]struct{} + + // callbackAfterFillProposal is a callback function that is called after + // filling the proposal with transactions from the lane. + callbackAfterFillProposal func(isLaneLimitExceeded bool) + + // blocked indicates whether the transactions in this lane should be + // excluded from the proposal for the current block. + blocked bool + + // Add mutex for thread safety. + mu sync.RWMutex +} + +// NewLane is a constructor for a lane. +func NewLane( + logger log.Logger, + txEncoder sdk.TxEncoder, + name string, + txMatchFn TxMatchFn, + maxTransactionBlockRatio math.LegacyDec, + maxLaneBlockRatio math.LegacyDec, + mempool sdkmempool.Mempool, + callbackAfterFillProposal func(isLaneLimitExceeded bool), +) *Lane { + return &Lane{ + logger: logger, + txEncoder: txEncoder, + name: name, + txMatchFn: txMatchFn, + maxTransactionBlockRatio: maxTransactionBlockRatio, + maxLaneBlockRatio: maxLaneBlockRatio, + mempool: mempool, + callbackAfterFillProposal: callbackAfterFillProposal, + + // Initialize the txIndex. + txIndex: make(map[string]struct{}), + + blocked: false, + } +} + +// Insert inserts a transaction into the lane's mempool. +func (l *Lane) Insert(ctx context.Context, tx sdk.Tx) error { + txInfo, err := l.getTxInfo(tx) + if err != nil { + return err + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + consensusParams := sdkCtx.ConsensusParams() + + transactionLimit, err := NewBlockSpace( + uint64(consensusParams.Block.MaxBytes), + uint64(consensusParams.Block.MaxGas), + ).Scale(l.maxTransactionBlockRatio) + if err != nil { + return err + } + + if transactionLimit.IsExceededBy(txInfo.BlockSpace) { + return fmt.Errorf( + "transaction exceeds limit: tx_hash %s, lane %s, limit %s, tx_size %s", + txInfo.Hash, + l.name, + transactionLimit, + txInfo.BlockSpace, + ) + } + + l.mu.Lock() + defer l.mu.Unlock() + + if err = l.mempool.Insert(ctx, tx); err != nil { + return err + } + + l.txIndex[txInfo.Hash] = struct{}{} + return nil +} + +// CountTx returns the total number of transactions in the lane's mempool. +func (l *Lane) CountTx() int { + return l.mempool.CountTx() +} + +// Remove removes a transaction from the lane's mempool. +func (l *Lane) Remove(tx sdk.Tx) error { + txInfo, err := l.getTxInfo(tx) + if err != nil { + return err + } + + l.mu.Lock() + defer l.mu.Unlock() + + if err = l.mempool.Remove(tx); err != nil { + return err + } + + delete(l.txIndex, txInfo.Hash) + return nil +} + +// Contains returns true if the lane's mempool contains the transaction. +func (l *Lane) Contains(tx sdk.Tx) bool { + txInfo, err := l.getTxInfo(tx) + if err != nil { + return false + } + + l.mu.RLock() + defer l.mu.RUnlock() + + _, exists := l.txIndex[txInfo.Hash] + return exists +} + +// Match returns true if the transaction belongs to the lane. +func (l *Lane) Match(ctx sdk.Context, tx sdk.Tx) bool { + return l.txMatchFn(ctx, tx) +} + +// FillProposal fills the proposal with transactions from the lane mempool with its own limit. +// It returns the total size and gas of the transactions added to the proposal. +// It also returns an iterator to the next transaction in the mempool. +func (l *Lane) FillProposal( + ctx sdk.Context, + proposal *Proposal, +) (blockUsed BlockSpace, iterator sdkmempool.Iterator) { + // if the lane is blocked, we do not add any transactions to the proposal. + if l.blocked { + l.logger.Info("lane %s is blocked, skipping proposal filling", l.name) + return + } + + // Get the lane limit for the lane. + laneLimit, err := proposal.maxBlockSpace.Scale(l.maxLaneBlockRatio) + if err != nil { + l.logger.Error("failed to scale lane limit with err:", err) + return + } + + // Select all transactions in the mempool that are valid and not already in the + // partial proposal. + for iterator = l.mempool.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { + // If the total size used or total gas used exceeds the limit, we break and do not attempt to include more txs. + // We can tolerate a few bytes/gas over the limit, since we limit the size of each transaction. + if laneLimit.IsReachedBy(blockUsed) { + break + } + + tx := iterator.Tx() + txInfo, err := l.getTxInfo(tx) + if err != nil { + // If the transaction is not valid, we skip it. + // This should never happen, but we log it for debugging purposes. + l.logger.Error("failed to get tx info with err:", err) + continue + } + + // Add the transaction to the proposal. + if err := proposal.Add(txInfo); err != nil { + l.logger.Info( + "failed to add tx to proposal", + "lane", l.name, + "tx_hash", txInfo.Hash, + "err", err, + ) + + break + } + + blockUsed = blockUsed.Add(txInfo.BlockSpace) + } + + // call the callback function of the lane after fill proposal. + if l.callbackAfterFillProposal != nil { + l.callbackAfterFillProposal(laneLimit.IsReachedBy(blockUsed)) + } + + return +} + +// FillProposalByIterator fills the proposal with transactions from the lane mempool with the given iterator and limit. +// It returns the total size and gas of the transactions added to the proposal. +func (l *Lane) FillProposalByIterator( + proposal *Proposal, + iterator sdkmempool.Iterator, + limit BlockSpace, +) (blockUsed BlockSpace) { + // if the lane is blocked, we do not add any transactions to the proposal. + if l.blocked { + return + } + + // Select all transactions in the mempool that are valid and not already in the partial proposal. + for ; iterator != nil; iterator = iterator.Next() { + // If the total size used or total gas used exceeds the limit, we break and do not attempt to include more txs. + // We can tolerate a few bytes/gas over the limit, since we limit the size of each transaction. + if limit.IsReachedBy(blockUsed) { + break + } + + tx := iterator.Tx() + txInfo, err := l.getTxInfo(tx) + if err != nil { + // If the transaction is not valid, we skip it. + // This should never happen, but we log it for debugging purposes. + l.logger.Error("failed to get tx info with err:", err) + continue + } + + // Add the transaction to the proposal. + if err := proposal.Add(txInfo); err != nil { + l.logger.Info( + "failed to add tx to proposal", + "lane", l.name, + "tx_hash", txInfo.Hash, + "err", err, + ) + + break + } + + // Update the total size and gas. + blockUsed = blockUsed.Add(txInfo.BlockSpace) + } + + return +} + +// getTxInfo returns various information about the transaction that +// belongs to the lane including its priority, signer's, sequence number, +// size and more. +func (l *Lane) getTxInfo(tx sdk.Tx) (TxWithInfo, error) { + txBytes, err := l.txEncoder(tx) + if err != nil { + return TxWithInfo{}, fmt.Errorf("failed to encode transaction: %w", err) + } + + gasTx, ok := tx.(sdk.FeeTx) + if !ok { + return TxWithInfo{}, fmt.Errorf("failed to cast transaction to gas tx") + } + + blockSpace := NewBlockSpace(uint64(len(txBytes)), gasTx.GetGas()) + + return TxWithInfo{ + Hash: strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBytes).Hash())), + BlockSpace: blockSpace, + TxBytes: txBytes, + }, nil +} + +// SetBlocked sets the blocked flag to the given value. +func (l *Lane) SetBlocked(blocked bool) { + l.blocked = blocked +} diff --git a/app/mempool/lane_test.go b/app/mempool/lane_test.go new file mode 100644 index 000000000..b89948ee4 --- /dev/null +++ b/app/mempool/lane_test.go @@ -0,0 +1,495 @@ +package mempool + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/suite" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + txsigning "github.com/cosmos/cosmos-sdk/types/tx/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// LaneTestSuite is a testify.Suite for unit-testing the Lane functionality. +type LaneTestSuite struct { + suite.Suite + + encodingConfig EncodingConfig + random *rand.Rand + accounts []Account + gasTokenDenom string + ctx sdk.Context +} + +func TestLaneTestSuite(t *testing.T) { + suite.Run(t, new(LaneTestSuite)) +} + +func (s *LaneTestSuite) SetupTest() { + s.encodingConfig = CreateTestEncodingConfig() + s.random = rand.New(rand.NewSource(1)) + s.accounts = RandomAccounts(s.random, 3) + s.gasTokenDenom = "uband" + + testCtx := testutil.DefaultContextWithDB( + s.T(), + storetypes.NewKVStoreKey("test"), + storetypes.NewTransientStoreKey("transient_test"), + ) + s.ctx = testCtx.Ctx.WithIsCheckTx(true) + s.ctx = s.ctx.WithBlockHeight(1) + s.ctx = s.ctx.WithConsensusParams(cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{ + MaxBytes: 1000000000000, + MaxGas: 100, + }, + }) +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +func (s *LaneTestSuite) TestLaneInsertAndCount() { + // Create a Lane that matches all txs (Match always returns true), + // just to test Insert/Count. + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.3"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + // Create and insert two transactions + tx1 := s.createSimpleTx(s.accounts[0], 0, 10) + tx2 := s.createSimpleTx(s.accounts[1], 0, 10) + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + s.Require().NoError(lane.Insert(s.ctx, tx2)) + + // Ensure lane sees 2 transactions + s.Require().Equal(2, lane.CountTx()) + + // Create over gas limit tx + tx3 := s.createSimpleTx(s.accounts[2], 0, 100) + s.Require().Error(lane.Insert(s.ctx, tx3)) + + // Ensure lane does not insert tx3 + s.Require().Equal(2, lane.CountTx()) + + // set bytes limit to 1000 + s.ctx = s.ctx.WithConsensusParams(cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{ + MaxBytes: 500, + MaxGas: 100, + }, + }) + + // Create over bytes limit tx + tx4 := s.createSimpleTx(s.accounts[2], 0, 0) // 217 bytes + s.Require().Error(lane.Insert(s.ctx, tx4)) + + // Ensure lane does not insert tx4 + s.Require().Equal(2, lane.CountTx()) +} + +func (s *LaneTestSuite) TestLaneRemove() { + // Lane that matches all txs + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.3"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + tx := s.createSimpleTx(s.accounts[0], 0, 10) + s.Require().NoError(lane.Insert(s.ctx, tx)) + s.Require().Equal(1, lane.CountTx()) + + // Remove it + err := lane.Remove(tx) + s.Require().NoError(err) + s.Require().Equal(0, lane.CountTx()) +} + +func (s *LaneTestSuite) TestLaneFillProposalWithGasLimit() { + // Lane that matches all txs + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.2"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + // Insert 3 transactions + tx1 := s.createSimpleTx(s.accounts[0], 0, 20) + tx2 := s.createSimpleTx(s.accounts[1], 1, 20) + tx3 := s.createSimpleTx(s.accounts[2], 2, 20) + tx4 := s.createSimpleTx(s.accounts[2], 3, 20) + tx5 := s.createSimpleTx(s.accounts[2], 4, 15) + tx6 := s.createSimpleTx(s.accounts[2], 5, 10) + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + s.Require().NoError(lane.Insert(s.ctx, tx2)) + s.Require().NoError(lane.Insert(s.ctx, tx3)) + s.Require().NoError(lane.Insert(s.ctx, tx4)) + s.Require().NoError(lane.Insert(s.ctx, tx5)) + s.Require().NoError(lane.Insert(s.ctx, tx6)) + + // Create a proposal with block-limits + proposal := NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator := lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 and tx2 to be included in the proposal. + // Since the 20% of 1000 is 200, the gas should be over the limit, so tx3 is yet to be considered. + s.Require().Equal(uint64(40), blockUsed.Gas(), "20 gas from tx1 and 20 gas from tx2") + s.Require().NotNil(iterator) + + // The proposal should contain 2 transactions in Txs(). + expectedIncludedTxs := s.getTxBytes(tx1, tx2) + s.Require().Equal(2, len(proposal.txs), "two txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + // Call FillProposalBy with the remainder limit and iterator from the previous call. + blockUsed = lane.FillProposalByIterator(&proposal, iterator, proposal.GetRemainingBlockSpace()) + + // We expect tx1, tx2, tx3, tx4, tx5 to be included in the proposal. + s.Require().Equal(uint64(55), blockUsed.Gas(), "20 gas from tx3 and 20 gas from tx4 + 15 gas from tx5") + + // The proposal should contain 5 transactions in Txs(). + expectedIncludedTxs = s.getTxBytes(tx1, tx2, tx3, tx4, tx5) + s.Require().Equal(5, len(proposal.txs), "five txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) +} + +func (s *LaneTestSuite) TestLaneFillProposalWithBytesLimit() { + // Lane that matches all txs + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.2"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + // Insert 3 transactions + tx1 := s.createSimpleTx(s.accounts[0], 0, 0) // 217 bytes + tx2 := s.createSimpleTx(s.accounts[1], 1, 0) // 219 bytes + tx3 := s.createSimpleTx(s.accounts[2], 2, 0) // 219 bytes + tx4 := s.createSimpleTx(s.accounts[2], 3, 0) // 219 bytes + tx5 := s.createSimpleTx(s.accounts[2], 4, 0) // 219 bytes + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + s.Require().NoError(lane.Insert(s.ctx, tx2)) + s.Require().NoError(lane.Insert(s.ctx, tx3)) + s.Require().NoError(lane.Insert(s.ctx, tx4)) + s.Require().NoError(lane.Insert(s.ctx, tx5)) + + // Create a proposal with block-limits + proposal := NewProposal( + log.NewTestLogger(s.T()), + 1000, + 1000000000000, + ) + + // FillProposal + blockUsed, iterator := lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 and tx2 to be included in the proposal. + // Since the 30% of 1000 is 300, the bytes should be over the limit, so tx3 is yet to be considered. + s.Require().Equal(uint64(436), blockUsed.TxBytes()) + + // The proposal should contain 2 transactions in Txs(). + expectedIncludedTxs := s.getTxBytes(tx1, tx2) + s.Require().Equal(2, len(proposal.txs), "two txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + // Call FillProposalBy with the remainder limit and iterator from the previous call. + blockUsed = lane.FillProposalByIterator(&proposal, iterator, proposal.GetRemainingBlockSpace()) + + // We expect tx1, tx2, tx3, tx4 to be included in the proposal. + s.Require().Equal(uint64(438), blockUsed.TxBytes()) + + // The proposal should contain 4 transactions in Txs(). + expectedIncludedTxs = s.getTxBytes(tx1, tx2, tx3, tx4) + s.Require().Equal(4, len(proposal.txs), "four txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) +} + +type callbackAfterFillProposalMock struct { + isLaneLimitExceeded bool +} + +func (f *callbackAfterFillProposalMock) callbackAfterFillProposal(isLaneLimitExceeded bool) { + f.isLaneLimitExceeded = isLaneLimitExceeded +} + +func (s *LaneTestSuite) TestLaneCallbackAfterFillProposal() { + callbackAfterFillProposalMock := &callbackAfterFillProposalMock{} + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.3"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + callbackAfterFillProposalMock.callbackAfterFillProposal, + ) + + // Insert a transaction + tx1 := s.createSimpleTx(s.accounts[0], 0, 20) + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + + // Create a proposal with block-limits + proposal := NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator := lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 to be included in the proposal. + s.Require().Equal(uint64(20), blockUsed.Gas(), "20 gas from tx1") + s.Require().Nil(iterator) + + // The proposal should contain 1 transaction in Txs(). + expectedIncludedTxs := s.getTxBytes(tx1) + s.Require().Equal(1, len(proposal.txs), "one txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().False(callbackAfterFillProposalMock.isLaneLimitExceeded, "callbackAfterFillProposal should be called with false") + + // Insert 2 more transactions + tx2 := s.createSimpleTx(s.accounts[1], 1, 20) + tx3 := s.createSimpleTx(s.accounts[2], 2, 30) + + s.Require().NoError(lane.Insert(s.ctx, tx2)) + s.Require().NoError(lane.Insert(s.ctx, tx3)) + + // Create a proposal with block-limits + proposal = NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator = lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 and tx2 to be included in the proposal. + // Then the gas should be over the limit, so tx3 is yet to be considered. + s.Require().Equal(uint64(40), blockUsed.Gas(), "20 gas from tx1 and 20 gas from tx2") + s.Require().NotNil(iterator) + + // The proposal should contain 2 transactions in Txs(). + expectedIncludedTxs = s.getTxBytes(tx1, tx2) + s.Require().Equal(2, len(proposal.txs), "two txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().True(callbackAfterFillProposalMock.isLaneLimitExceeded, "OoLaneLimitExceeded should be called with true") +} + +func (s *LaneTestSuite) TestLaneExactlyFilled() { + callbackAfterFillProposalMock := &callbackAfterFillProposalMock{} + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.3"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + callbackAfterFillProposalMock.callbackAfterFillProposal, + ) + + // Insert a transaction + tx1 := s.createSimpleTx(s.accounts[0], 0, 20) + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + + // Create a proposal with block-limits + proposal := NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator := lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 to be included in the proposal. + s.Require().Equal(uint64(20), blockUsed.Gas(), "20 gas from tx1") + s.Require().Nil(iterator) + + // The proposal should contain 1 transaction in Txs(). + expectedIncludedTxs := s.getTxBytes(tx1) + s.Require().Equal(1, len(proposal.txs), "one txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().False(callbackAfterFillProposalMock.isLaneLimitExceeded, "callbackAfterFillProposal should be called with false") + + // Insert 2 more transactions + tx2 := s.createSimpleTx(s.accounts[1], 1, 10) + tx3 := s.createSimpleTx(s.accounts[2], 2, 30) + + s.Require().NoError(lane.Insert(s.ctx, tx2)) + s.Require().NoError(lane.Insert(s.ctx, tx3)) + + // Create a proposal with block-limits + proposal = NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator = lane.FillProposal(s.ctx, &proposal) + + // We expect tx1 and tx2 to be included in the proposal. + // Then the gas should be over the limit, so tx3 is yet to be considered. + s.Require().Equal(uint64(30), blockUsed.Gas(), "20 gas from tx1 and 10 gas from tx2") + s.Require().NotNil(iterator) + + // The proposal should contain 2 transactions in Txs(). + expectedIncludedTxs = s.getTxBytes(tx1, tx2) + s.Require().Equal(2, len(proposal.txs), "two txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().True(callbackAfterFillProposalMock.isLaneLimitExceeded, "callbackAfterFillProposal should be called with true") +} + +func (s *LaneTestSuite) TestLaneBlocked() { + // Lane that matches all txs + lane := NewLane( + log.NewNopLogger(), + s.encodingConfig.TxConfig.TxEncoder(), + "testLane", + func(sdk.Context, sdk.Tx) bool { return true }, // accept all + math.LegacyMustNewDecFromStr("0.2"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + lane.SetBlocked(true) + + // Insert 3 transactions + tx1 := s.createSimpleTx(s.accounts[0], 0, 20) + tx2 := s.createSimpleTx(s.accounts[1], 1, 20) + + s.Require().NoError(lane.Insert(s.ctx, tx1)) + s.Require().NoError(lane.Insert(s.ctx, tx2)) + + // Create a proposal with block-limits + proposal := NewProposal( + log.NewTestLogger(s.T()), + 1000000000000, + 100, + ) + + // FillProposal + blockUsed, iterator := lane.FillProposal(s.ctx, &proposal) + + s.Require().True(lane.blocked) + + // We expect no txs to be included in the proposal. + s.Require().Equal(uint64(0), blockUsed.TxBytes()) + s.Require().Equal(uint64(0), blockUsed.Gas(), "0 gas") + s.Require().Nil(iterator) + + // The proposal should contain 0 transactions in Txs(). + expectedIncludedTxs := [][]byte{} + s.Require().Equal(0, len(proposal.txs), "no txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().Equal(lane.mempool.Select(s.ctx, nil).Tx(), tx1) + + // Call FillProposalBy with the remainder limit and iterator from the previous call. + blockUsed = lane.FillProposalByIterator(&proposal, iterator, proposal.GetRemainingBlockSpace()) + + // We expect no txs to be included in the proposal. + s.Require().Equal(uint64(0), blockUsed.TxBytes()) + s.Require().Equal(uint64(0), blockUsed.Gas()) + + // The proposal should contain 0 transactions in Txs(). + expectedIncludedTxs = [][]byte{} + s.Require().Equal(0, len(proposal.txs), "no txs in the proposal") + s.Require().Equal(expectedIncludedTxs, proposal.txs) + + s.Require().Equal(lane.mempool.Select(s.ctx, nil).Tx(), tx1) +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// createSimpleTx creates a basic single-bank-send Tx with the specified gasLimit. +func (s *LaneTestSuite) createSimpleTx(account Account, sequence uint64, gasLimit uint64) sdk.Tx { + msg := &banktypes.MsgSend{ + FromAddress: account.Address.String(), + ToAddress: account.Address.String(), + } + txBuilder := s.encodingConfig.TxConfig.NewTxBuilder() + if err := txBuilder.SetMsgs(msg); err != nil { + s.Require().NoError(err) + } + + sigV2 := txsigning.SignatureV2{ + PubKey: account.PrivKey.PubKey(), + Data: &txsigning.SingleSignatureData{ + SignMode: txsigning.SignMode_SIGN_MODE_DIRECT, + Signature: nil, + }, + Sequence: sequence, + } + err := txBuilder.SetSignatures(sigV2) + s.Require().NoError(err) + + txBuilder.SetGasLimit(gasLimit) + return txBuilder.GetTx() +} + +// getTxBytes encodes the given transactions to raw bytes for comparison. +func (s *LaneTestSuite) 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 +} diff --git a/app/mempool/mempool.go b/app/mempool/mempool.go new file mode 100644 index 000000000..ee3869d5d --- /dev/null +++ b/app/mempool/mempool.go @@ -0,0 +1,164 @@ +package mempool + +import ( + "context" + "fmt" + "math" + + "cosmossdk.io/log" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" +) + +var _ sdkmempool.Mempool = (*Mempool)(nil) + +// Mempool implements the sdkmempool.Mempool interface and uses Lanes internally. +type Mempool struct { + logger log.Logger + + lanes []*Lane +} + +// NewMempool returns a new mempool with the given lanes. +func NewMempool( + logger log.Logger, + lanes []*Lane, +) *Mempool { + return &Mempool{ + logger: logger, + lanes: lanes, + } +} + +// Insert will insert a transaction into the mempool. It inserts the transaction +// into the first lane that it matches. +func (m *Mempool) Insert(ctx context.Context, tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Insert", "err", r) + err = fmt.Errorf("panic in Insert: %v", r) + } + }() + + cacheSDKCtx, _ := sdk.UnwrapSDKContext(ctx).CacheContext() + for _, lane := range m.lanes { + if lane.Match(cacheSDKCtx, tx) { + return lane.Insert(ctx, tx) + } + } + + return +} + +// Select returns a Mempool iterator (currently nil). +func (m *Mempool) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator { + return nil +} + +// CountTx returns the total number of transactions across all lanes. +// Returns math.MaxInt if the total count would overflow. +func (m *Mempool) CountTx() int { + count := 0 + for _, lane := range m.lanes { + laneCount := lane.CountTx() + if laneCount > 0 && count > math.MaxInt-laneCount { + // If adding laneCount would cause overflow, return MaxInt + return math.MaxInt + } + count += laneCount + } + return count +} + +// Remove removes a transaction from the mempool. This assumes that the transaction +// is contained in only one of the lanes. +func (m *Mempool) Remove(tx sdk.Tx) (err error) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Remove", "err", r) + err = fmt.Errorf("panic in Remove: %v", r) + } + }() + + for _, lane := range m.lanes { + if lane.Contains(tx) { + return lane.Remove(tx) + } + } + + return nil +} + +// PrepareProposal divides the block gas limit among lanes (based on lane percentage), +// then calls each lane's FillProposal method. If leftover gas is important to you, +// you can implement a second pass or distribute leftover to subsequent lanes, etc. +func (m *Mempool) PrepareProposal(ctx sdk.Context, proposal Proposal) (Proposal, error) { + cacheCtx, _ := ctx.CacheContext() + + // Perform the initial fill of proposals + laneIterators, _ := m.fillInitialProposals(cacheCtx, &proposal) + + // Fill proposals with leftover space + m.fillRemainderProposals(&proposal, laneIterators) + + return proposal, nil +} + +// fillInitialProposals iterates over lanes, calling FillProposal. It returns: +// - laneIterators: the Iterator for each lane +// - totalSize: total block size used +// - totalGas: total gas used +func (m *Mempool) fillInitialProposals( + ctx sdk.Context, + proposal *Proposal, +) ( + []sdkmempool.Iterator, + BlockSpace, +) { + totalBlockUsed := NewBlockSpace(0, 0) + + laneIterators := make([]sdkmempool.Iterator, len(m.lanes)) + + for i, lane := range m.lanes { + blockUsed, iterator := lane.FillProposal(ctx, proposal) + totalBlockUsed = totalBlockUsed.Add(blockUsed) + + laneIterators[i] = iterator + } + + return laneIterators, totalBlockUsed +} + +// fillRemainderProposals performs an additional fill on each lane using the leftover +// BlockSpace. It updates txsToRemove to include any newly removed transactions. +func (m *Mempool) fillRemainderProposals( + proposal *Proposal, + laneIterators []sdkmempool.Iterator, +) { + for i, lane := range m.lanes { + lane.FillProposalByIterator( + proposal, + laneIterators[i], + proposal.GetRemainingBlockSpace(), + ) + } +} + +// Contains returns true if the transaction is contained in any of the lanes. +func (m *Mempool) Contains(tx sdk.Tx) (contains bool) { + defer func() { + if r := recover(); r != nil { + m.logger.Error("panic in Contains", "err", r) + contains = false + } + }() + + for _, lane := range m.lanes { + if lane.Contains(tx) { + return true + } + } + + return false +} diff --git a/app/mempool/mempool_test.go b/app/mempool/mempool_test.go new file mode 100644 index 000000000..3d886229d --- /dev/null +++ b/app/mempool/mempool_test.go @@ -0,0 +1,722 @@ +package mempool + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/suite" + + tmprototypes "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/cosmos/gogoproto/proto" + + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "cosmossdk.io/x/tx/signing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + txsigning "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// ----------------------------------------------------------------------------- +// Test Suite Setup +// ----------------------------------------------------------------------------- + +type Account struct { + PrivKey cryptotypes.PrivKey + PubKey cryptotypes.PubKey + Address sdk.AccAddress + ConsKey cryptotypes.PrivKey +} + +type MempoolTestSuite struct { + suite.Suite + + ctx sdk.Context + encodingConfig EncodingConfig + random *rand.Rand + accounts []Account + gasTokenDenom string +} + +func TestMempoolTestSuite(t *testing.T) { + suite.Run(t, new(MempoolTestSuite)) +} + +func (s *MempoolTestSuite) SetupTest() { + s.encodingConfig = CreateTestEncodingConfig() + + s.random = rand.New(rand.NewSource(1)) + s.accounts = RandomAccounts(s.random, 5) + s.gasTokenDenom = "uband" + + testCtx := testutil.DefaultContextWithDB( + s.T(), + storetypes.NewKVStoreKey("test"), + storetypes.NewTransientStoreKey("transient_test"), + ) + s.ctx = testCtx.Ctx.WithIsCheckTx(true) + s.ctx = s.ctx.WithBlockHeight(1) + + // Default consensus params + s.setBlockParams(1000000000000, 100) +} + +type EncodingConfig struct { + InterfaceRegistry types.InterfaceRegistry + Codec codec.Codec + TxConfig client.TxConfig + Amino *codec.LegacyAmino +} + +func CreateTestEncodingConfig() EncodingConfig { + legacyAmino := codec.NewLegacyAmino() + interfaceRegistry, err := types.NewInterfaceRegistryWithOptions(types.InterfaceRegistryOptions{ + ProtoFiles: proto.HybridResolver, + SigningOptions: signing.Options{ + AddressCodec: address.Bech32Codec{ + Bech32Prefix: sdk.GetConfig().GetBech32AccountAddrPrefix(), + }, + ValidatorAddressCodec: address.Bech32Codec{ + Bech32Prefix: sdk.GetConfig().GetBech32ValidatorAddrPrefix(), + }, + }, + }) + if err != nil { + panic(err) + } + + appCodec := codec.NewProtoCodec(interfaceRegistry) + txConfig := authtx.NewTxConfig(appCodec, authtx.DefaultSignModes) + + std.RegisterLegacyAminoCodec(legacyAmino) + std.RegisterInterfaces(interfaceRegistry) + + return EncodingConfig{ + InterfaceRegistry: interfaceRegistry, + Codec: appCodec, + TxConfig: txConfig, + Amino: legacyAmino, + } +} + +func RandomAccounts(r *rand.Rand, n int) []Account { + accs := make([]Account, n) + for i := 0; i < n; i++ { + pkSeed := make([]byte, 15) + r.Read(pkSeed) + + accs[i].PrivKey = secp256k1.GenPrivKeyFromSecret(pkSeed) + accs[i].PubKey = accs[i].PrivKey.PubKey() + accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) + + accs[i].ConsKey = ed25519.GenPrivKeyFromSecret(pkSeed) + } + return accs +} + +func (s *MempoolTestSuite) setBlockParams(maxBlockSize, maxBlockGas int64) { + s.ctx = s.ctx.WithConsensusParams( + tmprototypes.ConsensusParams{ + Block: &tmprototypes.BlockParams{ + MaxBytes: maxBlockSize, + MaxGas: maxBlockGas, + }, + }, + ) +} + +// ----------------------------------------------------------------------------- +// Create Mempool + Lanes +// ----------------------------------------------------------------------------- + +func (s *MempoolTestSuite) newMempool() *Mempool { + BankSendLane := NewLane( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + "bankSend", + isBankSendTx, + math.LegacyMustNewDecFromStr("0.2"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + DelegateLane := NewLane( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + "delegate", + isDelegateTx, + math.LegacyMustNewDecFromStr("0.2"), + math.LegacyMustNewDecFromStr("0.3"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + OtherLane := NewLane( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + "other", + isOtherTx, + math.LegacyMustNewDecFromStr("0.4"), + math.LegacyMustNewDecFromStr("0.4"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + lanes := []*Lane{BankSendLane, DelegateLane, OtherLane} + + return NewMempool( + log.NewTestLogger(s.T()), + lanes, + ) +} + +func isBankSendTx(_ sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if _, ok := msg.(*banktypes.MsgSend); !ok { + return false + } + } + return true +} + +func isDelegateTx(_ sdk.Context, tx sdk.Tx) bool { + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if _, ok := msg.(*stakingtypes.MsgDelegate); !ok { + return false + } + } + return true +} + +func isOtherTx(_ sdk.Context, tx sdk.Tx) bool { + // fallback if not pure bank send nor pure delegate + return true +} + +// ----------------------------------------------------------------------------- +// Individual Test Methods +// ----------------------------------------------------------------------------- + +// TestNoTransactions ensures no transactions exist => empty proposal +func (s *MempoolTestSuite) TestNoTransactions() { + mem := s.newMempool() + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Require().Equal(0, len(result.txs)) +} + +// TestSingleBankTx ensures a single bank tx is included +func (s *MempoolTestSuite) TestSingleBankTx() { + tx, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mem := s.newMempool() + s.Require().NoError(mem.Insert(s.ctx, tx)) + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + expectedIncludedTxs := s.getTxBytes(tx) + s.Require().Equal(1, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) +} + +// TestOneTxPerLane checks a single transaction in each lane type +func (s *MempoolTestSuite) TestOneTxPerLane() { + tx1, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + tx2, err := CreateDelegateTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + tx3, err := CreateMixedTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + 1, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mem := s.newMempool() + // Insert in reverse order to ensure ordering is correct + s.Require().NoError(mem.Insert(s.ctx, tx3)) + s.Require().NoError(mem.Insert(s.ctx, tx2)) + s.Require().NoError(mem.Insert(s.ctx, tx1)) + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + expectedIncludedTxs := s.getTxBytes(tx1, tx2, tx3) + s.Require().Equal(3, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) +} + +// TestTxOverLimit checks if a tx over the block limit is rejected +func (s *MempoolTestSuite) TestTxOverLimit() { + tx, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 101, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mem := s.newMempool() + s.Require().Error(mem.Insert(s.ctx, tx)) + + // Ensure the tx is rejected + for _, lane := range mem.lanes { + s.Require().Equal(0, lane.CountTx()) + } + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + + s.Require().Equal(0, len(result.txs)) +} + +// TestTxsOverGasLimit checks if txs over the gas limit are rejected +func (s *MempoolTestSuite) TestTxsOverGasLimit() { + bankTx1, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + bankTx2, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 1, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + delegateTx1, err := CreateDelegateTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + delegateTx2, err := CreateDelegateTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + otherTx1, err := CreateMixedTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + 40, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mem := s.newMempool() + // Insert in reverse order to ensure ordering is correct + s.Require().NoError(mem.Insert(s.ctx, otherTx1)) + s.Require().NoError(mem.Insert(s.ctx, delegateTx2)) + s.Require().NoError(mem.Insert(s.ctx, delegateTx1)) + s.Require().NoError(mem.Insert(s.ctx, bankTx2)) + s.Require().NoError(mem.Insert(s.ctx, bankTx1)) + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + // should not contain the otherTx1 + expectedIncludedTxs := s.getTxBytes(bankTx1, bankTx2, delegateTx1, delegateTx2) + s.Require().Equal(4, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) +} + +// TestFillUpLeftOverSpace checks if the proposal fills up the remaining space +func (s *MempoolTestSuite) TestFillUpLeftOverSpace() { + bankTx1, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + bankTx2, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 1, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + bankTx3, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 2, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + delegateTx1, err := CreateDelegateTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + delegateTx2, err := CreateDelegateTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 1, + 0, + 20, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + mem := s.newMempool() + // Insert in reverse order to ensure ordering is correct + s.Require().NoError(mem.Insert(s.ctx, delegateTx2)) + s.Require().NoError(mem.Insert(s.ctx, delegateTx1)) + s.Require().NoError(mem.Insert(s.ctx, bankTx3)) + s.Require().NoError(mem.Insert(s.ctx, bankTx2)) + s.Require().NoError(mem.Insert(s.ctx, bankTx1)) + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + // should contain bankTx3 as the last tx + expectedIncludedTxs := s.getTxBytes(bankTx1, bankTx2, delegateTx1, delegateTx2, bankTx3) + s.Require().Equal(5, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) +} + +func (s *MempoolTestSuite) TestDependencyBlockLane() { + DependentLane := NewLane( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + "dependent", + isOtherTx, + math.LegacyMustNewDecFromStr("0.5"), + math.LegacyMustNewDecFromStr("0.5"), + sdkmempool.DefaultPriorityMempool(), + nil, + ) + + DependencyLane := NewLane( + log.NewTestLogger(s.T()), + s.encodingConfig.TxConfig.TxEncoder(), + "dependency", + isBankSendTx, + math.LegacyMustNewDecFromStr("0.5"), + math.LegacyMustNewDecFromStr("0.5"), + sdkmempool.DefaultPriorityMempool(), + func(isLaneLimitExceeded bool) { + DependentLane.SetBlocked(isLaneLimitExceeded) + }, + ) + + lanes := []*Lane{DependencyLane, DependentLane} + + mem := NewMempool( + log.NewTestLogger(s.T()), + lanes, + ) + + bankTx1, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[0], + 0, + 0, + 30, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + MixedTx1, err := CreateMixedTx( + s.encodingConfig.TxConfig, + s.accounts[2], + 0, + 0, + 30, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + // Insert in reverse order to ensure ordering is correct + s.Require().NoError(mem.Insert(s.ctx, MixedTx1)) + s.Require().NoError(mem.Insert(s.ctx, bankTx1)) + + proposal := NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err := mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + expectedIncludedTxs := s.getTxBytes(bankTx1, MixedTx1) + s.Require().Equal(2, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) + + bankTx2, err := CreateBankSendTx( + s.encodingConfig.TxConfig, + s.accounts[1], + 0, + 0, + 30, + sdk.NewCoin(s.gasTokenDenom, math.NewInt(1000000)), + ) + s.Require().NoError(err) + + s.Require().NoError(mem.Insert(s.ctx, bankTx2)) + + proposal = NewProposal( + log.NewTestLogger(s.T()), + uint64(s.ctx.ConsensusParams().Block.MaxBytes), + uint64(s.ctx.ConsensusParams().Block.MaxGas), + ) + + result, err = mem.PrepareProposal(s.ctx, proposal) + s.Require().NoError(err) + s.Require().NotNil(result) + + expectedIncludedTxs = s.getTxBytes(bankTx1, bankTx2) + s.Require().Equal(2, len(result.txs)) + s.Require().Equal(expectedIncludedTxs, result.txs) +} + +// ----------------------------------------------------------------------------- +// Tx creation helpers +// ----------------------------------------------------------------------------- + +func CreateBankSendTx( + txCfg client.TxConfig, + account Account, + nonce, timeout uint64, + gasLimit uint64, + fees ...sdk.Coin, +) (authsigning.Tx, error) { + msgs := []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: account.Address.String(), + ToAddress: account.Address.String(), + }, + } + return buildTx(txCfg, account, msgs, nonce, timeout, gasLimit, fees...) +} + +func CreateDelegateTx( + txCfg client.TxConfig, + account Account, + nonce, timeout uint64, + gasLimit uint64, + fees ...sdk.Coin, +) (authsigning.Tx, error) { + msgs := []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: account.Address.String(), + ValidatorAddress: account.Address.String(), + }, + } + return buildTx(txCfg, account, msgs, nonce, timeout, gasLimit, fees...) +} + +// MixedTx includes both a bank send and delegate to ensure it goes to "other". +func CreateMixedTx( + txCfg client.TxConfig, + account Account, + nonce, timeout uint64, + gasLimit uint64, + fees ...sdk.Coin, +) (authsigning.Tx, error) { + msgs := []sdk.Msg{ + &banktypes.MsgSend{ + FromAddress: account.Address.String(), + ToAddress: account.Address.String(), + }, + &stakingtypes.MsgDelegate{ + DelegatorAddress: account.Address.String(), + ValidatorAddress: account.Address.String(), + }, + } + return buildTx(txCfg, account, msgs, nonce, timeout, gasLimit, fees...) +} + +func buildTx( + txCfg client.TxConfig, + account Account, + msgs []sdk.Msg, + nonce, timeout, gasLimit uint64, + fees ...sdk.Coin, +) (authsigning.Tx, error) { + txBuilder := txCfg.NewTxBuilder() + if err := txBuilder.SetMsgs(msgs...); err != nil { + return nil, err + } + + sigV2 := txsigning.SignatureV2{ + PubKey: account.PrivKey.PubKey(), + Data: &txsigning.SingleSignatureData{ + SignMode: txsigning.SignMode_SIGN_MODE_DIRECT, + Signature: nil, + }, + Sequence: nonce, + } + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + + txBuilder.SetTimeoutHeight(timeout) + txBuilder.SetFeeAmount(fees) + txBuilder.SetGasLimit(gasLimit) + + return txBuilder.GetTx(), nil +} + +// getTxBytes encodes the given transactions to raw bytes for comparison. +func (s *MempoolTestSuite) 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 +} + +// decodeTxs decodes the given TxWithInfo slice back into sdk.Tx for easy comparison. +func (s *MempoolTestSuite) decodeTxs(infos []TxWithInfo) []sdk.Tx { + res := make([]sdk.Tx, len(infos)) + for i, info := range infos { + tx, err := s.encodingConfig.TxConfig.TxDecoder()(info.TxBytes) + s.Require().NoError(err) + res[i] = tx + } + return res +} + +// extractTxBytes is a convenience to get a [][]byte from []TxWithInfo. +func extractTxBytes(txs []TxWithInfo) [][]byte { + bz := make([][]byte, len(txs)) + for i, tx := range txs { + bz[i] = tx.TxBytes + } + return bz +} diff --git a/app/mempool/proposal.go b/app/mempool/proposal.go new file mode 100644 index 000000000..99dcc1811 --- /dev/null +++ b/app/mempool/proposal.go @@ -0,0 +1,68 @@ +package mempool + +import ( + "fmt" + + "cosmossdk.io/log" +) + +// Proposal represents a block proposal under construction. +type Proposal struct { + logger log.Logger + + // txs is the list of transactions in the proposal. + txs [][]byte + // seen helps quickly check for duplicates by tx hash. + seen map[string]struct{} + // maxBlockSpace is the maximum block space available for this proposal. + maxBlockSpace BlockSpace + // totalBlockSpaceUsed is the total block space used by the proposal. + totalBlockSpaceUsed BlockSpace +} + +// NewProposal returns a new empty proposal constrained by max block size and max gas limit. +func NewProposal(logger log.Logger, maxBlockSize uint64, maxGasLimit uint64) Proposal { + return Proposal{ + logger: logger, + txs: make([][]byte, 0), + seen: make(map[string]struct{}), + maxBlockSpace: NewBlockSpace(maxBlockSize, maxGasLimit), + totalBlockSpaceUsed: NewBlockSpace(0, 0), + } +} + +// Contains returns true if the proposal already has a transaction with the given txHash. +func (p *Proposal) Contains(txHash string) bool { + _, ok := p.seen[txHash] + return ok +} + +// Add attempts to add a transaction to the proposal, respecting size/gas limits. +func (p *Proposal) Add(txInfo TxWithInfo) error { + if p.Contains(txInfo.Hash) { + return fmt.Errorf("transaction already in proposal: %s", txInfo.Hash) + } + + currentBlockSpaceUsed := p.totalBlockSpaceUsed.Add(txInfo.BlockSpace) + + // Check block size limit + if p.maxBlockSpace.IsExceededBy(currentBlockSpaceUsed) { + return fmt.Errorf( + "transaction space exceeds max block space: %s > %s", + currentBlockSpaceUsed.String(), p.maxBlockSpace.String(), + ) + } + + // Add transaction + p.txs = append(p.txs, txInfo.TxBytes) + p.seen[txInfo.Hash] = struct{}{} + + p.totalBlockSpaceUsed = currentBlockSpaceUsed + + return nil +} + +// GetRemainingBlockSpace returns the remaining block space available for the proposal. +func (p *Proposal) GetRemainingBlockSpace() BlockSpace { + return p.maxBlockSpace.Sub(p.totalBlockSpaceUsed) +} diff --git a/app/mempool/proposal_handler.go b/app/mempool/proposal_handler.go new file mode 100644 index 000000000..0d82ca6e0 --- /dev/null +++ b/app/mempool/proposal_handler.go @@ -0,0 +1,112 @@ +package mempool + +import ( + "fmt" + "math" + + abci "github.com/cometbft/cometbft/abci/types" + comettypes "github.com/cometbft/cometbft/types" + + "cosmossdk.io/log" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ProposalHandler wraps ABCI++ PrepareProposal/ProcessProposal for the Mempool. +type ProposalHandler struct { + logger log.Logger + txDecoder sdk.TxDecoder + mempool *Mempool +} + +// NewProposalHandler returns a new ABCI++ proposal handler for the Mempool. +func NewProposalHandler( + logger log.Logger, + txDecoder sdk.TxDecoder, + mempool *Mempool, +) *ProposalHandler { + return &ProposalHandler{ + logger: logger, + txDecoder: txDecoder, + mempool: mempool, + } +} + +// PrepareProposalHandler builds the next block proposal from the Mempool. +func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { + return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (resp *abci.ResponsePrepareProposal, err error) { + // For height <= 1, just return the default TXs (e.g., chain start). + if req.Height <= 1 { + return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil + } + + defer func() { + if rec := recover(); rec != nil { + h.logger.Error("failed to prepare proposal", "err", err) + resp = &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)} + err = fmt.Errorf("failed to prepare proposal: %v", rec) + } + }() + + h.logger.Info("preparing proposal from Mempool", "height", req.Height) + + // Gather block limits + maxBytesLimit, maxGasLimit := getBlockLimits(ctx) + if req.MaxTxBytes >= 0 { + maxBytesLimit = min(uint64(req.MaxTxBytes), maxBytesLimit) + } + + proposal := NewProposal( + h.logger, + maxBytesLimit, + maxGasLimit, + ) + + // Populate proposal from Mempool + finalProposal, err := h.mempool.PrepareProposal(ctx, proposal) + if err != nil { + // If an error occurs, we can still return what we have or choose to return nothing + h.logger.Error("failed to prepare proposal", "err", err) + return &abci.ResponsePrepareProposal{Txs: [][]byte{}}, err + } + + h.logger.Info( + "prepared proposal", + "num_txs", len(finalProposal.txs), + "total_block_space", finalProposal.totalBlockSpaceUsed.String(), + "max_block_space", finalProposal.maxBlockSpace.String(), + "height", req.Height, + ) + + return &abci.ResponsePrepareProposal{ + Txs: finalProposal.txs, + }, nil + } +} + +// ProcessProposalHandler returns a no-op process proposal handler. +func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { + return baseapp.NoOpProcessProposal() +} + +// getBlockLimits retrieves the maximum block size and gas limit from context. +func getBlockLimits(ctx sdk.Context) (uint64, uint64) { + blockParams := ctx.ConsensusParams().Block + + var maxBytesLimit uint64 + if blockParams.MaxBytes == -1 { + maxBytesLimit = uint64(comettypes.MaxBlockSizeBytes) + } else { + maxBytesLimit = uint64(blockParams.MaxBytes) + } + + var maxGasLimit uint64 + if blockParams.MaxGas == -1 { + maxGasLimit = math.MaxUint64 + } else { + maxGasLimit = uint64(blockParams.MaxGas) + } + + return maxBytesLimit, maxGasLimit +} diff --git a/app/mempool/types.go b/app/mempool/types.go new file mode 100644 index 000000000..561462670 --- /dev/null +++ b/app/mempool/types.go @@ -0,0 +1,73 @@ +package mempool + +import ( + "reflect" + "slices" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// TxWithInfo holds metadata required for a transaction to be included in a proposal. +type TxWithInfo struct { + // Hash is the hex-encoded hash of the transaction. + Hash string + // BlockSpace is the block space used by the transaction. + BlockSpace BlockSpace + // TxBytes is the raw transaction bytes. + TxBytes []byte +} + +type TxMatchFn func(sdk.Context, sdk.Tx) bool + +func NewLaneTxMatchFn(msgs []sdk.Msg, onlyFree bool) TxMatchFn { + msgTypes := make([]reflect.Type, len(msgs)) + + for i, msg := range msgs { + msgTypes[i] = reflect.TypeOf(msg) + } + + var matchMsgFn func(sdk.Msg) bool + + matchMsgFn = func(msg sdk.Msg) bool { + msgExec, ok := msg.(*authz.MsgExec) + if ok { + subMsgs, err := msgExec.GetMessages() + if err != nil { + return false + } + for _, m := range subMsgs { + if !matchMsgFn(m) { + return false + } + } + return true + } else { + return slices.Contains(msgTypes, reflect.TypeOf(msg)) + } + } + + return func(_ sdk.Context, tx sdk.Tx) bool { + if onlyFree { + gasTx, ok := tx.(sdk.FeeTx) + if !ok { + return false + } + + if !gasTx.GetFee().IsZero() { + return false + } + } + + msgs := tx.GetMsgs() + if len(msgs) == 0 { + return false + } + for _, msg := range msgs { + if !matchMsgFn(msg) { + return false + } + } + return true + } +} diff --git a/cylinder/README.md b/cylinder/README.md index 72750823e..27f8354d8 100644 --- a/cylinder/README.md +++ b/cylinder/README.md @@ -109,7 +109,7 @@ cylinder config chain-id $CHAIN_ID --home $CYLINDER_HOME_PATH cylinder config node $RPC_URL --home $CYLINDER_HOME_PATH cylinder config granter $(bandd keys show $WALLET_NAME -a --keyring-backend test) --home $CYLINDER_HOME_PATH cylinder config gas-prices "0uband" --home $CYLINDER_HOME_PATH -cylinder config max-messages 20 --home $CYLINDER_HOME_PATH +cylinder config max-messages 10 --home $CYLINDER_HOME_PATH cylinder config broadcast-timeout "5m" --home $CYLINDER_HOME_PATH cylinder config rpc-poll-interval "1s" --home $CYLINDER_HOME_PATH cylinder config max-try 5 --home $CYLINDER_HOME_PATH diff --git a/go.mod b/go.mod index d58e20257..5252f8bf6 100644 --- a/go.mod +++ b/go.mod @@ -100,7 +100,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect @@ -138,7 +138,7 @@ require ( github.com/hashicorp/go-metrics v0.5.3 // indirect github.com/hashicorp/go-plugin v1.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0c3e4c9fa..a90bd8bd0 100644 --- a/go.sum +++ b/go.sum @@ -448,8 +448,8 @@ github.com/ethereum/go-ethereum v1.14.13 h1:L81Wmv0OUP6cf4CW6wtXsr23RUrDhKs2+Y9Q github.com/ethereum/go-ethereum v1.14.13/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -703,8 +703,9 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/scripts/bandtss/start_cylinder.sh b/scripts/bandtss/start_cylinder.sh index 93c7d54e9..0247f6084 100755 --- a/scripts/bandtss/start_cylinder.sh +++ b/scripts/bandtss/start_cylinder.sh @@ -19,7 +19,7 @@ cylinder config chain-id bandchain --home $HOME_PATH cylinder config granter $(bandd keys show $KEY -a --keyring-backend test) --home $HOME_PATH # setup max-messages to cylinder config -cylinder config max-messages 20 --home $HOME_PATH +cylinder config max-messages 10 --home $HOME_PATH # setup broadcast-timeout to cylinder config cylinder config broadcast-timeout "5m" --home $HOME_PATH diff --git a/x/globalfee/feechecker/feechecker.go b/x/globalfee/feechecker/feechecker.go index c4766fd62..13aefc5ec 100644 --- a/x/globalfee/feechecker/feechecker.go +++ b/x/globalfee/feechecker/feechecker.go @@ -1,66 +1,27 @@ package feechecker import ( - "math" - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/authz" - authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - bandtsskeeper "github.com/bandprotocol/chain/v3/x/bandtss/keeper" - feedskeeper "github.com/bandprotocol/chain/v3/x/feeds/keeper" - feedstypes "github.com/bandprotocol/chain/v3/x/feeds/types" "github.com/bandprotocol/chain/v3/x/globalfee/keeper" - oraclekeeper "github.com/bandprotocol/chain/v3/x/oracle/keeper" - oracletypes "github.com/bandprotocol/chain/v3/x/oracle/types" - tsskeeper "github.com/bandprotocol/chain/v3/x/tss/keeper" - tsstypes "github.com/bandprotocol/chain/v3/x/tss/types" ) type FeeChecker struct { - cdc codec.Codec - - AuthzKeeper *authzkeeper.Keeper - OracleKeeper *oraclekeeper.Keeper GlobalfeeKeeper *keeper.Keeper StakingKeeper *stakingkeeper.Keeper - TSSKeeper *tsskeeper.Keeper - BandtssKeeper *bandtsskeeper.Keeper - FeedsKeeper *feedskeeper.Keeper - - TSSMsgServer tsstypes.MsgServer - FeedsMsgServer feedstypes.MsgServer } func NewFeeChecker( - cdc codec.Codec, - authzKeeper *authzkeeper.Keeper, - oracleKeeper *oraclekeeper.Keeper, globalfeeKeeper *keeper.Keeper, stakingKeeper *stakingkeeper.Keeper, - tssKeeper *tsskeeper.Keeper, - bandtssKeeper *bandtsskeeper.Keeper, - feedsKeeper *feedskeeper.Keeper, ) FeeChecker { - tssMsgServer := tsskeeper.NewMsgServerImpl(tssKeeper) - feedsMsgServer := feedskeeper.NewMsgServerImpl(*feedsKeeper) - return FeeChecker{ - cdc: cdc, - AuthzKeeper: authzKeeper, - OracleKeeper: oracleKeeper, GlobalfeeKeeper: globalfeeKeeper, StakingKeeper: stakingKeeper, - TSSKeeper: tssKeeper, - BandtssKeeper: bandtssKeeper, - FeedsKeeper: feedsKeeper, - TSSMsgServer: tssMsgServer, - FeedsMsgServer: feedsMsgServer, } } @@ -89,11 +50,6 @@ func (fc FeeChecker) CheckTxFee( return feeCoins, priority, nil } - // Check if this tx should be free or not - if fc.IsBypassMinFeeTx(ctx, tx) { - return sdk.Coins{}, int64(math.MaxInt64), nil - } - minGasPrices := getMinGasPrices(ctx) globalMinGasPrices, err := fc.GetGlobalMinGasPrices(ctx) if err != nil { @@ -125,106 +81,6 @@ func (fc FeeChecker) CheckTxFee( return feeCoins, priority, nil } -// IsBypassMinFeeTx checks whether tx is min fee bypassable. -func (fc FeeChecker) IsBypassMinFeeTx(ctx sdk.Context, tx sdk.Tx) bool { - newCtx, _ := ctx.CacheContext() - - // Check if all messages are free - for _, msg := range tx.GetMsgs() { - if !fc.IsBypassMinFeeMsg(newCtx, msg) { - return false - } - } - - return true -} - -// IsBypassMinFeeMsg checks whether msg is min fee bypassable. -func (fc FeeChecker) IsBypassMinFeeMsg(ctx sdk.Context, msg sdk.Msg) bool { - switch msg := msg.(type) { - case *oracletypes.MsgReportData: - if err := checkValidMsgReport(ctx, fc.OracleKeeper, msg); err != nil { - return false - } - case *feedstypes.MsgSubmitSignalPrices: - if _, err := fc.FeedsMsgServer.SubmitSignalPrices(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgSubmitDKGRound1: - if _, err := fc.TSSMsgServer.SubmitDKGRound1(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgSubmitDKGRound2: - if _, err := fc.TSSMsgServer.SubmitDKGRound2(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgConfirm: - if _, err := fc.TSSMsgServer.Confirm(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgComplain: - if _, err := fc.TSSMsgServer.Complain(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgSubmitDEs: - acc, err := sdk.AccAddressFromBech32(msg.Sender) - if err != nil { - return false - } - - currentGroupID := fc.BandtssKeeper.GetCurrentGroup(ctx).GroupID - incomingGroupID := fc.BandtssKeeper.GetIncomingGroupID(ctx) - if !fc.BandtssKeeper.HasMember(ctx, acc, currentGroupID) && - !fc.BandtssKeeper.HasMember(ctx, acc, incomingGroupID) { - return false - } - - if _, err := fc.TSSMsgServer.SubmitDEs(ctx, msg); err != nil { - return false - } - case *tsstypes.MsgSubmitSignature: - if _, err := fc.TSSMsgServer.SubmitSignature(ctx, msg); err != nil { - return false - } - case *authz.MsgExec: - msgs, err := msg.GetMessages() - if err != nil { - return false - } - - grantee, err := sdk.AccAddressFromBech32(msg.Grantee) - if err != nil { - return false - } - - for _, m := range msgs { - signers, _, err := fc.cdc.GetMsgV1Signers(m) - if err != nil { - return false - } - // Check if this grantee have authorization for the message. - cap, _ := fc.AuthzKeeper.GetAuthorization( - ctx, - grantee, - sdk.AccAddress(signers[0]), - sdk.MsgTypeURL(m), - ) - if cap == nil { - return false - } - - // Check if this message should be free or not. - if !fc.IsBypassMinFeeMsg(ctx, m) { - return false - } - } - default: - return false - } - - return true -} - // GetGlobalMinGasPrices returns global min gas prices func (fc FeeChecker) GetGlobalMinGasPrices(ctx sdk.Context) (sdk.DecCoins, error) { globalMinGasPrices := fc.GlobalfeeKeeper.GetParams(ctx).MinimumGasPrices diff --git a/x/globalfee/feechecker/feechecker_test.go b/x/globalfee/feechecker/feechecker_test.go index 442c6252f..c7245a8ad 100644 --- a/x/globalfee/feechecker/feechecker_test.go +++ b/x/globalfee/feechecker/feechecker_test.go @@ -1,9 +1,7 @@ package feechecker_test import ( - "math" "testing" - "time" "github.com/stretchr/testify/suite" protov2 "google.golang.org/protobuf/proto" @@ -16,16 +14,10 @@ import ( "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/authz" - "github.com/bandprotocol/chain/v3/pkg/tss" bandtesting "github.com/bandprotocol/chain/v3/testing" - bandtsstypes "github.com/bandprotocol/chain/v3/x/bandtss/types" - feedstypes "github.com/bandprotocol/chain/v3/x/feeds/types" "github.com/bandprotocol/chain/v3/x/globalfee/feechecker" oracletypes "github.com/bandprotocol/chain/v3/x/oracle/types" - tsstypes "github.com/bandprotocol/chain/v3/x/tss/types" ) var ( @@ -81,12 +73,6 @@ func (suite *FeeCheckerTestSuite) SetupTest() { app := bandtesting.SetupWithCustomHome(false, dir) ctx := app.BaseApp.NewUncachedContext(false, cmtproto.Header{}) - // Activate validators - for _, v := range bandtesting.Validators { - err := app.OracleKeeper.Activate(ctx, v.ValAddress) - suite.Require().NoError(err) - } - _, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: app.LastBlockHeight() + 1}) suite.Require().NoError(err) _, err = app.Commit() @@ -96,695 +82,10 @@ func (suite *FeeCheckerTestSuite) SetupTest() { WithIsCheckTx(true). WithMinGasPrices(sdk.DecCoins{{Denom: "uband", Amount: sdkmath.LegacyNewDecWithPrec(1, 4)}}) - err = app.OracleKeeper.GrantReporter(suite.ctx, bandtesting.Validators[0].ValAddress, bandtesting.Alice.Address) - suite.Require().NoError(err) - - expiration := ctx.BlockTime().Add(1000 * time.Hour) - - msgTypeURLs := []sdk.Msg{&tsstypes.MsgSubmitDEs{}, &feedstypes.MsgSubmitSignalPrices{}} - for _, msg := range msgTypeURLs { - err = app.AuthzKeeper.SaveGrant( - ctx, - bandtesting.Alice.Address, - bandtesting.Validators[0].Address, - authz.NewGenericAuthorization(sdk.MsgTypeURL(msg)), - &expiration, - ) - suite.Require().NoError(err) - } - - // mock setup bandtss module - app.BandtssKeeper.SetCurrentGroup(ctx, bandtsstypes.NewCurrentGroup(1, ctx.BlockTime())) - app.BandtssKeeper.SetMember(ctx, bandtsstypes.Member{ - Address: bandtesting.Validators[0].Address.String(), - IsActive: true, - GroupID: 1, - }) - - req := oracletypes.NewRequest( - 1, - BasicCalldata, - []sdk.ValAddress{bandtesting.Validators[0].ValAddress}, - 1, - 1, - bandtesting.ParseTime(0), - "", - nil, - nil, - 0, - 0, - bandtesting.FeePayer.Address.String(), - bandtesting.Coins100band, - ) - suite.requestID = app.OracleKeeper.AddRequest(suite.ctx, req) - suite.FeeChecker = feechecker.NewFeeChecker( - app.AppCodec(), - &app.AuthzKeeper, - &app.OracleKeeper, &app.GlobalFeeKeeper, app.StakingKeeper, - app.TSSKeeper, - &app.BandtssKeeper, - &app.FeedsKeeper, - ) -} - -func (suite *FeeCheckerTestSuite) TestValidRawReport() { - msgs := []sdk.Msg{ - oracletypes.NewMsgReportData(suite.requestID, []oracletypes.RawReport{}, bandtesting.Validators[0].ValAddress), - } - stubTx := &StubTx{Msgs: msgs} - - // test - check report tx - isReportTx := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, msgs[0]) - suite.Require().True(isReportTx) - - // test - check tx fee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) - suite.Require().Equal(sdk.Coins{}, fee) - suite.Require().Equal(int64(math.MaxInt64), priority) -} - -func (suite *FeeCheckerTestSuite) TestNotValidRawReport() { - msgs := []sdk.Msg{oracletypes.NewMsgReportData(1, []oracletypes.RawReport{}, bandtesting.Alice.ValAddress)} - stubTx := &StubTx{Msgs: msgs} - - // test - check report tx - isReportTx := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, msgs[0]) - suite.Require().False(isReportTx) - - // test - check tx fee - _, _, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().Error(err) -} - -func (suite *FeeCheckerTestSuite) TestValidReport() { - reportMsgs := []sdk.Msg{ - oracletypes.NewMsgReportData(suite.requestID, []oracletypes.RawReport{}, bandtesting.Validators[0].ValAddress), - } - authzMsg := authz.NewMsgExec(bandtesting.Alice.Address, reportMsgs) - stubTx := &StubTx{Msgs: []sdk.Msg{&authzMsg}} - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, reportMsgs[0]) - suite.Require().True(isBypassMinFeeMsg) - - // test - check tx fee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) - suite.Require().Equal(sdk.Coins{}, fee) - suite.Require().Equal(int64(math.MaxInt64), priority) -} - -func (suite *FeeCheckerTestSuite) TestNoAuthzReport() { - reportMsgs := []sdk.Msg{ - oracletypes.NewMsgReportData(suite.requestID, []oracletypes.RawReport{}, bandtesting.Validators[0].ValAddress), - } - authzMsg := authz.NewMsgExec(bandtesting.Bob.Address, reportMsgs) - stubTx := &StubTx{ - Msgs: []sdk.Msg{&authzMsg}, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, &authzMsg) - suite.Require().False(isBypassMinFeeMsg) - - // test - check tx fee - _, _, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) -} - -func (suite *FeeCheckerTestSuite) TestNotValidReport() { - reportMsgs := []sdk.Msg{ - oracletypes.NewMsgReportData( - suite.requestID+1, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ), - } - authzMsg := authz.NewMsgExec(bandtesting.Alice.Address, reportMsgs) - stubTx := &StubTx{Msgs: []sdk.Msg{&authzMsg}} - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, &authzMsg) - suite.Require().False(isBypassMinFeeMsg) - - // test - check tx fee - _, _, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().Error(err) -} - -func (suite *FeeCheckerTestSuite) TestNotReportMsg() { - requestMsg := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, ) - stubTx := &StubTx{ - Msgs: []sdk.Msg{requestMsg}, - GasPrices: sdk.NewDecCoins( - sdk.NewDecCoinFromDec("uaaaa", sdkmath.LegacyNewDecWithPrec(100, 3)), - sdk.NewDecCoinFromDec("uaaab", sdkmath.LegacyNewDecWithPrec(1, 3)), - sdk.NewDecCoinFromDec("uaaac", sdkmath.LegacyNewDecWithPrec(0, 3)), - sdk.NewDecCoinFromDec("uband", sdkmath.LegacyNewDecWithPrec(3, 3)), - sdk.NewDecCoinFromDec("uccca", sdkmath.LegacyNewDecWithPrec(0, 3)), - sdk.NewDecCoinFromDec("ucccb", sdkmath.LegacyNewDecWithPrec(1, 3)), - sdk.NewDecCoinFromDec("ucccc", sdkmath.LegacyNewDecWithPrec(100, 3)), - ), - } - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, requestMsg) - suite.Require().False(isBypassMinFeeMsg) - - // test - check tx fee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) - suite.Require().Equal(stubTx.GetFee(), fee) - suite.Require().Equal(int64(30), priority) -} - -func (suite *FeeCheckerTestSuite) TestReportMsgAndOthersTypeMsgInTheSameAuthzMsgs() { - reportMsg := oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ) - requestMsg := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, - ) - msgs := []sdk.Msg{reportMsg, requestMsg} - authzMsg := authz.NewMsgExec(bandtesting.Alice.Address, msgs) - stubTx := &StubTx{ - Msgs: []sdk.Msg{&authzMsg}, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, &authzMsg) - suite.Require().False(isBypassMinFeeMsg) - - // test - check tx fee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) - suite.Require().Equal(stubTx.GetFee(), fee) - suite.Require().Equal(int64(10000), priority) -} - -func (suite *FeeCheckerTestSuite) TestReportMsgAndOthersTypeMsgInTheSameTx() { - reportMsg := oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ) - requestMsg := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, - ) - stubTx := &StubTx{ - Msgs: []sdk.Msg{reportMsg, requestMsg}, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - - // test - check bypass min fee - isBypassMinFeeMsg := suite.FeeChecker.IsBypassMinFeeMsg(suite.ctx, requestMsg) - suite.Require().False(isBypassMinFeeMsg) - - // test - check tx fee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().NoError(err) - suite.Require().Equal(stubTx.GetFee(), fee) - suite.Require().Equal(int64(10000), priority) -} - -func (suite *FeeCheckerTestSuite) TestIsBypassMinFeeTxAndCheckTxFee() { - testCases := []struct { - name string - stubTx func() *StubTx - expIsBypassMinFeeTx bool - expErr error - expFee sdk.Coins - expPriority int64 - }{ - { - name: "valid MsgReportData", - stubTx: func() *StubTx { - return &StubTx{ - Msgs: []sdk.Msg{ - oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ), - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "valid MsgSubmitDEs", - stubTx: func() *StubTx { - privD, _ := tss.GenerateSigningNonce([]byte{}) - privE, _ := tss.GenerateSigningNonce([]byte{}) - - return &StubTx{ - Msgs: []sdk.Msg{ - &tsstypes.MsgSubmitDEs{ - DEs: []tsstypes.DE{ - { - PubD: privD.Point(), - PubE: privE.Point(), - }, - }, - Sender: bandtesting.Validators[0].Address.String(), - }, - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "invalid MsgSubmitDEs", - stubTx: func() *StubTx { - return &StubTx{ - Msgs: []sdk.Msg{ - &tsstypes.MsgSubmitDEs{ - DEs: []tsstypes.DE{ - { - PubD: nil, - PubE: nil, - }, - }, - Sender: "wrong address", - }, - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "valid MsgSubmitDEs in valid MsgExec", - stubTx: func() *StubTx { - privD, _ := tss.GenerateSigningNonce([]byte{}) - privE, _ := tss.GenerateSigningNonce([]byte{}) - - msgExec := authz.NewMsgExec(bandtesting.Alice.Address, []sdk.Msg{ - &tsstypes.MsgSubmitDEs{ - DEs: []tsstypes.DE{ - { - PubD: privD.Point(), - PubE: privE.Point(), - }, - }, - Sender: bandtesting.Validators[0].Address.String(), - }, - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "valid MsgSubmitDEs in invalid MsgExec", - stubTx: func() *StubTx { - privD, _ := tss.GenerateSigningNonce([]byte{}) - privE, _ := tss.GenerateSigningNonce([]byte{}) - - msgExec := authz.NewMsgExec(bandtesting.Bob.Address, []sdk.Msg{ - &tsstypes.MsgSubmitDEs{ - DEs: []tsstypes.DE{ - { - PubD: privD.Point(), - PubE: privE.Point(), - }, - }, - Sender: bandtesting.Validators[0].Address.String(), - }, - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "valid MsgReportData in valid MsgExec", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Alice.Address, []sdk.Msg{ - oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "invalid MsgReportData with not enough fee", - stubTx: func() *StubTx { - return &StubTx{ - Msgs: []sdk.Msg{ - oracletypes.NewMsgReportData(1, []oracletypes.RawReport{}, bandtesting.Alice.ValAddress), - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "invalid MsgReportData in valid MsgExec with not enough fee", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Alice.Address, []sdk.Msg{ - oracletypes.NewMsgReportData( - suite.requestID+1, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "valid MsgReportData in invalid MsgExec with enough fee", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Bob.Address, []sdk.Msg{ - oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - }, - expIsBypassMinFeeTx: false, - expErr: nil, - expFee: sdk.NewCoins(sdk.NewCoin("uband", sdkmath.NewInt(1000000))), - expPriority: 10000, - }, - { - name: "valid MsgRequestData", - stubTx: func() *StubTx { - msgRequestData := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, - ) - - return &StubTx{ - Msgs: []sdk.Msg{msgRequestData}, - GasPrices: sdk.NewDecCoins( - sdk.NewDecCoinFromDec("uaaaa", sdkmath.LegacyNewDecWithPrec(100, 3)), - sdk.NewDecCoinFromDec("uaaab", sdkmath.LegacyNewDecWithPrec(1, 3)), - sdk.NewDecCoinFromDec("uaaac", sdkmath.LegacyNewDecWithPrec(0, 3)), - sdk.NewDecCoinFromDec("uband", sdkmath.LegacyNewDecWithPrec(3, 3)), - sdk.NewDecCoinFromDec("uccca", sdkmath.LegacyNewDecWithPrec(0, 3)), - sdk.NewDecCoinFromDec("ucccb", sdkmath.LegacyNewDecWithPrec(1, 3)), - sdk.NewDecCoinFromDec("ucccc", sdkmath.LegacyNewDecWithPrec(100, 3)), - ), - } - }, - expIsBypassMinFeeTx: false, - expErr: nil, - expFee: sdk.NewCoins( - sdk.NewCoin("uaaaa", sdkmath.NewInt(100000)), - sdk.NewCoin("uaaab", sdkmath.NewInt(1000)), - sdk.NewCoin("uband", sdkmath.NewInt(3000)), - sdk.NewCoin("ucccb", sdkmath.NewInt(1000)), - sdk.NewCoin("ucccc", sdkmath.NewInt(100000)), - ), - expPriority: 30, - }, - { - name: "valid MsgRequestData and valid MsgReport in valid MsgExec with enough fee", - stubTx: func() *StubTx { - msgReportData := oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ) - msgRequestData := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, - ) - msgs := []sdk.Msg{msgReportData, msgRequestData} - authzMsg := authz.NewMsgExec(bandtesting.Alice.Address, msgs) - - return &StubTx{ - Msgs: []sdk.Msg{&authzMsg}, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - }, - expIsBypassMinFeeTx: false, - expErr: nil, - expFee: sdk.NewCoins( - sdk.NewCoin("uband", sdkmath.NewInt(1000000)), - ), - expPriority: 10000, - }, - { - name: "valid MsgRequestData and valid MsgReport with enough fee", - stubTx: func() *StubTx { - msgReportData := oracletypes.NewMsgReportData( - suite.requestID, - []oracletypes.RawReport{}, - bandtesting.Validators[0].ValAddress, - ) - msgRequestData := oracletypes.NewMsgRequestData( - 1, - BasicCalldata, - 1, - 1, - BasicClientID, - bandtesting.Coins100band, - bandtesting.TestDefaultPrepareGas, - bandtesting.TestDefaultExecuteGas, - bandtesting.FeePayer.Address, - 0, - ) - - return &StubTx{ - Msgs: []sdk.Msg{msgReportData, msgRequestData}, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - }, - expIsBypassMinFeeTx: false, - expErr: nil, - expFee: sdk.NewCoins( - sdk.NewCoin("uband", sdkmath.NewInt(1000000)), - ), - expPriority: 10000, - }, - { - name: "valid MsgSubmitSignalPrices", - stubTx: func() *StubTx { - return &StubTx{ - Msgs: []sdk.Msg{ - feedstypes.NewMsgSubmitSignalPrices( - bandtesting.Validators[0].ValAddress.String(), - suite.ctx.BlockTime().Unix(), - []feedstypes.SignalPrice{}, - ), - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "valid MsgSubmitSignalPrices in valid MsgExec", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Alice.Address, []sdk.Msg{ - feedstypes.NewMsgSubmitSignalPrices( - bandtesting.Validators[0].ValAddress.String(), - suite.ctx.BlockTime().Unix(), - []feedstypes.SignalPrice{}, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: true, - expErr: nil, - expFee: sdk.Coins{}, - expPriority: math.MaxInt64, - }, - { - name: "invalid MsgSubmitSignalPrices with not enough fee", - stubTx: func() *StubTx { - return &StubTx{ - Msgs: []sdk.Msg{ - feedstypes.NewMsgSubmitSignalPrices( - bandtesting.Alice.ValAddress.String(), - suite.ctx.BlockTime().Unix(), - []feedstypes.SignalPrice{}, - ), - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "invalid MsgSubmitSignalPrices in valid MsgExec with not enough fee", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Alice.Address, []sdk.Msg{ - feedstypes.NewMsgSubmitSignalPrices( - bandtesting.Alice.ValAddress.String(), - suite.ctx.BlockTime().Unix(), - []feedstypes.SignalPrice{}, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - } - }, - expIsBypassMinFeeTx: false, - expErr: sdkerrors.ErrInsufficientFee, - expFee: nil, - expPriority: 0, - }, - { - name: "valid MsgSubmitSignalPrices in invalid MsgExec with enough fee", - stubTx: func() *StubTx { - msgExec := authz.NewMsgExec(bandtesting.Bob.Address, []sdk.Msg{ - feedstypes.NewMsgSubmitSignalPrices( - bandtesting.Validators[0].ValAddress.String(), - suite.ctx.BlockTime().Unix(), - []feedstypes.SignalPrice{}, - ), - }) - - return &StubTx{ - Msgs: []sdk.Msg{ - &msgExec, - }, - GasPrices: sdk.NewDecCoins(sdk.NewDecCoin("uband", sdkmath.NewInt(1))), - } - }, - expIsBypassMinFeeTx: false, - expErr: nil, - expFee: sdk.NewCoins(sdk.NewCoin("uband", sdkmath.NewInt(1000000))), - expPriority: 10000, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - stubTx := tc.stubTx() - - // test - IsByPassMinFeeTx - isByPassMinFeeTx := suite.FeeChecker.IsBypassMinFeeTx(suite.ctx, stubTx) - suite.Require().Equal(tc.expIsBypassMinFeeTx, isByPassMinFeeTx) - - // test - CheckTxFee - fee, priority, err := suite.FeeChecker.CheckTxFee(suite.ctx, stubTx) - suite.Require().ErrorIs(err, tc.expErr) - suite.Require().Equal(fee, tc.expFee) - suite.Require().Equal(tc.expPriority, priority) - }) - } } func (suite *FeeCheckerTestSuite) TestDefaultZeroGlobalFee() { diff --git a/x/globalfee/feechecker/utils.go b/x/globalfee/feechecker/utils.go index c44c0bca2..5c4986c98 100644 --- a/x/globalfee/feechecker/utils.go +++ b/x/globalfee/feechecker/utils.go @@ -4,9 +4,6 @@ import ( "math" sdk "github.com/cosmos/cosmos-sdk/types" - - oraclekeeper "github.com/bandprotocol/chain/v3/x/oracle/keeper" - oracletypes "github.com/bandprotocol/chain/v3/x/oracle/types" ) // getTxPriority returns priority of the provided fee based on gas prices of uband @@ -57,12 +54,3 @@ func CombinedGasPricesRequirement(globalMinGasPrices, minGasPrices sdk.DecCoins) return allGasPrices.Sort() } - -func checkValidMsgReport(ctx sdk.Context, oracleKeeper *oraclekeeper.Keeper, msg *oracletypes.MsgReportData) error { - validator, err := sdk.ValAddressFromBech32(msg.Validator) - if err != nil { - return err - } - - return oracleKeeper.CheckValidReport(ctx, msg.RequestID, validator, msg.RawReports) -}