diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bd85670090..3361c43616de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ FEATURES * [x/bank] Tx tags with sender/recipient for indexing & later retrieval * [x/stake] Tx tags with delegator/candidate for delegation & unbonding, and candidate info for declare candidate / edit candidacy * [x/auth] Added ability to change pubkey to auth module +* [baseapp] baseapp now has settable functions for filtering peers by address/port & public key +* [sdk] Gas consumption is now measured as transactions are executed + * Transactions which run out of gas stop execution and revert state changes + * A "simulate" query has been added to determine how much gas a transaction will need + * Modules can include their own gas costs for execution of particular message types +* [x/bank] Bank module now tags transactions with sender/recipient for indexing & later retrieval +* [x/stake] Stake module now tags transactions with delegator/candidate for delegation & unbonding, and candidate info for declare candidate / edit candidacy IMPROVEMENTS @@ -43,6 +50,7 @@ BREAKING CHANGES * gaiad init now requires use of `--name` flag * Removed Get from Msg interface * types/rational now extends big.Rat +* Queries against the store must be prefixed with the path "/store" FEATURES: @@ -55,7 +63,8 @@ FEATURES: * New genesis account keys are automatically added to the client keybase (introduce `--client-home` flag) * Initialize with genesis txs using `--gen-txs` flag * Context now has access to the application-configured logger - +* Add (non-proof) subspace query helper functions +* Add more staking query functions: candidates, delegator-bonds BUG FIXES * Gaia now uses stake, ported from github.com/cosmos/gaia diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index d3bf85fe8167..ef3bbc3c79d2 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -3,6 +3,7 @@ package baseapp import ( "fmt" "runtime/debug" + "strings" "github.com/pkg/errors" @@ -22,11 +23,24 @@ import ( // and to avoid affecting the Merkle root. var dbHeaderKey = []byte("header") +// Enum mode for app.runTx +type runTxMode uint8 + +const ( + // Check a transaction + runTxModeCheck runTxMode = iota + // Simulate a transaction + runTxModeSimulate runTxMode = iota + // Deliver a transaction + runTxModeDeliver runTxMode = iota +) + // The ABCI application type BaseApp struct { // initialized on creation Logger log.Logger name string // application name from abci.Info + cdc *wire.Codec // Amino codec db dbm.DB // common DB backend cms sdk.CommitMultiStore // Main (uncached) state router Router // handle any kind of message @@ -37,9 +51,11 @@ type BaseApp struct { anteHandler sdk.AnteHandler // ante handler for fee and auth // may be nil - 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 + 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 + addrPeerFilter sdk.PeerFilter // filter peers by address and port + pubkeyPeerFilter sdk.PeerFilter // filter peers by public key //-------------------- // Volatile @@ -61,6 +77,7 @@ func NewBaseApp(name string, cdc *wire.Codec, logger log.Logger, db dbm.DB) *Bas app := &BaseApp{ Logger: logger, name: name, + cdc: cdc, db: db, cms: store.NewCommitMultiStore(db), router: NewRouter(), @@ -137,6 +154,12 @@ func (app *BaseApp) SetEndBlocker(endBlocker sdk.EndBlocker) { func (app *BaseApp) SetAnteHandler(ah sdk.AnteHandler) { app.anteHandler = ah } +func (app *BaseApp) SetAddrPeerFilter(pf sdk.PeerFilter) { + app.addrPeerFilter = pf +} +func (app *BaseApp) SetPubKeyPeerFilter(pf sdk.PeerFilter) { + app.pubkeyPeerFilter = pf +} func (app *BaseApp) Router() Router { return app.router } // load latest application version @@ -277,15 +300,74 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC return } +// Filter peers by address / port +func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { + if app.addrPeerFilter != nil { + return app.addrPeerFilter(info) + } + return abci.ResponseQuery{} +} + +// Filter peers by public key +func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery { + if app.pubkeyPeerFilter != nil { + return app.pubkeyPeerFilter(info) + } + return abci.ResponseQuery{} +} + // Implements ABCI. // Delegates to CommitMultiStore if it implements Queryable func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { - queryable, ok := app.cms.(sdk.Queryable) - if !ok { - msg := "application doesn't support queries" - return sdk.ErrUnknownRequest(msg).QueryResult() + path := strings.Split(req.Path, "/") + // first element is empty string + if len(path) > 0 && path[0] == "" { + path = path[1:] + } + // "/app" prefix for special application queries + if len(path) >= 2 && path[0] == "app" { + var result sdk.Result + switch path[1] { + case "simulate": + txBytes := req.Data + tx, err := app.txDecoder(txBytes) + if err != nil { + result = err.Result() + } else { + result = app.Simulate(tx) + } + default: + result = sdk.ErrUnknownRequest(fmt.Sprintf("Unknown query: %s", path)).Result() + } + value := app.cdc.MustMarshalBinary(result) + return abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Value: value, + } } - return queryable.Query(req) + // "/store" prefix for store queries + if len(path) >= 1 && path[0] == "store" { + queryable, ok := app.cms.(sdk.Queryable) + if !ok { + msg := "multistore doesn't support queries" + return sdk.ErrUnknownRequest(msg).QueryResult() + } + req.Path = "/" + strings.Join(path[1:], "/") + return queryable.Query(req) + } + // "/p2p" prefix for p2p queries + if len(path) >= 4 && path[0] == "p2p" { + if path[1] == "filter" { + if path[2] == "addr" { + return app.FilterPeerByAddrPort(path[3]) + } + if path[2] == "pubkey" { + return app.FilterPeerByPubKey(path[3]) + } + } + } + msg := "unknown query path" + return sdk.ErrUnknownRequest(msg).QueryResult() } // Implements ABCI @@ -312,7 +394,7 @@ func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { if err != nil { result = err.Result() } else { - result = app.runTx(true, txBytes, tx) + result = app.runTx(runTxModeCheck, txBytes, tx) } return abci.ResponseCheckTx{ @@ -320,6 +402,7 @@ func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { Data: result.Data, Log: result.Log, GasWanted: result.GasWanted, + GasUsed: result.GasUsed, Fee: cmn.KI64Pair{ []byte(result.FeeDenom), result.FeeAmount, @@ -336,7 +419,7 @@ func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { if err != nil { result = err.Result() } else { - result = app.runTx(false, txBytes, tx) + result = app.runTx(runTxModeDeliver, txBytes, tx) } // After-handler hooks. @@ -358,22 +441,35 @@ func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { } } -// nolint- Mostly for testing +// nolint - Mostly for testing func (app *BaseApp) Check(tx sdk.Tx) (result sdk.Result) { - return app.runTx(true, nil, tx) + return app.runTx(runTxModeCheck, nil, tx) +} + +// nolint - full tx execution +func (app *BaseApp) Simulate(tx sdk.Tx) (result sdk.Result) { + return app.runTx(runTxModeSimulate, nil, tx) } + +// nolint func (app *BaseApp) Deliver(tx sdk.Tx) (result sdk.Result) { - return app.runTx(false, nil, tx) + return app.runTx(runTxModeDeliver, nil, tx) } // txBytes may be nil in some cases, eg. in tests. // Also, in the future we may support "internal" transactions. -func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk.Result) { +func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk.Result) { // Handle any panics. defer func() { if r := recover(); r != nil { - log := fmt.Sprintf("Recovered: %v\nstack:\n%v", r, string(debug.Stack())) - result = sdk.ErrInternal(log).Result() + switch r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("Out of gas in location: %v", r.(sdk.ErrorOutOfGas).Descriptor) + result = sdk.ErrOutOfGas(log).Result() + default: + log := fmt.Sprintf("Recovered: %v\nstack:\n%v", r, string(debug.Stack())) + result = sdk.ErrInternal(log).Result() + } } }() @@ -392,12 +488,17 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk // Get the context var ctx sdk.Context - if isCheckTx { + if mode == runTxModeCheck || mode == runTxModeSimulate { ctx = app.checkState.ctx.WithTxBytes(txBytes) } else { ctx = app.deliverState.ctx.WithTxBytes(txBytes) } + // Simulate a DeliverTx for gas calculation + if mode == runTxModeSimulate { + ctx = ctx.WithIsCheckTx(false) + } + // Run the ante handler. if app.anteHandler != nil { newCtx, result, abort := app.anteHandler(ctx, tx) @@ -418,7 +519,7 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk // Get the correct cache var msCache sdk.CacheMultiStore - if isCheckTx == true { + if mode == runTxModeCheck || mode == runTxModeSimulate { // CacheWrap app.checkState.ms in case it fails. msCache = app.checkState.CacheMultiStore() ctx = ctx.WithMultiStore(msCache) @@ -426,13 +527,15 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk // CacheWrap app.deliverState.ms in case it fails. msCache = app.deliverState.CacheMultiStore() ctx = ctx.WithMultiStore(msCache) - } result = handler(ctx, msg) - // If result was successful, write to app.checkState.ms or app.deliverState.ms - if result.IsOK() { + // Set gas utilized + result.GasUsed = ctx.GasMeter().GasConsumed() + + // If not a simulated run and result was successful, write to app.checkState.ms or app.deliverState.ms + if mode != runTxModeSimulate && result.IsOK() { msCache.Write() } diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index aab64fc20084..969a9f9adda6 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" "github.com/tendermint/go-crypto" @@ -16,6 +17,7 @@ import ( "github.com/tendermint/tmlibs/log" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" ) func defaultLogger() log.Logger { @@ -25,7 +27,9 @@ func defaultLogger() log.Logger { func newBaseApp(name string) *BaseApp { logger := defaultLogger() db := dbm.NewMemDB() - return NewBaseApp(name, nil, logger, db) + codec := wire.NewCodec() + wire.RegisterCrypto(codec) + return NewBaseApp(name, codec, logger, db) } func TestMountStores(t *testing.T) { @@ -167,7 +171,7 @@ func TestInitChainer(t *testing.T) { } query := abci.RequestQuery{ - Path: "/main/key", + Path: "/store/main/key", Data: key, } @@ -260,6 +264,97 @@ func TestDeliverTx(t *testing.T) { } } +func TestSimulateTx(t *testing.T) { + app := newBaseApp(t.Name()) + + // make a cap key and mount the store + capKey := sdk.NewKVStoreKey("main") + app.MountStoresIAVL(capKey) + err := app.LoadLatestVersion(capKey) // needed to make stores non-nil + assert.Nil(t, err) + + counter := 0 + app.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { return }) + app.Router().AddRoute(msgType, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx.GasMeter().ConsumeGas(10, "test") + store := ctx.KVStore(capKey) + // ensure store is never written + require.Nil(t, store.Get([]byte("key"))) + store.Set([]byte("key"), []byte("value")) + // check we can see the current header + thisHeader := ctx.BlockHeader() + height := int64(counter) + assert.Equal(t, height, thisHeader.Height) + counter++ + return sdk.Result{} + }) + + tx := testUpdatePowerTx{} // doesn't matter + header := abci.Header{AppHash: []byte("apphash")} + + app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) { + var ttx testUpdatePowerTx + fromJSON(txBytes, &ttx) + return ttx, nil + }) + + nBlocks := 3 + for blockN := 0; blockN < nBlocks; blockN++ { + // block1 + header.Height = int64(blockN + 1) + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + result := app.Simulate(tx) + require.Equal(t, result.Code, sdk.ABCICodeOK) + require.Equal(t, int64(80), result.GasUsed) + counter-- + encoded, err := json.Marshal(tx) + require.Nil(t, err) + query := abci.RequestQuery{ + Path: "/app/simulate", + Data: encoded, + } + queryResult := app.Query(query) + require.Equal(t, queryResult.Code, uint32(sdk.ABCICodeOK)) + var res sdk.Result + app.cdc.MustUnmarshalBinary(queryResult.Value, &res) + require.Equal(t, sdk.ABCICodeOK, res.Code) + require.Equal(t, int64(160), res.GasUsed) + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + } +} + +// Test that transactions exceeding gas limits fail +func TestTxGasLimits(t *testing.T) { + logger := defaultLogger() + db := dbm.NewMemDB() + app := NewBaseApp(t.Name(), nil, logger, db) + + // make a cap key and mount the store + capKey := sdk.NewKVStoreKey("main") + app.MountStoresIAVL(capKey) + err := app.LoadLatestVersion(capKey) // needed to make stores non-nil + assert.Nil(t, err) + + app.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { + newCtx = ctx.WithGasMeter(sdk.NewGasMeter(0)) + return + }) + app.Router().AddRoute(msgType, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx.GasMeter().ConsumeGas(10, "counter") + return sdk.Result{} + }) + + tx := testUpdatePowerTx{} // doesn't matter + header := abci.Header{AppHash: []byte("apphash")} + + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + res := app.Deliver(tx) + assert.Equal(t, res.Code, sdk.ToABCICode(sdk.CodespaceRoot, sdk.CodeOutOfGas), "Expected transaction to run out of gas") + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() +} + // Test that we can only query from the latest committed state. func TestQuery(t *testing.T) { app := newBaseApp(t.Name()) @@ -280,7 +375,7 @@ func TestQuery(t *testing.T) { }) query := abci.RequestQuery{ - Path: "/main/key", + Path: "/store/main/key", Data: key, } @@ -307,6 +402,39 @@ func TestQuery(t *testing.T) { assert.Equal(t, value, res.Value) } +// Test p2p filter queries +func TestP2PQuery(t *testing.T) { + app := newBaseApp(t.Name()) + + // make a cap key and mount the store + capKey := sdk.NewKVStoreKey("main") + app.MountStoresIAVL(capKey) + err := app.LoadLatestVersion(capKey) // needed to make stores non-nil + assert.Nil(t, err) + + app.SetAddrPeerFilter(func(addrport string) abci.ResponseQuery { + require.Equal(t, "1.1.1.1:8000", addrport) + return abci.ResponseQuery{Code: uint32(3)} + }) + + app.SetPubKeyPeerFilter(func(pubkey string) abci.ResponseQuery { + require.Equal(t, "testpubkey", pubkey) + return abci.ResponseQuery{Code: uint32(4)} + }) + + addrQuery := abci.RequestQuery{ + Path: "/p2p/filter/addr/1.1.1.1:8000", + } + res := app.Query(addrQuery) + require.Equal(t, uint32(3), res.Code) + + pubkeyQuery := abci.RequestQuery{ + Path: "/p2p/filter/pubkey/testpubkey", + } + res = app.Query(pubkeyQuery) + require.Equal(t, uint32(4), res.Code) +} + //---------------------- // TODO: clean this up diff --git a/client/context/helpers.go b/client/context/helpers.go index c3dc0a4abb24..562bde9b4ca7 100644 --- a/client/context/helpers.go +++ b/client/context/helpers.go @@ -58,8 +58,7 @@ func (ctx CoreContext) QuerySubspace(cdc *wire.Codec, subspace []byte, storeName // Query from Tendermint with the provided storename and path func (ctx CoreContext) query(key cmn.HexBytes, storeName, endPath string) (res []byte, err error) { - - path := fmt.Sprintf("/%s/%s", storeName, endPath) + path := fmt.Sprintf("/store/%s/key", storeName) node, err := ctx.GetNode() if err != nil { return res, err @@ -114,6 +113,7 @@ func (ctx CoreContext) SignAndBuild(name, passphrase string, msg sdk.Msg, cdc *w ChainID: chainID, Sequences: []int64{sequence}, Msg: msg, + Fee: sdk.NewStdFee(10000, sdk.Coin{}), // TODO run simulate to estimate gas? } keybase, err := keys.GetKeyBase() diff --git a/cmd/gaia/app/app_test.go b/cmd/gaia/app/app_test.go index 3bca2654b839..fd26ce27a319 100644 --- a/cmd/gaia/app/app_test.go +++ b/cmd/gaia/app/app_test.go @@ -40,7 +40,7 @@ var ( manyCoins = sdk.Coins{{"foocoin", 1}, {"barcoin", 1}} fee = sdk.StdFee{ sdk.Coins{{"foocoin", 0}}, - 0, + 100000, } sendMsg1 = bank.MsgSend{ diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index 034e198609ad..d0b59f331372 100644 --- a/examples/basecoin/app/app_test.go +++ b/examples/basecoin/app/app_test.go @@ -39,7 +39,7 @@ var ( manyCoins = sdk.Coins{{"foocoin", 1}, {"barcoin", 1}} fee = sdk.StdFee{ sdk.Coins{{"foocoin", 0}}, - 0, + 100000, } sendMsg1 = bank.MsgSend{ diff --git a/examples/democoin/app/app_test.go b/examples/democoin/app/app_test.go index c67782d926e3..b0f188f10b37 100644 --- a/examples/democoin/app/app_test.go +++ b/examples/democoin/app/app_test.go @@ -33,7 +33,7 @@ var ( coins = sdk.Coins{{"foocoin", 10}} fee = sdk.StdFee{ sdk.Coins{{"foocoin", 0}}, - 0, + 1000000, } sendMsg = bank.MsgSend{ diff --git a/mock/app_test.go b/mock/app_test.go index 7c84f9a1d197..be1d778295bb 100644 --- a/mock/app_test.go +++ b/mock/app_test.go @@ -31,10 +31,9 @@ func TestInitApp(t *testing.T) { app.InitChain(req) app.Commit() - // XXX test failing // make sure we can query these values query := abci.RequestQuery{ - Path: "/main/key", + Path: "/store/main/key", Data: []byte("foo"), } qres := app.Query(query) @@ -70,7 +69,7 @@ func TestDeliverTx(t *testing.T) { // make sure we can query these values query := abci.RequestQuery{ - Path: "/main/key", + Path: "/store/main/key", Data: []byte(key), } qres := app.Query(query) diff --git a/mock/store.go b/mock/store.go index 329eb250bbde..7f62234eaffa 100644 --- a/mock/store.go +++ b/mock/store.go @@ -50,6 +50,10 @@ func (ms multiStore) GetKVStore(key sdk.StoreKey) sdk.KVStore { return ms.kv[key] } +func (ms multiStore) GetKVStoreWithGas(meter sdk.GasMeter, key sdk.StoreKey) sdk.KVStore { + panic("not implemented") +} + func (ms multiStore) GetStore(key sdk.StoreKey) sdk.Store { panic("not implemented") } diff --git a/store/cachemultistore.go b/store/cachemultistore.go index b1a7548811ea..47878fb15511 100644 --- a/store/cachemultistore.go +++ b/store/cachemultistore.go @@ -72,3 +72,8 @@ func (cms cacheMultiStore) GetStore(key StoreKey) Store { func (cms cacheMultiStore) GetKVStore(key StoreKey) KVStore { return cms.stores[key].(KVStore) } + +// Implements MultiStore. +func (cms cacheMultiStore) GetKVStoreWithGas(meter sdk.GasMeter, key StoreKey) KVStore { + return NewGasKVStore(meter, cms.GetKVStore(key)) +} diff --git a/store/gaskvstore.go b/store/gaskvstore.go new file mode 100644 index 000000000000..9f50f3444162 --- /dev/null +++ b/store/gaskvstore.go @@ -0,0 +1,148 @@ +package store + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// nolint +const ( + HasCost = 10 + ReadCostFlat = 10 + ReadCostPerByte = 1 + WriteCostFlat = 10 + WriteCostPerByte = 10 + KeyCostFlat = 5 + ValueCostFlat = 10 + ValueCostPerByte = 1 +) + +// gasKVStore applies gas tracking to an underlying kvstore +type gasKVStore struct { + gasMeter sdk.GasMeter + parent sdk.KVStore +} + +// nolint +func NewGasKVStore(gasMeter sdk.GasMeter, parent sdk.KVStore) *gasKVStore { + kvs := &gasKVStore{ + gasMeter: gasMeter, + parent: parent, + } + return kvs +} + +// Implements Store. +func (gi *gasKVStore) GetStoreType() sdk.StoreType { + return gi.parent.GetStoreType() +} + +// Implements KVStore. +func (gi *gasKVStore) Get(key []byte) (value []byte) { + gi.gasMeter.ConsumeGas(ReadCostFlat, "GetFlat") + value = gi.parent.Get(key) + // TODO overflow-safe math? + gi.gasMeter.ConsumeGas(ReadCostPerByte*sdk.Gas(len(value)), "ReadPerByte") + return value +} + +// Implements KVStore. +func (gi *gasKVStore) Set(key []byte, value []byte) { + gi.gasMeter.ConsumeGas(WriteCostFlat, "SetFlat") + // TODO overflow-safe math? + gi.gasMeter.ConsumeGas(WriteCostPerByte*sdk.Gas(len(value)), "SetPerByte") + gi.parent.Set(key, value) +} + +// Implements KVStore. +func (gi *gasKVStore) Has(key []byte) bool { + gi.gasMeter.ConsumeGas(HasCost, "Has") + return gi.parent.Has(key) +} + +// Implements KVStore. +func (gi *gasKVStore) Delete(key []byte) { + // No gas costs for deletion + gi.parent.Delete(key) +} + +// Implements KVStore. +func (gi *gasKVStore) Iterator(start, end []byte) sdk.Iterator { + return gi.iterator(start, end, true) +} + +// Implements KVStore. +func (gi *gasKVStore) ReverseIterator(start, end []byte) sdk.Iterator { + return gi.iterator(start, end, false) +} + +// Implements KVStore. +func (gi *gasKVStore) SubspaceIterator(prefix []byte) sdk.Iterator { + return gi.iterator(prefix, sdk.PrefixEndBytes(prefix), true) +} + +// Implements KVStore. +func (gi *gasKVStore) ReverseSubspaceIterator(prefix []byte) sdk.Iterator { + return gi.iterator(prefix, sdk.PrefixEndBytes(prefix), false) +} + +// Implements KVStore. +func (gi *gasKVStore) CacheWrap() sdk.CacheWrap { + panic("you cannot CacheWrap a GasKVStore") +} + +func (gi *gasKVStore) iterator(start, end []byte, ascending bool) sdk.Iterator { + var parent sdk.Iterator + if ascending { + parent = gi.parent.Iterator(start, end) + } else { + parent = gi.parent.ReverseIterator(start, end) + } + return newGasIterator(gi.gasMeter, parent) +} + +type gasIterator struct { + gasMeter sdk.GasMeter + parent sdk.Iterator +} + +func newGasIterator(gasMeter sdk.GasMeter, parent sdk.Iterator) sdk.Iterator { + return &gasIterator{ + gasMeter: gasMeter, + parent: parent, + } +} + +// Implements Iterator. +func (g *gasIterator) Domain() (start []byte, end []byte) { + return g.parent.Domain() +} + +// Implements Iterator. +func (g *gasIterator) Valid() bool { + return g.parent.Valid() +} + +// Implements Iterator. +func (g *gasIterator) Next() { + g.parent.Next() +} + +// Implements Iterator. +func (g *gasIterator) Key() (key []byte) { + g.gasMeter.ConsumeGas(KeyCostFlat, "KeyFlat") + key = g.parent.Key() + return key +} + +// Implements Iterator. +func (g *gasIterator) Value() (value []byte) { + value = g.parent.Value() + g.gasMeter.ConsumeGas(ValueCostFlat, "ValueFlat") + g.gasMeter.ConsumeGas(ValueCostPerByte*sdk.Gas(len(value)), "ValuePerByte") + return value +} + +// Implements Iterator. +func (g *gasIterator) Close() { + g.parent.Close() +} diff --git a/store/gaskvstore_test.go b/store/gaskvstore_test.go new file mode 100644 index 000000000000..524dc53237c3 --- /dev/null +++ b/store/gaskvstore_test.go @@ -0,0 +1,68 @@ +package store + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tmlibs/db" +) + +func newGasKVStore() KVStore { + meter := sdk.NewGasMeter(1000) + mem := dbStoreAdapter{dbm.NewMemDB()} + return NewGasKVStore(meter, mem) +} + +func TestGasKVStoreBasic(t *testing.T) { + mem := dbStoreAdapter{dbm.NewMemDB()} + meter := sdk.NewGasMeter(1000) + st := NewGasKVStore(meter, mem) + require.Empty(t, st.Get(keyFmt(1)), "Expected `key1` to be empty") + st.Set(keyFmt(1), valFmt(1)) + require.Equal(t, valFmt(1), st.Get(keyFmt(1))) + st.Delete(keyFmt(1)) + require.Empty(t, st.Get(keyFmt(1)), "Expected `key1` to be empty") + require.Equal(t, meter.GasConsumed(), sdk.Gas(183)) +} + +func TestGasKVStoreIterator(t *testing.T) { + mem := dbStoreAdapter{dbm.NewMemDB()} + meter := sdk.NewGasMeter(1000) + st := NewGasKVStore(meter, mem) + require.Empty(t, st.Get(keyFmt(1)), "Expected `key1` to be empty") + require.Empty(t, st.Get(keyFmt(2)), "Expected `key2` to be empty") + st.Set(keyFmt(1), valFmt(1)) + st.Set(keyFmt(2), valFmt(2)) + iterator := st.Iterator(nil, nil) + ka := iterator.Key() + require.Equal(t, ka, keyFmt(1)) + va := iterator.Value() + require.Equal(t, va, valFmt(1)) + iterator.Next() + kb := iterator.Key() + require.Equal(t, kb, keyFmt(2)) + vb := iterator.Value() + require.Equal(t, vb, valFmt(2)) + iterator.Next() + require.False(t, iterator.Valid()) + require.Panics(t, iterator.Next) + require.Equal(t, meter.GasConsumed(), sdk.Gas(356)) +} + +func TestGasKVStoreOutOfGasSet(t *testing.T) { + mem := dbStoreAdapter{dbm.NewMemDB()} + meter := sdk.NewGasMeter(0) + st := NewGasKVStore(meter, mem) + require.Panics(t, func() { st.Set(keyFmt(1), valFmt(1)) }, "Expected out-of-gas") +} + +func TestGasKVStoreOutOfGasIterator(t *testing.T) { + mem := dbStoreAdapter{dbm.NewMemDB()} + meter := sdk.NewGasMeter(200) + st := NewGasKVStore(meter, mem) + st.Set(keyFmt(1), valFmt(1)) + iterator := st.Iterator(nil, nil) + iterator.Next() + require.Panics(t, func() { iterator.Value() }, "Expected out-of-gas") +} diff --git a/store/rootmultistore.go b/store/rootmultistore.go index 217e8eb1405e..11cebc22ea9d 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -183,6 +183,11 @@ func (rs *rootMultiStore) GetKVStore(key StoreKey) KVStore { return rs.stores[key].(KVStore) } +// Implements MultiStore. +func (rs *rootMultiStore) GetKVStoreWithGas(meter sdk.GasMeter, key StoreKey) KVStore { + return NewGasKVStore(meter, rs.GetKVStore(key)) +} + // getStoreByName will first convert the original name to // a special key, before looking up the CommitStore. // This is not exposed to the extensions (which will need the diff --git a/types/abci.go b/types/abci.go index 40651163c436..a46e797ebe74 100644 --- a/types/abci.go +++ b/types/abci.go @@ -10,3 +10,6 @@ type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeg // run code after the transactions in a block and return updates to the validator set type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock + +// respond to p2p filtering queries from Tendermint +type PeerFilter func(info string) abci.ResponseQuery diff --git a/types/context.go b/types/context.go index e28523ebeba6..4ab0a5d0932e 100644 --- a/types/context.go +++ b/types/context.go @@ -43,6 +43,7 @@ func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, txBytes []byt c = c.WithIsCheckTx(isCheckTx) c = c.WithTxBytes(txBytes) c = c.WithLogger(logger) + c = c.WithGasMeter(NewInfiniteGasMeter()) return c } @@ -68,7 +69,7 @@ func (c Context) Value(key interface{}) interface{} { // KVStore fetches a KVStore from the MultiStore. func (c Context) KVStore(key StoreKey) KVStore { - return c.multiStore().GetKVStore(key) + return c.multiStore().GetKVStoreWithGas(c.GasMeter(), key) } //---------------------------------------- @@ -127,6 +128,7 @@ const ( contextKeyIsCheckTx contextKeyTxBytes contextKeyLogger + contextKeyGasMeter ) // NOTE: Do not expose MultiStore. @@ -155,6 +157,9 @@ func (c Context) TxBytes() []byte { func (c Context) Logger() log.Logger { return c.Value(contextKeyLogger).(log.Logger) } +func (c Context) GasMeter() GasMeter { + return c.Value(contextKeyGasMeter).(GasMeter) +} func (c Context) WithMultiStore(ms MultiStore) Context { return c.withValue(contextKeyMultiStore, ms) } @@ -177,6 +182,9 @@ func (c Context) WithTxBytes(txBytes []byte) Context { func (c Context) WithLogger(logger log.Logger) Context { return c.withValue(contextKeyLogger, logger) } +func (c Context) WithGasMeter(meter GasMeter) Context { + return c.withValue(contextKeyGasMeter, meter) +} // Cache the multistore and return a new cached context. The cached context is // written to the context when writeCache is called. diff --git a/types/errors.go b/types/errors.go index 059a0dd749d9..20d4524644ce 100644 --- a/types/errors.go +++ b/types/errors.go @@ -52,6 +52,7 @@ const ( CodeUnknownAddress CodeType = 9 CodeInsufficientCoins CodeType = 10 CodeInvalidCoins CodeType = 11 + CodeOutOfGas CodeType = 12 // CodespaceRoot is a codespace for error codes in this file only. // Notice that 0 is an "unset" codespace, which can be overridden with @@ -88,6 +89,8 @@ func CodeToDefaultMsg(code CodeType) string { return "Insufficient coins" case CodeInvalidCoins: return "Invalid coins" + case CodeOutOfGas: + return "Out of gas" default: return fmt.Sprintf("Unknown code %d", code) } @@ -131,6 +134,9 @@ func ErrInsufficientCoins(msg string) Error { func ErrInvalidCoins(msg string) Error { return newErrorWithRootCodespace(CodeInvalidCoins, msg) } +func ErrOutOfGas(msg string) Error { + return newErrorWithRootCodespace(CodeOutOfGas, msg) +} //---------------------------------------- // Error & sdkError diff --git a/types/gas.go b/types/gas.go new file mode 100644 index 000000000000..49bfa27ece87 --- /dev/null +++ b/types/gas.go @@ -0,0 +1,58 @@ +package types + +import () + +// Gas measured by the SDK +type Gas = int64 + +// Error thrown when out of gas +type ErrorOutOfGas struct { + Descriptor string +} + +// GasMeter interface to track gas consumption +type GasMeter interface { + GasConsumed() Gas + ConsumeGas(amount Gas, descriptor string) +} + +type basicGasMeter struct { + limit Gas + consumed Gas +} + +func NewGasMeter(limit Gas) GasMeter { + return &basicGasMeter{ + limit: limit, + consumed: 0, + } +} + +func (g *basicGasMeter) GasConsumed() Gas { + return g.consumed +} + +func (g *basicGasMeter) ConsumeGas(amount Gas, descriptor string) { + g.consumed += amount + if g.consumed > g.limit { + panic(ErrorOutOfGas{descriptor}) + } +} + +type infiniteGasMeter struct { + consumed Gas +} + +func NewInfiniteGasMeter() GasMeter { + return &infiniteGasMeter{ + consumed: 0, + } +} + +func (g *infiniteGasMeter) GasConsumed() Gas { + return g.consumed +} + +func (g *infiniteGasMeter) ConsumeGas(amount Gas, descriptor string) { + g.consumed += amount +} diff --git a/types/store.go b/types/store.go index f8367a1260d1..abf02ec071a5 100644 --- a/types/store.go +++ b/types/store.go @@ -49,6 +49,7 @@ type MultiStore interface { //nolint // Convenience for fetching substores. GetStore(StoreKey) Store GetKVStore(StoreKey) KVStore + GetKVStoreWithGas(GasMeter, StoreKey) KVStore } // From MultiStore.CacheMultiStore().... diff --git a/x/auth/ante.go b/x/auth/ante.go index 4be1c6674dbc..248083206d5c 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/viper" ) +const ( + verifyCost = 100 +) + // NewAnteHandler returns an AnteHandler that checks // and increments sequence numbers, checks signatures, // and deducts fees from the first signer. @@ -88,6 +92,9 @@ func NewAnteHandler(am sdk.AccountMapper, feeHandler sdk.FeeHandler) sdk.AnteHan // cache the signer accounts in the context ctx = WithSigners(ctx, signerAccs) + // set the gas meter + ctx = ctx.WithGasMeter(sdk.NewGasMeter(stdTx.Fee.Gas)) + // TODO: tx tags (?) return ctx, sdk.Result{}, false // continue... @@ -134,6 +141,7 @@ func processSig( } // Check sig. + ctx.GasMeter().ConsumeGas(verifyCost, "ante verify") if !pubKey.VerifyBytes(signBytes, sig.Signature) { return nil, sdk.ErrUnauthorized("signature verification failed").Result() } diff --git a/x/bank/keeper.go b/x/bank/keeper.go index d23167c3c52b..6ef73c68b619 100644 --- a/x/bank/keeper.go +++ b/x/bank/keeper.go @@ -6,6 +6,14 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +const ( + costGetCoins sdk.Gas = 10 + costHasCoins sdk.Gas = 10 + costSetCoins sdk.Gas = 100 + costSubtractCoins sdk.Gas = 10 + costAddCoins sdk.Gas = 10 +) + // Keeper manages transfers between accounts type Keeper struct { am sdk.AccountMapper @@ -108,6 +116,7 @@ func (keeper ViewKeeper) HasCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coi //______________________________________________________________________________________________ func getCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address) sdk.Coins { + ctx.GasMeter().ConsumeGas(costGetCoins, "getCoins") acc := am.GetAccount(ctx, addr) if acc == nil { return sdk.Coins{} @@ -116,6 +125,7 @@ func getCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address) sdk.Coins } func setCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) sdk.Error { + ctx.GasMeter().ConsumeGas(costSetCoins, "setCoins") acc := am.GetAccount(ctx, addr) if acc == nil { acc = am.NewAccountWithAddress(ctx, addr) @@ -127,11 +137,13 @@ func setCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.C // HasCoins returns whether or not an account has at least amt coins. func hasCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) bool { + ctx.GasMeter().ConsumeGas(costHasCoins, "hasCoins") return getCoins(ctx, am, addr).IsGTE(amt) } // SubtractCoins subtracts amt from the coins at the addr. func subtractCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { + ctx.GasMeter().ConsumeGas(costSubtractCoins, "subtractCoins") oldCoins := getCoins(ctx, am, addr) newCoins := oldCoins.Minus(amt) if !newCoins.IsNotNegative() { @@ -144,6 +156,7 @@ func subtractCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt // AddCoins adds amt to the coins at the addr. func addCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { + ctx.GasMeter().ConsumeGas(costAddCoins, "addCoins") oldCoins := getCoins(ctx, am, addr) newCoins := oldCoins.Plus(amt) if !newCoins.IsNotNegative() { diff --git a/x/bank/keeper_test.go b/x/bank/keeper_test.go index 3db16c5f92fd..117c69e7aece 100644 --- a/x/bank/keeper_test.go +++ b/x/bank/keeper_test.go @@ -65,8 +65,7 @@ func TestKeeper(t *testing.T) { coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) - _, _, err := coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) - assert.Implements(t, (*sdk.Error)(nil), err) + coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}})