diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc71108ba..bfb576fd8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * (x/bank) [#16795](https://github.com/cosmos/cosmos-sdk/pull/16852) Add `DenomMetadataByQueryString` query in bank module to support metadata query by query string. +* (baseapp) [#16239](https://github.com/cosmos/cosmos-sdk/pull/16239) Add Gas Limits to allow node operators to resource bound queries. ### Improvements diff --git a/baseapp/abci.go b/baseapp/abci.go index 02b2d666c3..cc4fa14e4c 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -1116,7 +1116,8 @@ func (app *BaseApp) CreateQueryContext(height int64, prove bool) (sdk.Context, e // branch the commit multi-store for safety ctx := sdk.NewContext(cacheMS, app.checkState.ctx.BlockHeader(), true, app.logger). WithMinGasPrices(app.minGasPrices). - WithBlockHeight(height) + WithBlockHeight(height). + WithGasMeter(storetypes.NewGasMeter(app.queryGasLimit)) if height != lastBlockHeight { rms, ok := app.cms.(*rootmulti.Store) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 71a297502b..959ecf80b3 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -3,6 +3,7 @@ package baseapp import ( "context" "fmt" + "math" "sort" "strconv" @@ -123,6 +124,9 @@ type BaseApp struct { // application parameter store. paramStore ParamStore + // queryGasLimit defines the maximum gas for queries; unbounded if 0. + queryGasLimit uint64 + // The minimum gas prices a validator is willing to accept for processing a // transaction. This is mainly used for DoS and spam prevention. minGasPrices sdk.DecCoins @@ -192,6 +196,7 @@ func NewBaseApp( msgServiceRouter: NewMsgServiceRouter(), txDecoder: txDecoder, fauxMerkleMode: false, + queryGasLimit: math.MaxUint64, } for _, option := range options { diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index e4579c5b23..09c76c5e9b 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -85,6 +85,26 @@ func NewBaseAppSuite(t *testing.T, opts ...func(*baseapp.BaseApp)) *BaseAppSuite } } +func getQueryBaseapp(t *testing.T) *baseapp.BaseApp { + t.Helper() + + db := dbm.NewMemDB() + name := t.Name() + app := baseapp.NewBaseApp(name, log.NewTestLogger(t), db, nil) + + _, err := app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 1}) + require.NoError(t, err) + _, err = app.Commit() + require.NoError(t, err) + + _, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{Height: 2}) + require.NoError(t, err) + _, err = app.Commit() + require.NoError(t, err) + + return app +} + func NewBaseAppSuiteWithSnapshots(t *testing.T, cfg SnapshotsConfig, opts ...func(*baseapp.BaseApp)) *BaseAppSuite { snapshotTimeout := 1 * time.Minute snapshotStore, err := snapshots.NewStore(dbm.NewMemDB(), testutil.GetTempDir(t)) @@ -614,6 +634,60 @@ func TestSetMinGasPrices(t *testing.T) { require.Equal(t, minGasPrices, ctx.MinGasPrices()) } +type ctxType string + +const ( + QueryCtx ctxType = "query" + CheckTxCtx ctxType = "checkTx" +) + +var ctxTypes = []ctxType{QueryCtx, CheckTxCtx} + +func (c ctxType) GetCtx(t *testing.T, bapp *baseapp.BaseApp) sdk.Context { + t.Helper() + if c == QueryCtx { + ctx, err := bapp.CreateQueryContext(1, false) + require.NoError(t, err) + return ctx + } else if c == CheckTxCtx { + return getCheckStateCtx(bapp) + } + // TODO: Not supported yet + return getFinalizeBlockStateCtx(bapp) +} + +func TestQueryGasLimit(t *testing.T) { + testCases := []struct { + queryGasLimit uint64 + gasActuallyUsed uint64 + shouldQueryErr bool + }{ + {queryGasLimit: 100, gasActuallyUsed: 50, shouldQueryErr: false}, // Valid case + {queryGasLimit: 100, gasActuallyUsed: 150, shouldQueryErr: true}, // gasActuallyUsed > queryGasLimit + {queryGasLimit: 0, gasActuallyUsed: 50, shouldQueryErr: false}, // fuzzing with queryGasLimit = 0 + {queryGasLimit: 0, gasActuallyUsed: 0, shouldQueryErr: false}, // both queryGasLimit and gasActuallyUsed are 0 + {queryGasLimit: 200, gasActuallyUsed: 200, shouldQueryErr: false}, // gasActuallyUsed == queryGasLimit + {queryGasLimit: 100, gasActuallyUsed: 1000, shouldQueryErr: true}, // gasActuallyUsed > queryGasLimit + } + + for _, tc := range testCases { + for _, ctxType := range ctxTypes { + t.Run(fmt.Sprintf("%s: %d - %d", ctxType, tc.queryGasLimit, tc.gasActuallyUsed), func(t *testing.T) { + app := getQueryBaseapp(t) + baseapp.SetQueryGasLimit(tc.queryGasLimit)(app) + ctx := ctxType.GetCtx(t, app) + + // query gas limit should have no effect when CtxType != QueryCtx + if tc.shouldQueryErr && ctxType == QueryCtx { + require.Panics(t, func() { ctx.GasMeter().ConsumeGas(tc.gasActuallyUsed, "test") }) + } else { + require.NotPanics(t, func() { ctx.GasMeter().ConsumeGas(tc.gasActuallyUsed, "test") }) + } + }) + } + } +} + func TestGetMaximumBlockGas(t *testing.T) { suite := NewBaseAppSuite(t) _, err := suite.baseApp.InitChain(&abci.RequestInitChain{}) diff --git a/baseapp/options.go b/baseapp/options.go index fbb15a6c5b..f46bf72088 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -3,6 +3,7 @@ package baseapp import ( "fmt" "io" + "math" dbm "github.com/cosmos/cosmos-db" @@ -36,6 +37,15 @@ func SetMinGasPrices(gasPricesStr string) func(*BaseApp) { return func(bapp *BaseApp) { bapp.setMinGasPrices(gasPrices) } } +// SetQueryGasLimit returns an option that sets a gas limit for queries. +func SetQueryGasLimit(queryGasLimit uint64) func(*BaseApp) { + if queryGasLimit == 0 { + queryGasLimit = math.MaxUint64 + } + + return func(bapp *BaseApp) { bapp.queryGasLimit = queryGasLimit } +} + // SetHaltHeight returns a BaseApp option function that sets the halt block height. func SetHaltHeight(blockHeight uint64) func(*BaseApp) { return func(bapp *BaseApp) { bapp.setHaltHeight(blockHeight) } diff --git a/server/config/config.go b/server/config/config.go index 44b67a9e4c..107b3aabf1 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -39,6 +39,10 @@ type BaseConfig struct { // specified in this config (e.g. 0.25token1;0.0001token2). MinGasPrices string `mapstructure:"minimum-gas-prices"` + // The maximum amount of gas a grpc/Rest query may consume. + // If set to 0, it is unbounded. + QueryGasLimit uint64 `mapstructure:"query-gas-limit"` + Pruning string `mapstructure:"pruning"` PruningKeepRecent string `mapstructure:"pruning-keep-recent"` PruningInterval string `mapstructure:"pruning-interval"` @@ -225,6 +229,7 @@ func DefaultConfig() *Config { return &Config{ BaseConfig: BaseConfig{ MinGasPrices: defaultMinGasPrices, + QueryGasLimit: 0, InterBlockCache: true, Pruning: pruningtypes.PruningOptionDefault, PruningKeepRecent: "0", diff --git a/server/config/toml.go b/server/config/toml.go index 877913fcf2..903303073a 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -21,6 +21,10 @@ const DefaultConfigTemplate = `# This is a TOML config file. # specified in this config (e.g. 0.25token1;0.0001token2). minimum-gas-prices = "{{ .BaseConfig.MinGasPrices }}" +# The maximum gas a query coming over rest/grpc may consume. +# If this is set to zero, the query can consume an unbounded amount of gas. +query-gas-limit = "{{ .BaseConfig.QueryGasLimit }}" + # default: the last 362880 states are kept, pruning at 10 block intervals # nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) # everything: 2 latest states will be kept; pruning at 10 block intervals. diff --git a/server/start.go b/server/start.go index ff82018a0c..49ce1ab0ec 100644 --- a/server/start.go +++ b/server/start.go @@ -50,6 +50,7 @@ const ( flagTraceStore = "trace-store" flagCPUProfile = "cpu-profile" FlagMinGasPrices = "minimum-gas-prices" + FlagQueryGasLimit = "query-gas-limit" FlagHaltHeight = "halt-height" FlagHaltTime = "halt-time" FlagInterBlockCache = "inter-block-cache" @@ -180,6 +181,7 @@ is performed. Note, when enabled, gRPC will also be automatically enabled. cmd.Flags().String(flagTransport, "socket", "Transport protocol: socket, grpc") cmd.Flags().String(flagTraceStore, "", "Enable KVStore tracing to an output file") cmd.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)") + cmd.Flags().Uint64(FlagQueryGasLimit, 0, "Maximum gas a Rest/Grpc query can consume. Blank and 0 imply unbounded.") cmd.Flags().IntSlice(FlagUnsafeSkipUpgrades, []int{}, "Skip a set of upgrade heights to continue the old binary") cmd.Flags().Uint64(FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node") cmd.Flags().Uint64(FlagHaltTime, 0, "Minimum block time (in Unix seconds) at which to gracefully halt the chain and shutdown the node") diff --git a/server/util.go b/server/util.go index 40842f4b2c..7b7b14e83e 100644 --- a/server/util.go +++ b/server/util.go @@ -516,6 +516,7 @@ func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) { baseapp.SetIAVLDisableFastNode(cast.ToBool(appOpts.Get(FlagDisableIAVLFastNode))), defaultMempool, baseapp.SetChainID(chainID), + baseapp.SetQueryGasLimit(cast.ToUint64(appOpts.Get(FlagQueryGasLimit))), } } diff --git a/tools/confix/data/v0.50-app.toml b/tools/confix/data/v0.50-app.toml index f4596897a8..5e01e120d8 100644 --- a/tools/confix/data/v0.50-app.toml +++ b/tools/confix/data/v0.50-app.toml @@ -10,6 +10,10 @@ # specified in this config (e.g. 0.25token1;0.0001token2). minimum-gas-prices = "0stake" +# The maximum gas a query coming over rest/grpc may consume. +# If this is set to zero, the query can consume an unbounded amount of gas. +query-gas-limit = "0" + # default: the last 362880 states are kept, pruning at 10 block intervals # nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) # everything: 2 latest states will be kept; pruning at 10 block intervals. @@ -226,4 +230,4 @@ max-txs = "5000" query_gas_limit = 300000 # This is the number of wasm vm instances we keep cached in memory for speed-up # Warning: this is currently unstable and may lead to crashes, best to keep for 0 unless testing locally -lru_size = 0 \ No newline at end of file +lru_size = 0