Limit simulation gas (#674)

* Limit simulation gas

* Put parameters on separate lines
This commit is contained in:
Alexander Peters 2021-11-17 17:20:26 +01:00 committed by GitHub
parent 1f43ca13c7
commit efe87743e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 14 deletions

View File

@ -8,6 +8,8 @@ import (
channelkeeper "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/keeper"
ibcante "github.com/cosmos/cosmos-sdk/x/ibc/core/ante"
wasmTypes "github.com/CosmWasm/wasmd/x/wasm/types"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
)
@ -15,15 +17,18 @@ import (
// numbers, checks signatures & account numbers, and deducts fees from the first
// signer.
func NewAnteHandler(
ak ante.AccountKeeper, bankKeeper types.BankKeeper,
ak ante.AccountKeeper,
bankKeeper types.BankKeeper,
sigGasConsumer ante.SignatureVerificationGasConsumer,
signModeHandler signing.SignModeHandler,
txCounterStoreKey sdk.StoreKey,
channelKeeper channelkeeper.Keeper,
wasmConfig wasmTypes.WasmConfig,
) sdk.AnteHandler {
// copied sdk https://github.com/cosmos/cosmos-sdk/blob/v0.42.9/x/auth/ante/ante.go
return sdk.ChainAnteDecorators(
ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
wasmkeeper.NewLimitSimulationGasDecorator(wasmConfig.SimulationGasLimit), // after setup context to enforce limits early
wasmkeeper.NewCountTXDecorator(txCounterStoreKey),
ante.NewRejectExtensionOptionsDecorator(),
ante.NewMempoolFeeDecorator(),

View File

@ -1,6 +1,7 @@
package app
import (
"fmt"
"io"
"net/http"
"os"
@ -235,9 +236,19 @@ type WasmApp struct {
}
// NewWasmApp returns a reference to an initialized WasmApp.
func NewWasmApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
skipUpgradeHeights map[int64]bool, homePath string, invCheckPeriod uint, enabledProposals []wasm.ProposalType,
appOpts servertypes.AppOptions, wasmOpts []wasm.Option, baseAppOptions ...func(*baseapp.BaseApp)) *WasmApp {
func NewWasmApp(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
skipUpgradeHeights map[int64]bool,
homePath string,
invCheckPeriod uint,
enabledProposals []wasm.ProposalType,
appOpts servertypes.AppOptions,
wasmOpts []wasm.Option,
baseAppOptions ...func(*baseapp.BaseApp),
) *WasmApp {
encodingConfig := MakeEncodingConfig()
appCodec, legacyAmino := encodingConfig.Marshaler, encodingConfig.Amino
@ -346,7 +357,7 @@ func NewWasmApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b
wasmDir := filepath.Join(homePath, "wasm")
wasmConfig, err := wasm.ReadWasmConfig(appOpts)
if err != nil {
panic("error while reading wasm config: " + err.Error())
panic(fmt.Sprintf("error while reading wasm config: %s", err))
}
// The last arguments can contain custom message handlers, and custom query handlers,
@ -485,6 +496,7 @@ func NewWasmApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b
NewAnteHandler(
app.accountKeeper, app.bankKeeper, ante.DefaultSigVerificationGasConsumer,
encodingConfig.TxConfig.SignModeHandler(), keys[wasm.StoreKey], app.ibcKeeper.ChannelKeeper,
wasmConfig,
),
)
app.SetEndBlocker(app.EndBlocker)

View File

@ -53,3 +53,44 @@ func encodeHeightCounter(height int64, counter uint32) []byte {
func decodeHeightCounter(bz []byte) (int64, uint32) {
return int64(sdk.BigEndianToUint64(bz[0:8])), binary.BigEndian.Uint32(bz[8:])
}
// LimitSimulationGasDecorator ante decorator to limit gas in simulation calls
type LimitSimulationGasDecorator struct {
gasLimit *sdk.Gas
}
// NewLimitSimulationGasDecorator constructor accepts nil value to fallback to block gas limit.
func NewLimitSimulationGasDecorator(gasLimit *sdk.Gas) *LimitSimulationGasDecorator {
if gasLimit != nil && *gasLimit == 0 {
panic("gas limit must not be zero")
}
return &LimitSimulationGasDecorator{gasLimit: gasLimit}
}
// AnteHandle that limits the maximum gas available in simulations only.
// A custom max value can be configured and will be applied when set. The value should not
// exceed the max block gas limit.
// Different values on nodes are not consensus breaking as they affect only
// simulations but may have effect on client user experience.
//
// When no custom value is set then the max block gas is used as default limit.
func (d LimitSimulationGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
if !simulate {
// Wasm code is not executed in checkTX so that we don't need to limit it further.
// Tendermint rejects the TX afterwards when the tx.gas > max block gas.
// On deliverTX we rely on the tendermint/sdk mechanics that ensure
// tx has gas set and gas < max block gas
return next(ctx, tx, simulate)
}
// apply custom node gas limit
if d.gasLimit != nil {
return next(ctx.WithGasMeter(sdk.NewGasMeter(*d.gasLimit)), tx, simulate)
}
// default to max block gas when set, to be on the safe side
if maxGas := ctx.ConsensusParams().GetBlock().MaxGas; maxGas > 0 {
return next(ctx.WithGasMeter(sdk.NewGasMeter(sdk.Gas(maxGas))), tx, simulate)
}
return next(ctx, tx, simulate)
}

View File

@ -1,9 +1,13 @@
package keeper
package keeper_test
import (
"testing"
"time"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/CosmWasm/wasmd/x/wasm/keeper"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
@ -97,7 +101,7 @@ func TestCountTxDecorator(t *testing.T) {
var anyTx sdk.Tx
// when
ante := NewCountTXDecorator(keyWasm)
ante := keeper.NewCountTXDecorator(keyWasm)
_, gotErr := ante.AnteHandle(ctx, anyTx, spec.simulate, spec.nextAssertAnte)
if spec.expErr {
require.Error(t, gotErr)
@ -107,3 +111,77 @@ func TestCountTxDecorator(t *testing.T) {
})
}
}
func TestLimitSimulationGasDecorator(t *testing.T) {
var (
hundred sdk.Gas = 100
zero sdk.Gas = 0
)
specs := map[string]struct {
customLimit *sdk.Gas
consumeGas sdk.Gas
maxBlockGas int64
simulation bool
expErr interface{}
}{
"custom limit set": {
customLimit: &hundred,
consumeGas: hundred + 1,
maxBlockGas: -1,
simulation: true,
expErr: sdk.ErrorOutOfGas{Descriptor: "testing"},
},
"block limit set": {
maxBlockGas: 100,
consumeGas: hundred + 1,
simulation: true,
expErr: sdk.ErrorOutOfGas{Descriptor: "testing"},
},
"no limits set": {
maxBlockGas: -1,
consumeGas: hundred + 1,
simulation: true,
},
"both limits set, custom applies": {
customLimit: &hundred,
consumeGas: hundred - 1,
maxBlockGas: 10,
simulation: true,
},
"not a simulation": {
customLimit: &hundred,
consumeGas: hundred + 1,
simulation: false,
},
"zero custom limit": {
customLimit: &zero,
simulation: true,
expErr: "gas limit must not be zero",
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
nextAnte := consumeGasAnteHandler(spec.consumeGas)
ctx := sdk.Context{}.
WithGasMeter(sdk.NewInfiniteGasMeter()).
WithConsensusParams(&abci.ConsensusParams{
Block: &abci.BlockParams{MaxGas: spec.maxBlockGas}})
// when
if spec.expErr != nil {
require.PanicsWithValue(t, spec.expErr, func() {
ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit)
ante.AnteHandle(ctx, nil, spec.simulation, nextAnte)
})
return
}
ante := keeper.NewLimitSimulationGasDecorator(spec.customLimit)
ante.AnteHandle(ctx, nil, spec.simulation, nextAnte)
})
}
}
func consumeGasAnteHandler(gasToConsume sdk.Gas) sdk.AnteHandler {
return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
ctx.GasMeter().ConsumeGas(gasToConsume, "testing")
return ctx, nil
}
}

View File

@ -33,8 +33,9 @@ var (
// Module init related flags
const (
flagWasmMemoryCacheSize = "wasm.memory_cache_size"
flagWasmQueryGasLimit = "wasm.query_gas_limit"
flagWasmMemoryCacheSize = "wasm.memory_cache_size"
flagWasmQueryGasLimit = "wasm.query_gas_limit"
flagWasmSimulationGasLimit = "wasm.simulation_gas_limit"
)
// AppModuleBasic defines the basic application module used by the wasm module.
@ -199,6 +200,7 @@ func AddModuleInitFlags(startCmd *cobra.Command) {
defaults := DefaultWasmConfig()
startCmd.Flags().Uint32(flagWasmMemoryCacheSize, defaults.MemoryCacheSize, "Sets the size in MiB (NOT bytes) of an in-memory cache for Wasm modules. Set to 0 to disable.")
startCmd.Flags().Uint64(flagWasmQueryGasLimit, defaults.SmartQueryGasLimit, "Set the max gas that can be spent on executing a query with a Wasm contract")
startCmd.Flags().String(flagWasmSimulationGasLimit, "", "Set the max gas that can be spent when executing a simulation TX")
}
// ReadWasmConfig reads the wasm specifig configuration
@ -215,6 +217,15 @@ func ReadWasmConfig(opts servertypes.AppOptions) (types.WasmConfig, error) {
return cfg, err
}
}
if v := opts.Get(flagWasmSimulationGasLimit); v != nil {
if raw, ok := v.(string); ok && raw != "" {
limit, err := cast.ToUint64E(v) // non empty string set
if err != nil {
return cfg, err
}
cfg.SimulationGasLimit = &limit
}
}
// attach contract debugging to global "trace" flag
if v := opts.Get(server.FlagTrace); v != nil {
if cfg.ContractDebugMode, err = cast.ToBoolE(v); err != nil {

View File

@ -12,9 +12,9 @@ import (
)
const (
defaultMemoryCacheSize uint32 = 100 // in MiB
defaultQueryGasLimit uint64 = 3000000
defaultContractDebugMode = false
defaultMemoryCacheSize uint32 = 100 // in MiB
defaultSmartQueryGasLimit uint64 = 3_000_000
defaultContractDebugMode = false
)
func (m Model) ValidateBasic() error {
@ -296,6 +296,10 @@ func NewWasmCoins(cosmosCoins sdk.Coins) (wasmCoins []wasmvmtypes.Coin) {
// WasmConfig is the extra config required for wasm
type WasmConfig struct {
// SimulationGasLimit is the max gas to be used in a tx simulation call.
// When not set the consensus max block gas is used instead
SimulationGasLimit *uint64
// SimulationGasLimit is the max gas to be used in a smart query contract call
SmartQueryGasLimit uint64
// MemoryCacheSize in MiB not bytes
MemoryCacheSize uint32
@ -306,7 +310,7 @@ type WasmConfig struct {
// DefaultWasmConfig returns the default settings for WasmConfig
func DefaultWasmConfig() WasmConfig {
return WasmConfig{
SmartQueryGasLimit: defaultQueryGasLimit,
SmartQueryGasLimit: defaultSmartQueryGasLimit,
MemoryCacheSize: defaultMemoryCacheSize,
ContractDebugMode: defaultContractDebugMode,
}