diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f931683b19..37376d22922e 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 02b2d666c3a2..cc4fa14e4c78 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 71a297502b88..959ecf80b38e 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 7a8c5004692c..0fef6903b776 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -86,6 +86,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 { t.Helper() snapshotTimeout := 1 * time.Minute @@ -620,6 +640,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 fbb15a6c5b17..f46bf720883b 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 44b67a9e4cfa..107b3aabf1f1 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 877913fcf20b..903303073a66 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 b968ca90fd1d..f6535c4d3474 100644 --- a/server/start.go +++ b/server/start.go @@ -49,6 +49,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" @@ -178,6 +179,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 fbaf6d0be1d7..4570ecd33136 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 f4596897a805..5e01e120d8b0 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