diff --git a/Gopkg.lock b/Gopkg.lock index 40192b2afd..b166713ee0 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -165,13 +165,12 @@ version = "v1.2.0" [[projects]] - digest = "1:c0d19ab64b32ce9fe5cf4ddceba78d5bc9807f0016db6b1183599da3dcc24d10" + digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8" name = "github.com/hashicorp/hcl" packages = [ ".", "hcl/ast", "hcl/parser", - "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -644,6 +643,7 @@ "github.com/bgentry/speakeasy", "github.com/btcsuite/btcd/btcec", "github.com/cosmos/go-bip39", + "github.com/gogo/protobuf/proto", "github.com/golang/protobuf/proto", "github.com/gorilla/mux", "github.com/mattn/go-isatty", diff --git a/PENDING.md b/PENDING.md index 47c6ab60e5..6f99f5705c 100644 --- a/PENDING.md +++ b/PENDING.md @@ -43,7 +43,8 @@ FEATURES * [app] \#2791 Support export at a specific height, with `gaiad export --height=HEIGHT`. * SDK - * [simulator] \#2682 MsgEditValidator now looks at the validator's max rate, thus it now succeeds a significant portion of the time + * [simulator] \#2682 MsgEditValidator now looks at the validator's max rate, thus it now succeeds a significant portion of the time + * [core] \#2775 Add deliverTx maximum block gas limit * Tendermint diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 286d58bbaa..a83713d424 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -6,6 +6,7 @@ import ( "runtime/debug" "strings" + "github.com/gogo/protobuf/proto" "github.com/pkg/errors" abci "github.com/tendermint/tendermint/abci/types" @@ -19,11 +20,8 @@ import ( "github.com/cosmos/cosmos-sdk/version" ) -// Key to store the header in the DB itself. -// Use the db directly instead of a store to avoid -// conflicts with handlers writing to the store -// and to avoid affecting the Merkle root. -var dbHeaderKey = []byte("header") +// Key to store the consensus params in the main store. +var mainConsensusParamsKey = []byte("consensus_params") // Enum mode for app.runTx type runTxMode uint8 @@ -48,9 +46,11 @@ type BaseApp struct { queryRouter QueryRouter // router for redirecting query calls txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx - anteHandler sdk.AnteHandler // ante handler for fee and auth + // set upon LoadVersion or LoadLatestVersion. + mainKey *sdk.KVStoreKey // Main KVStore in cms // may be nil + anteHandler sdk.AnteHandler // ante handler for fee and auth initChainer sdk.InitChainer // initialize state with validators and state blob beginBlocker sdk.BeginBlocker // logic to run before any txs endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes @@ -66,7 +66,11 @@ type BaseApp struct { deliverState *state // for DeliverTx voteInfos []abci.VoteInfo // absent validators from begin block - // minimum fees for spam prevention + // consensus params + // TODO move this in the future to baseapp param store on main store. + consensusParams *abci.ConsensusParams + + // spam prevention minimumFees sdk.Coins // flag for sealing @@ -77,10 +81,6 @@ var _ abci.Application = (*BaseApp)(nil) // NewBaseApp returns a reference to an initialized BaseApp. // -// TODO: Determine how to use a flexible and robust configuration paradigm that -// allows for sensible defaults while being highly configurable -// (e.g. functional options). -// // NOTE: The db is used to store the version number for now. // Accepts a user-defined txDecoder // Accepts variable number of option functions, which act on the BaseApp to set configuration choices @@ -94,7 +94,6 @@ func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecod queryRouter: NewQueryRouter(), txDecoder: txDecoder, } - for _, option := range options { option(app) } @@ -137,21 +136,23 @@ func (app *BaseApp) MountStore(key sdk.StoreKey, typ sdk.StoreType) { } // load latest application version -func (app *BaseApp) LoadLatestVersion(mainKey sdk.StoreKey) error { +// panics if called more than once on a running baseapp +func (app *BaseApp) LoadLatestVersion(mainKey *sdk.KVStoreKey) error { err := app.cms.LoadLatestVersion() if err != nil { return err } - return app.initFromStore(mainKey) + return app.initFromMainStore(mainKey) } // load application version -func (app *BaseApp) LoadVersion(version int64, mainKey sdk.StoreKey) error { +// panics if called more than once on a running baseapp +func (app *BaseApp) LoadVersion(version int64, mainKey *sdk.KVStoreKey) error { err := app.cms.LoadVersion(version) if err != nil { return err } - return app.initFromStore(mainKey) + return app.initFromMainStore(mainKey) } // the last CommitID of the multistore @@ -165,13 +166,34 @@ func (app *BaseApp) LastBlockHeight() int64 { } // initializes the remaining logic from app.cms -func (app *BaseApp) initFromStore(mainKey sdk.StoreKey) error { +func (app *BaseApp) initFromMainStore(mainKey *sdk.KVStoreKey) error { + // main store should exist. - // TODO: we don't actually need the main store here - main := app.cms.GetKVStore(mainKey) - if main == nil { + mainStore := app.cms.GetKVStore(mainKey) + if mainStore == nil { return errors.New("baseapp expects MultiStore with 'main' KVStore") } + + // memoize mainKey + if app.mainKey != nil { + panic("app.mainKey expected to be nil; duplicate init?") + } + app.mainKey = mainKey + + // load consensus params from the main store + consensusParamsBz := mainStore.Get(mainConsensusParamsKey) + if consensusParamsBz != nil { + var consensusParams = &abci.ConsensusParams{} + err := proto.Unmarshal(consensusParamsBz, consensusParams) + if err != nil { + panic(err) + } + app.setConsensusParams(consensusParams) + } else { + // It will get saved later during InitChain. + // TODO assert that InitChain hasn't yet been called. + } + // Needed for `gaiad export`, which inits from store but never calls initchain app.setCheckState(abci.Header{}) @@ -220,6 +242,29 @@ func (app *BaseApp) setDeliverState(header abci.Header) { } } +// setConsensusParams memoizes the consensus params. +func (app *BaseApp) setConsensusParams(consensusParams *abci.ConsensusParams) { + app.consensusParams = consensusParams +} + +// setConsensusParams stores the consensus params to the main store. +func (app *BaseApp) storeConsensusParams(consensusParams *abci.ConsensusParams) { + consensusParamsBz, err := proto.Marshal(consensusParams) + if err != nil { + panic(err) + } + mainStore := app.cms.GetKVStore(app.mainKey) + mainStore.Set(mainConsensusParamsKey, consensusParamsBz) +} + +// getMaximumBlockGas gets the maximum gas from the consensus params. +func (app *BaseApp) getMaximumBlockGas() (maxGas uint64) { + if app.consensusParams == nil || app.consensusParams.BlockSize == nil { + return 0 + } + return uint64(app.consensusParams.BlockSize.MaxGas) +} + //______________________________________________________________________________ // ABCI @@ -242,8 +287,15 @@ func (app *BaseApp) SetOption(req abci.RequestSetOption) (res abci.ResponseSetOp } // Implements ABCI -// InitChain runs the initialization logic directly on the CommitMultiStore and commits it. +// InitChain runs the initialization logic directly on the CommitMultiStore. func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) { + + // Stash the consensus params in the cms main store and memoize. + if req.ConsensusParams != nil { + app.setConsensusParams(req.ConsensusParams) + app.storeConsensusParams(req.ConsensusParams) + } + // Initialize the deliver state and check state with ChainID and run initChain app.setDeliverState(abci.Header{ChainID: req.ChainId}) app.setCheckState(abci.Header{ChainID: req.ChainId}) @@ -251,6 +303,11 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC if app.initChainer == nil { return } + + // add block gas meter for any genesis transactions (allow infinite gas) + app.deliverState.ctx = app.deliverState.ctx. + WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + res = app.initChainer(app.deliverState.ctx, req) // NOTE: we don't commit, but BeginBlock for block 1 @@ -424,9 +481,20 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg } else { // In the first block, app.deliverState.ctx will already be initialized // by InitChain. Context is now updated with Header information. - app.deliverState.ctx = app.deliverState.ctx.WithBlockHeader(req.Header).WithBlockHeight(req.Header.Height) + app.deliverState.ctx = app.deliverState.ctx. + WithBlockHeader(req.Header). + WithBlockHeight(req.Header.Height) } + // add block gas meter + var gasMeter sdk.GasMeter + if maxGas := app.getMaximumBlockGas(); maxGas > 0 { + gasMeter = sdk.NewGasMeter(maxGas) + } else { + gasMeter = sdk.NewInfiniteGasMeter() + } + app.deliverState.ctx = app.deliverState.ctx.WithBlockGasMeter(gasMeter) + if app.beginBlocker != nil { res = app.beginBlocker(app.deliverState.ctx, req) } @@ -464,9 +532,10 @@ func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { // Implements ABCI func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { + // Decode the Tx. - var result sdk.Result var tx, err = app.txDecoder(txBytes) + var result sdk.Result if err != nil { result = err.Result() } else { @@ -617,6 +686,12 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk ctx := app.getContextForTx(mode, txBytes) ms := ctx.MultiStore() + // only run the tx if there is block gas remaining + if mode == runTxModeDeliver && ctx.BlockGasMeter().IsOutOfGas() { + result = sdk.ErrOutOfGas("no block gas left to run tx").Result() + return + } + defer func() { if r := recover(); r != nil { switch rType := r.(type) { @@ -633,6 +708,18 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk result.GasUsed = ctx.GasMeter().GasConsumed() }() + // If BlockGasMeter() panics it will be caught by the above recover and + // return an error - in any case BlockGasMeter will consume gas past + // the limit. + // NOTE: this must exist in a separate defer function for the + // above recovery to recover from this one + defer func() { + if mode == runTxModeDeliver { + ctx.BlockGasMeter().ConsumeGas( + ctx.GasMeter().GasConsumedToLimit(), "block gas meter") + } + }() + var msgs = tx.GetMsgs() if err := validateBasicTxMsgs(msgs); err != nil { return err.Result() @@ -704,14 +791,6 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc // Implements ABCI func (app *BaseApp) Commit() (res abci.ResponseCommit) { header := app.deliverState.ctx.BlockHeader() - /* - // Write the latest Header to the store - headerBytes, err := proto.Marshal(&header) - if err != nil { - panic(err) - } - app.db.SetSync(dbHeaderKey, headerBytes) - */ // Write the Deliver state and commit the MultiStore app.deliverState.ms.Write() diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index ab6916f82a..c09f050ede 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -56,7 +56,9 @@ func setupBaseApp(t *testing.T, options ...func(*BaseApp)) *BaseApp { require.Equal(t, t.Name(), app.Name()) // no stores are mounted - require.Panics(t, func() { app.LoadLatestVersion(capKey1) }) + require.Panics(t, func() { + app.LoadLatestVersion(capKey1) + }) app.MountStoresIAVL(capKey1, capKey2) @@ -514,6 +516,7 @@ func TestDeliverTx(t *testing.T) { } app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{}) // Create same codec used in txDecoder codec := codec.New() @@ -853,6 +856,110 @@ func TestTxGasLimits(t *testing.T) { } } +// Test that transactions exceeding gas limits fail +func TestMaxBlockGasLimits(t *testing.T) { + gasGranted := uint64(10) + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasGranted)) + + // NOTE/TODO/XXX: + // AnteHandlers must have their own defer/recover in order + // for the BaseApp to know how much gas was used used! + // This is because the GasMeter is created in the AnteHandler, + // but if it panics the context won't be set properly in runTx's recover ... + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + res = sdk.ErrOutOfGas(log).Result() + res.GasWanted = gasGranted + res.GasUsed = newCtx.GasMeter().GasConsumed() + default: + panic(r) + } + } + }() + + count := tx.(*txTest).Counter + newCtx.GasMeter().ConsumeGas(uint64(count), "counter-ante") + res = sdk.Result{ + GasWanted: gasGranted, + } + return + }) + + } + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + count := msg.(msgCounter).Counter + ctx.GasMeter().ConsumeGas(uint64(count), "counter-handler") + return sdk.Result{} + }) + } + + app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{ + ConsensusParams: &abci.ConsensusParams{ + BlockSize: &abci.BlockSizeParams{ + MaxGas: 100, + }, + }, + }) + + testCases := []struct { + tx *txTest + numDelivers int + gasUsedPerDeliver uint64 + fail bool + failAfterDeliver int + }{ + {newTxCounter(0, 0), 0, 0, false, 0}, + {newTxCounter(9, 1), 2, 10, false, 0}, + {newTxCounter(10, 0), 3, 10, false, 0}, + {newTxCounter(10, 0), 10, 10, false, 0}, + {newTxCounter(2, 7), 11, 9, false, 0}, + {newTxCounter(10, 0), 10, 10, false, 0}, // hit the limit but pass + + {newTxCounter(10, 0), 11, 10, true, 10}, + {newTxCounter(10, 0), 15, 10, true, 10}, + {newTxCounter(9, 0), 12, 9, true, 11}, // fly past the limit + } + + for i, tc := range testCases { + fmt.Printf("debug i: %v\n", i) + tx := tc.tx + + // reset the block gas + app.BeginBlock(abci.RequestBeginBlock{}) + + // execute the transaction multiple times + for j := 0; j < tc.numDelivers; j++ { + res := app.Deliver(tx) + + ctx := app.getState(runTxModeDeliver).ctx + blockGasUsed := ctx.BlockGasMeter().GasConsumed() + + // check for failed transactions + if tc.fail && (j+1) > tc.failAfterDeliver { + require.Equal(t, res.Code, sdk.CodeOutOfGas, fmt.Sprintf("%d: %v, %v", i, tc, res)) + require.Equal(t, res.Codespace, sdk.CodespaceRoot, fmt.Sprintf("%d: %v, %v", i, tc, res)) + require.True(t, ctx.BlockGasMeter().IsOutOfGas()) + } else { + // check gas used and wanted + expBlockGasUsed := tc.gasUsedPerDeliver * uint64(j+1) + require.Equal(t, expBlockGasUsed, blockGasUsed, + fmt.Sprintf("%d,%d: %v, %v, %v, %v", i, j, tc, expBlockGasUsed, blockGasUsed, res)) + + require.True(t, res.IsOK(), fmt.Sprintf("%d,%d: %v, %v", i, j, tc, res)) + require.False(t, ctx.BlockGasMeter().IsPastLimit()) + } + } + } +} + func TestBaseAppAnteHandler(t *testing.T) { anteKey := []byte("ante-key") anteOpt := func(bapp *BaseApp) { diff --git a/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index ab958e9331..15010af98f 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -41,8 +41,10 @@ type GenesisState struct { GenTxs []json.RawMessage `json:"gentxs"` } -func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, stakeData stake.GenesisState, mintData mint.GenesisState, - distrData distr.GenesisState, govData gov.GenesisState, slashingData slashing.GenesisState) GenesisState { +func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, + stakeData stake.GenesisState, mintData mint.GenesisState, + distrData distr.GenesisState, govData gov.GenesisState, + slashingData slashing.GenesisState) GenesisState { return GenesisState{ Accounts: accounts, @@ -260,7 +262,7 @@ func CollectStdTxs(cdc *codec.Codec, moniker string, genTxsDir string, genDoc tm "account %v not in genesis.json: %+v", addr, addrMap) } if acc.Coins.AmountOf(msg.Delegation.Denom).LT(msg.Delegation.Amount) { - err = fmt.Errorf("insufficient fund for the delegation: %s < %s", + err = fmt.Errorf("insufficient fund for the delegation: %v < %v", acc.Coins.AmountOf(msg.Delegation.Denom), msg.Delegation.Amount) } diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index bea3ac9525..f3a8309e06 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -62,8 +62,9 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application } func exportAppStateAndTMValidators( - logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, -) (json.RawMessage, []tmtypes.GenesisValidator, error) { + logger log.Logger, db dbm.DB, traceStore io.Writer, height int64) ( + json.RawMessage, []tmtypes.GenesisValidator, error) { + gApp := app.NewGaiaApp(logger, db, traceStore) if height != -1 { err := gApp.LoadHeight(height) diff --git a/docs/reference/baseapp.md b/docs/reference/baseapp.md index e1a80e2933..ad3b567167 100644 --- a/docs/reference/baseapp.md +++ b/docs/reference/baseapp.md @@ -61,3 +61,64 @@ persisted even when the following Handler processing logic fails. It is possible that a malicious proposer may include a transaction in a block that fails the AnteHandler. In this case, all state transitions for the offending transaction are discarded. + + +## Other ABCI Messages + +Besides `CheckTx` and `DeliverTx`, BaseApp handles the following ABCI messages. + +### Info +TODO complete description + +### SetOption +TODO complete description + +### Query +TODO complete description + +### InitChain +TODO complete description + +During chain initialization InitChain runs the initialization logic directly on +the CommitMultiStore. The deliver and check states are initialized with the +ChainID. + +Note that we do not commit after InitChain, so BeginBlock for block 1 starts +from the deliver state as initialized by InitChain. + +### BeginBlock +TODO complete description + +### EndBlock +TODO complete description + +### Commit +TODO complete description + + +## Gas Management + +### Gas: InitChain + +During InitChain, the block gas meter is initialized with an infinite amount of +gas to run any genesis transactions. + +Additionally, the InitChain request message includes ConsensusParams as +declared in the genesis.json file. + +### Gas: BeginBlock + +The block gas meter is reset during BeginBlock for the deliver state. If no +maximum block gas is set within baseapp then an infinite gas meter is set, +otherwise a gas meter with `ConsensusParam.BlockSize.MaxGas` is initialized. + +### Gas: DeliverTx + +Before the transaction logic is run, the `BlockGasMeter` is first checked to +see if any gas remains. If no gas remains, then `DeliverTx` immediately returns +an error. + +After the transaction has been processed, the used gas (up to the transaction +gas limit) is deducted from the BlockGasMeter. If the remaining gas exceeds the +meter's limits, then DeliverTx returns an error and the transaction is not +committed. diff --git a/store/rootmultistore.go b/store/rootmultistore.go index cd2d0135f1..3faf67a5e5 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -230,13 +230,19 @@ func (rs *rootMultiStore) CacheMultiStore() CacheMultiStore { } // Implements MultiStore. +// If the store does not exist, panics. func (rs *rootMultiStore) GetStore(key StoreKey) Store { - return rs.stores[key] + store := rs.stores[key] + if store == nil { + panic("Could not load store " + key.String()) + } + return store } // GetKVStore implements the MultiStore interface. If tracing is enabled on the // rootMultiStore, a wrapped TraceKVStore will be returned with the given // tracer, otherwise, the original KVStore will be returned. +// If the store does not exist, panics. func (rs *rootMultiStore) GetKVStore(key StoreKey) KVStore { store := rs.stores[key].(KVStore) diff --git a/types/context.go b/types/context.go index aac05fdfdf..add88bfc33 100644 --- a/types/context.go +++ b/types/context.go @@ -133,13 +133,13 @@ const ( contextKeyMultiStore contextKey = iota contextKeyBlockHeader contextKeyBlockHeight - contextKeyConsensusParams contextKeyChainID contextKeyIsCheckTx contextKeyTxBytes contextKeyLogger contextKeyVoteInfos contextKeyGasMeter + contextKeyBlockGasMeter contextKeyMinimumFees ) @@ -151,10 +151,6 @@ func (c Context) BlockHeader() abci.Header { return c.Value(contextKeyBlockHeade func (c Context) BlockHeight() int64 { return c.Value(contextKeyBlockHeight).(int64) } -func (c Context) ConsensusParams() abci.ConsensusParams { - return c.Value(contextKeyConsensusParams).(abci.ConsensusParams) -} - func (c Context) ChainID() string { return c.Value(contextKeyChainID).(string) } func (c Context) TxBytes() []byte { return c.Value(contextKeyTxBytes).([]byte) } @@ -167,6 +163,8 @@ func (c Context) VoteInfos() []abci.VoteInfo { func (c Context) GasMeter() GasMeter { return c.Value(contextKeyGasMeter).(GasMeter) } +func (c Context) BlockGasMeter() GasMeter { return c.Value(contextKeyBlockGasMeter).(GasMeter) } + func (c Context) IsCheckTx() bool { return c.Value(contextKeyIsCheckTx).(bool) } func (c Context) MinimumFees() Coins { return c.Value(contextKeyMinimumFees).(Coins) } @@ -198,16 +196,6 @@ func (c Context) WithBlockHeight(height int64) Context { return c.withValue(contextKeyBlockHeight, height).withValue(contextKeyBlockHeader, newHeader) } -func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context { - if params == nil { - return c - } - - // TODO: Do we need to handle invalid MaxGas values? - return c.withValue(contextKeyConsensusParams, params). - WithGasMeter(NewGasMeter(uint64(params.BlockSize.MaxGas))) -} - func (c Context) WithChainID(chainID string) Context { return c.withValue(contextKeyChainID, chainID) } func (c Context) WithTxBytes(txBytes []byte) Context { return c.withValue(contextKeyTxBytes, txBytes) } @@ -220,6 +208,10 @@ func (c Context) WithVoteInfos(VoteInfos []abci.VoteInfo) Context { func (c Context) WithGasMeter(meter GasMeter) Context { return c.withValue(contextKeyGasMeter, meter) } +func (c Context) WithBlockGasMeter(meter GasMeter) Context { + return c.withValue(contextKeyBlockGasMeter, meter) +} + func (c Context) WithIsCheckTx(isCheckTx bool) Context { return c.withValue(contextKeyIsCheckTx, isCheckTx) } diff --git a/types/gas.go b/types/gas.go index 656bd1d04c..100b004305 100644 --- a/types/gas.go +++ b/types/gas.go @@ -34,7 +34,11 @@ type ErrorGasOverflow struct { // GasMeter interface to track gas consumption type GasMeter interface { GasConsumed() Gas + GasConsumedToLimit() Gas + Limit() Gas ConsumeGas(amount Gas, descriptor string) + IsPastLimit() bool + IsOutOfGas() bool } type basicGasMeter struct { @@ -54,6 +58,17 @@ func (g *basicGasMeter) GasConsumed() Gas { return g.consumed } +func (g *basicGasMeter) Limit() Gas { + return g.limit +} + +func (g *basicGasMeter) GasConsumedToLimit() Gas { + if g.IsPastLimit() { + return g.limit + } + return g.consumed +} + func (g *basicGasMeter) ConsumeGas(amount Gas, descriptor string) { var overflow bool @@ -68,6 +83,14 @@ func (g *basicGasMeter) ConsumeGas(amount Gas, descriptor string) { } } +func (g *basicGasMeter) IsPastLimit() bool { + return g.consumed > g.limit +} + +func (g *basicGasMeter) IsOutOfGas() bool { + return g.consumed >= g.limit +} + type infiniteGasMeter struct { consumed Gas } @@ -83,6 +106,14 @@ func (g *infiniteGasMeter) GasConsumed() Gas { return g.consumed } +func (g *infiniteGasMeter) GasConsumedToLimit() Gas { + return g.consumed +} + +func (g *infiniteGasMeter) Limit() Gas { + return 0 +} + func (g *infiniteGasMeter) ConsumeGas(amount Gas, descriptor string) { var overflow bool @@ -93,6 +124,14 @@ func (g *infiniteGasMeter) ConsumeGas(amount Gas, descriptor string) { } } +func (g *infiniteGasMeter) IsPastLimit() bool { + return false +} + +func (g *infiniteGasMeter) IsOutOfGas() bool { + return false +} + // GasConfig defines gas cost for each operation on KVStores type GasConfig struct { HasCost Gas diff --git a/types/gas_test.go b/types/gas_test.go index f4452053fb..5f862dccdb 100644 --- a/types/gas_test.go +++ b/types/gas_test.go @@ -27,9 +27,18 @@ func TestGasMeter(t *testing.T) { used += usage require.NotPanics(t, func() { meter.ConsumeGas(usage, "") }, "Not exceeded limit but panicked. tc #%d, usage #%d", tcnum, unum) require.Equal(t, used, meter.GasConsumed(), "Gas consumption not match. tc #%d, usage #%d", tcnum, unum) + require.Equal(t, used, meter.GasConsumedToLimit(), "Gas consumption (to limit) not match. tc #%d, usage #%d", tcnum, unum) + require.False(t, meter.IsPastLimit(), "Not exceeded limit but got IsPastLimit() true") + if unum < len(tc.usage)-1 { + require.False(t, meter.IsOutOfGas(), "Not yet at limit but got IsOutOfGas() true") + } else { + require.True(t, meter.IsOutOfGas(), "At limit but got IsOutOfGas() false") + } } require.Panics(t, func() { meter.ConsumeGas(1, "") }, "Exceeded but not panicked. tc #%d", tcnum) + require.Equal(t, meter.GasConsumedToLimit(), meter.Limit(), "Gas consumption (to limit) not match limit") + require.Equal(t, meter.GasConsumed(), meter.Limit()+1, "Gas consumption not match limit+1") break } diff --git a/types/store.go b/types/store.go index 8fe0321f5c..c2e57a3428 100644 --- a/types/store.go +++ b/types/store.go @@ -64,6 +64,7 @@ type MultiStore interface { //nolint CacheMultiStore() CacheMultiStore // Convenience for fetching substores. + // If the store does not exist, panics. GetStore(StoreKey) Store GetKVStore(StoreKey) KVStore