diff --git a/server/v2/commands.go b/server/v2/commands.go index a0b2b05ae902..101a23d3983e 100644 --- a/server/v2/commands.go +++ b/server/v2/commands.go @@ -38,13 +38,14 @@ func AddCommands[T transaction.Tx]( rootCmd *cobra.Command, newApp AppCreator[T], logger log.Logger, + serverCfg ServerConfig, components ...ServerComponent[T], ) error { if len(components) == 0 { return errors.New("no components provided") } - server := NewServer(logger, components...) + server := NewServer(logger, serverCfg, components...) originalPersistentPreRunE := rootCmd.PersistentPreRunE rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // set the default command outputs diff --git a/server/v2/config.go b/server/v2/config.go index 7ce73fe88a9a..b5c525fdf682 100644 --- a/server/v2/config.go +++ b/server/v2/config.go @@ -7,6 +7,18 @@ import ( "github.com/spf13/viper" ) +// ServerConfig defines configuration for the server component. +type ServerConfig struct { + MinGasPrices string `mapstructure:"minimum-gas-prices" toml:"minimum-gas-prices" comment:"minimum-gas-prices defines the price which a validator is willing to accept for processing a transaction. A transaction's fees must meet the minimum of any denomination specified in this config (e.g. 0.25token1;0.0001token2)."` +} + +// DefaultServerConfig returns the default config of server component +func DefaultServerConfig() ServerConfig { + return ServerConfig{ + MinGasPrices: "0stake", + } +} + // ReadConfig returns a viper instance of the config file func ReadConfig(configPath string) (*viper.Viper, error) { v := viper.New() diff --git a/server/v2/flags.go b/server/v2/flags.go index a86154eb769c..fc36cdcf2b4c 100644 --- a/server/v2/flags.go +++ b/server/v2/flags.go @@ -1,6 +1,16 @@ // Package serverv2 defines constants for server configuration flags and output formats. package serverv2 +import "fmt" + +// start flags are prefixed with the server name +// this allows viper to properly bind the flags +func prefix(f string) string { + return fmt.Sprintf("%s.%s", serverName, f) +} + +var FlagMinGasPrices = prefix("minimum-gas-prices") + const ( // FlagHome specifies the home directory flag. FlagHome = "home" diff --git a/server/v2/server.go b/server/v2/server.go index 6e1a4e7114db..08ae9a6a06fa 100644 --- a/server/v2/server.go +++ b/server/v2/server.go @@ -56,25 +56,32 @@ type CLIConfig struct { Txs []*cobra.Command } +const ( + serverName = "server" +) + var _ ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil) type Server[T transaction.Tx] struct { logger log.Logger components []ServerComponent[T] + config ServerConfig } func NewServer[T transaction.Tx]( logger log.Logger, + config ServerConfig, components ...ServerComponent[T], ) *Server[T] { return &Server[T]{ logger: logger, + config: config, components: components, } } func (s *Server[T]) Name() string { - return "server" + return serverName } // Start starts all components concurrently. @@ -151,9 +158,19 @@ func (s *Server[T]) CLICommands() CLIConfig { return commands } +// Config returns config of the server component +func (s *Server[T]) Config() ServerConfig { + return s.config +} + // Configs returns all configs of all server components. func (s *Server[T]) Configs() map[string]any { cfgs := make(map[string]any) + + // add server component config + cfgs[s.Name()] = s.config + + // add other components' config for _, mod := range s.components { if configmod, ok := mod.(HasConfig); ok { cfg := configmod.Config() @@ -164,9 +181,22 @@ func (s *Server[T]) Configs() map[string]any { return cfgs } +func (s *Server[T]) StartCmdFlags() *pflag.FlagSet { + flags := pflag.NewFlagSet(s.Name(), pflag.ExitOnError) + flags.String(FlagMinGasPrices, "", "Minimum gas prices to accept for transactions; Any fee in a tx must meet this minimum (e.g. 0.01photino;0.0001stake)") + return flags +} + // Init initializes all server components with the provided application, configuration, and logger. // It returns an error if any component fails to initialize. func (s *Server[T]) Init(appI AppI[T], v *viper.Viper, logger log.Logger) error { + cfg := s.config + if v != nil { + if err := UnmarshalSubConfig(v, s.Name(), &cfg); err != nil { + return fmt.Errorf("failed to unmarshal config: %w", err) + } + } + var components []ServerComponent[T] for _, mod := range s.components { mod := mod @@ -177,6 +207,7 @@ func (s *Server[T]) Init(appI AppI[T], v *viper.Viper, logger log.Logger) error components = append(components, mod) } + s.config = cfg s.components = components return nil } @@ -217,6 +248,11 @@ func (s *Server[T]) WriteConfig(configPath string) error { // StartFlags returns all flags of all server components. func (s *Server[T]) StartFlags() []*pflag.FlagSet { flags := []*pflag.FlagSet{} + + // add server component flags + flags = append(flags, s.StartCmdFlags()) + + // add other components' start cmd flags for _, mod := range s.components { if startmod, ok := mod.(HasStartFlags); ok { flags = append(flags, startmod.StartCmdFlags()) diff --git a/server/v2/server_test.go b/server/v2/server_test.go index 659e5aff34d4..572d91272062 100644 --- a/server/v2/server_test.go +++ b/server/v2/server_test.go @@ -65,6 +65,7 @@ func TestServer(t *testing.T) { server := serverv2.NewServer( logger, + serverv2.DefaultServerConfig(), grpcServer, mockServer, ) diff --git a/server/v2/testdata/app.toml b/server/v2/testdata/app.toml index 5b0112dd8b37..192dc8d67865 100644 --- a/server/v2/testdata/app.toml +++ b/server/v2/testdata/app.toml @@ -10,6 +10,10 @@ max-recv-msg-size = 10485760 # The default value is math.MaxInt32. max-send-msg-size = 2147483647 +[server] +# minimum-gas-prices defines the price which a validator is willing to accept for processing a transaction. A transaction's fees must meet the minimum of any denomination specified in this config (e.g. 0.25token1;0.0001token2). +minimum-gas-prices = '0stake' + [store] # The type of database for application and snapshots databases. app-db-backend = 'goleveldb' diff --git a/simapp/v2/simdv2/cmd/commands.go b/simapp/v2/simdv2/cmd/commands.go index b4d13aa7a78c..d9fa1fa54983 100644 --- a/simapp/v2/simdv2/cmd/commands.go +++ b/simapp/v2/simdv2/cmd/commands.go @@ -75,6 +75,7 @@ func initRootCmd[T transaction.Tx]( rootCmd, newApp, logger, + initServerConfig(), cometbft.New(&genericTxDecoder[T]{txConfig}, cometbft.DefaultServerOptions[T]()), grpc.New[T](), store.New[T](newApp), diff --git a/simapp/v2/simdv2/cmd/config.go b/simapp/v2/simdv2/cmd/config.go index 0794a8a42a79..c7f6b707c89c 100644 --- a/simapp/v2/simdv2/cmd/config.go +++ b/simapp/v2/simdv2/cmd/config.go @@ -3,6 +3,8 @@ package cmd import ( "strings" + serverv2 "cosmossdk.io/server/v2" + clientconfig "github.com/cosmos/cosmos-sdk/client/config" "github.com/cosmos/cosmos-sdk/crypto/keyring" ) @@ -49,3 +51,20 @@ gas-adjustment = {{ .GasConfig.GasAdjustment }} return customClientConfigTemplate, customClientConfig } + +// Allow the chain developer to overwrite the server default app toml config. +func initServerConfig() serverv2.ServerConfig { + serverCfg := serverv2.DefaultServerConfig() + // The server's default minimum gas price is set to "0stake" inside + // app.toml. However, the chain developer can set a default app.toml value for their + // validators here. Please update value based on chain denom. + // + // In summary: + // - if you set serverCfg.MinGasPrices value, validators CAN tweak their + // own app.toml to override, or use this default value. + // + // In simapp, we set the min gas prices to 0. + serverCfg.MinGasPrices = "0stake" + + return serverCfg +} diff --git a/simapp/v2/simdv2/cmd/testnet.go b/simapp/v2/simdv2/cmd/testnet.go index 91c1a30a12a6..5ec782389036 100644 --- a/simapp/v2/simdv2/cmd/testnet.go +++ b/simapp/v2/simdv2/cmd/testnet.go @@ -41,7 +41,6 @@ import ( ) var ( - flagMinGasPrices = "minimum-gas-prices" flagNodeDirPrefix = "node-dir-prefix" flagNumValidators = "validator-count" flagOutputDir = "output-dir" @@ -72,7 +71,7 @@ func addTestnetFlagsToCmd(cmd *cobra.Command) { cmd.Flags().IntP(flagNumValidators, "n", 4, "Number of validators to initialize the testnet with") cmd.Flags().StringP(flagOutputDir, "o", "./.testnets", "Directory to store initialization data for the testnet") cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") - cmd.Flags().String(flagMinGasPrices, fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)") + cmd.Flags().String(serverv2.FlagMinGasPrices, fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)") cmd.Flags().String(flags.FlagKeyType, string(hd.Secp256k1Type), "Key signing algorithm to generate keys for") // support old flags name for backwards compatibility @@ -129,7 +128,7 @@ Example: args.outputDir, _ = cmd.Flags().GetString(flagOutputDir) args.keyringBackend, _ = cmd.Flags().GetString(flags.FlagKeyringBackend) args.chainID, _ = cmd.Flags().GetString(flags.FlagChainID) - args.minGasPrices, _ = cmd.Flags().GetString(flagMinGasPrices) + args.minGasPrices, _ = cmd.Flags().GetString(serverv2.FlagMinGasPrices) args.nodeDirPrefix, _ = cmd.Flags().GetString(flagNodeDirPrefix) args.nodeDaemonHome, _ = cmd.Flags().GetString(flagNodeDaemonHome) args.startingIPAddress, _ = cmd.Flags().GetString(flagStartingIPAddress) @@ -337,6 +336,9 @@ func initTestnetFiles[T transaction.Tx]( return err } + serverCfg := serverv2.DefaultServerConfig() + serverCfg.MinGasPrices = args.minGasPrices + // Write server config cometServer := cometbft.New[T]( &genericTxDecoder[T]{clientCtx.TxConfig}, @@ -345,7 +347,7 @@ func initTestnetFiles[T transaction.Tx]( ) storeServer := store.New[T](newApp) grpcServer := grpc.New[T](grpc.OverwriteDefaultConfig(grpcConfig)) - server := serverv2.NewServer(log.NewNopLogger(), cometServer, grpcServer, storeServer) + server := serverv2.NewServer(log.NewNopLogger(), serverCfg, cometServer, grpcServer, storeServer) err = server.WriteConfig(filepath.Join(nodeDir, "config")) if err != nil { return err diff --git a/tests/systemtests/staking_test.go b/tests/systemtests/staking_test.go index 0b18408410df..f197bff1a0ac 100644 --- a/tests/systemtests/staking_test.go +++ b/tests/systemtests/staking_test.go @@ -10,7 +10,6 @@ import ( ) func TestStakeUnstake(t *testing.T) { - t.Skip("The fee deduction is not yet implemented in v2") // Scenario: // delegate tokens to validator // undelegate some tokens diff --git a/tools/confix/data/v2-app.toml b/tools/confix/data/v2-app.toml index 6a93006eccfb..8f7f7f9940d2 100644 --- a/tools/confix/data/v2-app.toml +++ b/tools/confix/data/v2-app.toml @@ -28,6 +28,10 @@ max-recv-msg-size = 10485760 # The default value is math.MaxInt32. max-send-msg-size = 2147483647 +[server] +# minimum-gas-prices defines the price which a validator is willing to accept for processing a transaction. A transaction's fees must meet the minimum of any denomination specified in this config (e.g. 0.25token1;0.0001token2). +minimum-gas-prices = '0stake' + [store] # The type of database for application and snapshots databases. app-db-backend = 'goleveldb' diff --git a/tools/confix/migrations.go b/tools/confix/migrations.go index 4a500898bc04..437c628d7b25 100644 --- a/tools/confix/migrations.go +++ b/tools/confix/migrations.go @@ -39,11 +39,12 @@ type v2KeyChangesMap map[string][]string // list all the keys which are need to be modified in v2 var v2KeyChanges = v2KeyChangesMap{ - "min-retain-blocks": []string{"comet.min-retain-blocks"}, - "index-events": []string{"comet.index-events"}, - "halt-height": []string{"comet.halt-height"}, - "halt-time": []string{"comet.halt-time"}, - "app-db-backend": []string{"store.app-db-backend"}, + "minimum-gas-prices": []string{"server.minimum-gas-prices"}, + "min-retain-blocks": []string{"comet.min-retain-blocks"}, + "index-events": []string{"comet.index-events"}, + "halt-height": []string{"comet.halt-height"}, + "halt-time": []string{"comet.halt-time"}, + "app-db-backend": []string{"store.app-db-backend"}, "pruning-keep-recent": []string{ "store.options.ss-pruning-option.keep-recent", "store.options.sc-pruning-option.keep-recent", diff --git a/x/auth/ante/fee.go b/x/auth/ante/fee.go index 298fae30a4cc..248d7163471b 100644 --- a/x/auth/ante/fee.go +++ b/x/auth/ante/fee.go @@ -2,8 +2,10 @@ package ante import ( "bytes" + "context" "fmt" + "cosmossdk.io/core/event" "cosmossdk.io/core/transaction" errorsmod "cosmossdk.io/errors" "cosmossdk.io/x/auth/types" @@ -14,7 +16,7 @@ import ( // TxFeeChecker checks if the provided fee is enough and returns the effective fee and tx priority. // The effective fee should be deducted later, and the priority should be returned in the ABCI response. -type TxFeeChecker func(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) +type TxFeeChecker func(ctx context.Context, tx transaction.Tx) (sdk.Coins, int64, error) // DeductFeeDecorator deducts fees from the fee payer. The fee payer is the fee granter (if specified) or first signer of the tx. // If the fee payer does not have the funds to pay for the fees, return an InsufficientFunds error. @@ -25,60 +27,78 @@ type DeductFeeDecorator struct { bankKeeper types.BankKeeper feegrantKeeper FeegrantKeeper txFeeChecker TxFeeChecker + minGasPrices sdk.DecCoins } -func NewDeductFeeDecorator(ak AccountKeeper, bk types.BankKeeper, fk FeegrantKeeper, tfc TxFeeChecker) DeductFeeDecorator { - if tfc == nil { - tfc = checkTxFeeWithValidatorMinGasPrices - } - - return DeductFeeDecorator{ +func NewDeductFeeDecorator(ak AccountKeeper, bk types.BankKeeper, fk FeegrantKeeper, tfc TxFeeChecker) *DeductFeeDecorator { + dfd := &DeductFeeDecorator{ accountKeeper: ak, bankKeeper: bk, feegrantKeeper: fk, txFeeChecker: tfc, + minGasPrices: sdk.NewDecCoins(), + } + + if tfc == nil { + dfd.txFeeChecker = dfd.checkTxFeeWithValidatorMinGasPrices } + + return dfd } -func (dfd DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, _ bool, next sdk.AnteHandler) (sdk.Context, error) { +// SetMinGasPrices sets the minimum-gas-prices value in the state of DeductFeeDecorator +func (dfd *DeductFeeDecorator) SetMinGasPrices(minGasPrices sdk.DecCoins) { + dfd.minGasPrices = minGasPrices +} + +// AnteHandle implements an AnteHandler decorator for the DeductFeeDecorator +func (dfd *DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, _ bool, next sdk.AnteHandler) (sdk.Context, error) { + dfd.minGasPrices = ctx.MinGasPrices() + txPriority, err := dfd.innerValidateTx(ctx, tx) + if err != nil { + return ctx, err + } + + newCtx := ctx.WithPriority(txPriority) + return next(newCtx, tx, false) +} + +func (dfd *DeductFeeDecorator) innerValidateTx(ctx context.Context, tx transaction.Tx) (priority int64, err error) { feeTx, ok := tx.(sdk.FeeTx) if !ok { - return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must implement the FeeTx interface") + return 0, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must implement the FeeTx interface") } - txService := dfd.accountKeeper.GetEnvironment().TransactionService - execMode := txService.ExecMode(ctx) - if execMode != transaction.ExecModeSimulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 { - return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas") - } + execMode := dfd.accountKeeper.GetEnvironment().TransactionService.ExecMode(ctx) + headerInfo := dfd.accountKeeper.GetEnvironment().HeaderService.HeaderInfo(ctx) - var ( - priority int64 - err error - ) + if execMode != transaction.ExecModeSimulate && headerInfo.Height > 0 && feeTx.GetGas() == 0 { + return 0, errorsmod.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas") + } fee := feeTx.GetFee() if execMode != transaction.ExecModeSimulate { fee, priority, err = dfd.txFeeChecker(ctx, tx) if err != nil { - return ctx, err + return 0, err } } - if err := dfd.checkDeductFee(ctx, tx, fee); err != nil { - return ctx, err - } - newCtx := ctx.WithPriority(priority) + if err := dfd.checkDeductFee(ctx, feeTx, fee); err != nil { + return 0, err + } - return next(newCtx, tx, false) + return priority, nil } -func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee sdk.Coins) error { - feeTx, ok := sdkTx.(sdk.FeeTx) - if !ok { - return errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must implement the FeeTx interface") - } +// ValidateTx implements an TxValidator for DeductFeeDecorator +// Note: This method is applicable only for transactions that implement the sdk.FeeTx interface. +func (dfd *DeductFeeDecorator) ValidateTx(ctx context.Context, tx transaction.Tx) error { + _, err := dfd.innerValidateTx(ctx, tx) + return err +} +func (dfd *DeductFeeDecorator) checkDeductFee(ctx context.Context, feeTx sdk.FeeTx, fee sdk.Coins) error { addr := dfd.accountKeeper.GetModuleAddress(types.FeeCollectorName) if len(addr) == 0 { return fmt.Errorf("fee collector module account (%s) has not been set", types.FeeCollectorName) @@ -91,12 +111,10 @@ func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee // if feegranter set, deduct fee from feegranter account. // this works only when feegrant is enabled. if feeGranter != nil { - feeGranterAddr := sdk.AccAddress(feeGranter) - if dfd.feegrantKeeper == nil { return sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled") - } else if !bytes.Equal(feeGranterAddr, feePayer) { - err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranterAddr, feePayer, fee, sdkTx.GetMsgs()) + } else if !bytes.Equal(feeGranter, feePayer) { + err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, feeTx.GetMsgs()) if err != nil { granterAddr, acErr := dfd.accountKeeper.AddressCodec().BytesToString(feeGranter) if acErr != nil { @@ -109,38 +127,34 @@ func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee return errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", granterAddr, payerAddr) } } - - deductFeesFrom = feeGranterAddr + deductFeesFrom = feeGranter } // deduct the fees if !fee.IsZero() { - err := DeductFees(dfd.bankKeeper, ctx, deductFeesFrom, fee) - if err != nil { + if err := DeductFees(dfd.bankKeeper, ctx, deductFeesFrom, fee); err != nil { return err } } - events := sdk.Events{ - sdk.NewEvent( - sdk.EventTypeTx, - sdk.NewAttribute(sdk.AttributeKeyFee, fee.String()), - sdk.NewAttribute(sdk.AttributeKeyFeePayer, sdk.AccAddress(deductFeesFrom).String()), - ), + if err := dfd.accountKeeper.GetEnvironment().EventService.EventManager(ctx).EmitKV( + sdk.EventTypeTx, + event.NewAttribute(sdk.AttributeKeyFee, fee.String()), + event.NewAttribute(sdk.AttributeKeyFeePayer, sdk.AccAddress(deductFeesFrom).String()), + ); err != nil { + return err } - ctx.EventManager().EmitEvents(events) return nil } // DeductFees deducts fees from the given account. -func DeductFees(bankKeeper types.BankKeeper, ctx sdk.Context, acc []byte, fees sdk.Coins) error { +func DeductFees(bankKeeper types.BankKeeper, ctx context.Context, acc []byte, fees sdk.Coins) error { if !fees.IsValid() { return errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) } - err := bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(acc), types.FeeCollectorName, fees) - if err != nil { + if err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc, types.FeeCollectorName, fees); err != nil { return fmt.Errorf("failed to deduct fees: %w", err) } diff --git a/x/auth/ante/fee_test.go b/x/auth/ante/fee_test.go index 0c9fa299a5e4..6736f17aed99 100644 --- a/x/auth/ante/fee_test.go +++ b/x/auth/ante/fee_test.go @@ -41,6 +41,11 @@ func TestDeductFeeDecorator_ZeroGas(t *testing.T) { // Set IsCheckTx to true s.ctx = s.ctx.WithIsCheckTx(true) + // Set current block height in headerInfo + headerInfo := s.ctx.HeaderInfo() + headerInfo.Height = s.ctx.BlockHeight() + s.ctx = s.ctx.WithHeaderInfo(headerInfo) + _, err = antehandler(s.ctx, tx, false) require.Error(t, err) diff --git a/x/auth/ante/validator_tx_fee.go b/x/auth/ante/validator_tx_fee.go index af4f02fed05f..421068175bea 100644 --- a/x/auth/ante/validator_tx_fee.go +++ b/x/auth/ante/validator_tx_fee.go @@ -1,8 +1,10 @@ package ante import ( + "context" "math" + "cosmossdk.io/core/transaction" errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" @@ -12,7 +14,7 @@ import ( // checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per // unit of gas is fixed and set by each validator, can the tx priority is computed from the gas price. -func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) { +func (dfd *DeductFeeDecorator) checkTxFeeWithValidatorMinGasPrices(ctx context.Context, tx transaction.Tx) (sdk.Coins, int64, error) { feeTx, ok := tx.(sdk.FeeTx) if !ok { return nil, 0, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") @@ -24,8 +26,8 @@ func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, // Ensure that the provided fees meet a minimum threshold for the validator, // if this is a CheckTx. This is only for local mempool purposes, and thus // is only ran on check tx. - if ctx.ExecMode() == sdk.ExecModeCheck { // NOTE: using environment here breaks the API of fee logic, an alternative must be found for server/v2. ref: https://github.com/cosmos/cosmos-sdk/issues/19640 - minGasPrices := ctx.MinGasPrices() + if dfd.accountKeeper.GetEnvironment().TransactionService.ExecMode(ctx) == transaction.ExecModeCheck { + minGasPrices := dfd.minGasPrices if !minGasPrices.IsZero() { requiredFees := make(sdk.Coins, len(minGasPrices)) diff --git a/x/auth/tx/config/depinject.go b/x/auth/tx/config/depinject.go index bb573f044f6a..e4e43ac3d80f 100644 --- a/x/auth/tx/config/depinject.go +++ b/x/auth/tx/config/depinject.go @@ -6,6 +6,7 @@ import ( "fmt" gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/codes" grpcstatus "google.golang.org/grpc/status" @@ -34,6 +35,9 @@ import ( signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" ) +// flagMinGasPricesV2 is the flag name for the minimum gas prices in the main server v2 component. +const flagMinGasPricesV2 = "server.minimum-gas-prices" + func init() { appconfig.RegisterModule(&txconfigv1.Config{}, appconfig.Provide(ProvideModule), @@ -50,7 +54,7 @@ type ModuleInputs struct { Codec codec.Codec ProtoFileResolver txsigning.ProtoFileResolver Environment appmodule.Environment - // BankKeeper is the expected bank keeper to be passed to AnteHandlers + // BankKeeper is the expected bank keeper to be passed to AnteHandlers / Tx Validators BankKeeper authtypes.BankKeeper `optional:"true"` MetadataBankKeeper BankKeeper `optional:"true"` AccountKeeper ante.AccountKeeper `optional:"true"` @@ -58,8 +62,10 @@ type ModuleInputs struct { AccountAbstractionKeeper ante.AccountAbstractionKeeper `optional:"true"` CustomSignModeHandlers func() []txsigning.SignModeHandler `optional:"true"` CustomGetSigners []txsigning.CustomGetSigner `optional:"true"` - UnorderedTxManager *unorderedtx.Manager `optional:"true"` ExtraTxValidators []appmodule.TxValidator[transaction.Tx] `optional:"true"` + UnorderedTxManager *unorderedtx.Manager `optional:"true"` + TxFeeChecker ante.TxFeeChecker `optional:"true"` + Viper *viper.Viper `optional:"true"` // server v2 } type ModuleOutputs struct { @@ -107,7 +113,40 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { panic(err) } - baseAppOption := func(app *baseapp.BaseApp) { + svd := ante.NewSigVerificationDecorator( + in.AccountKeeper, + txConfig.SignModeHandler(), + ante.DefaultSigVerificationGasConsumer, + in.AccountAbstractionKeeper, + ) + + var ( + minGasPrices sdk.DecCoins + feeTxValidator *ante.DeductFeeDecorator + ) + if in.AccountKeeper != nil && in.BankKeeper != nil && in.Viper != nil { + minGasPricesStr := in.Viper.GetString(flagMinGasPricesV2) + minGasPrices, err = sdk.ParseDecCoins(minGasPricesStr) + if err != nil { + panic(fmt.Sprintf("invalid minimum gas prices: %v", err)) + } + + feeTxValidator = ante.NewDeductFeeDecorator(in.AccountKeeper, in.BankKeeper, in.FeeGrantKeeper, in.TxFeeChecker) + feeTxValidator.SetMinGasPrices(minGasPrices) // set min gas price in deduct fee decorator + } + + return ModuleOutputs{ + Module: NewAppModule(svd, feeTxValidator, in.ExtraTxValidators...), + BaseAppOption: newBaseAppOption(txConfig, in), + TxConfig: txConfig, + TxConfigOptions: txConfigOptions, + } +} + +// newBaseAppOption returns baseapp option that sets the ante handler and post handler +// and set the tx encoder and decoder on baseapp. +func newBaseAppOption(txConfig client.TxConfig, in ModuleInputs) func(app *baseapp.BaseApp) { + return func(app *baseapp.BaseApp) { // AnteHandlers if !in.Config.SkipAnteHandler { anteHandler, err := newAnteHandler(txConfig, in) @@ -145,20 +184,6 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { app.SetTxDecoder(txConfig.TxDecoder()) app.SetTxEncoder(txConfig.TxEncoder()) } - - svd := ante.NewSigVerificationDecorator( - in.AccountKeeper, - txConfig.SignModeHandler(), - ante.DefaultSigVerificationGasConsumer, - in.AccountAbstractionKeeper, - ) - - return ModuleOutputs{ - Module: NewAppModule(svd, in.ExtraTxValidators...), - TxConfig: txConfig, - TxConfigOptions: txConfigOptions, - BaseAppOption: baseAppOption, - } } func newAnteHandler(txConfig client.TxConfig, in ModuleInputs) (sdk.AnteHandler, error) { diff --git a/x/auth/tx/config/module.go b/x/auth/tx/config/module.go index 6e94e0e8cfac..f4a1207fa7b1 100644 --- a/x/auth/tx/config/module.go +++ b/x/auth/tx/config/module.go @@ -19,6 +19,7 @@ var ( // This module is only useful for chains using server/v2. Ante/Post handlers are setup via baseapp options in depinject. type AppModule struct { sigVerification ante.SigVerificationDecorator + feeTxValidator *ante.DeductFeeDecorator // txValidators contains tx validator that can be injected into the module via depinject. // tx validators should be module based, but it can happen that you do not want to create a new module // and simply depinject-it. @@ -28,10 +29,12 @@ type AppModule struct { // NewAppModule creates a new AppModule object. func NewAppModule( sigVerification ante.SigVerificationDecorator, + feeTxValidator *ante.DeductFeeDecorator, txValidators ...appmodulev2.TxValidator[transaction.Tx], ) AppModule { return AppModule{ sigVerification: sigVerification, + feeTxValidator: feeTxValidator, txValidators: txValidators, } } @@ -50,5 +53,9 @@ func (a AppModule) TxValidator(ctx context.Context, tx transaction.Tx) error { } } + if err := a.feeTxValidator.ValidateTx(ctx, tx); err != nil { + return err + } + return a.sigVerification.ValidateTx(ctx, tx) }