diff --git a/CHANGELOG.md b/CHANGELOG.md index f57466711..52006fe0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,48 @@ # Changelog -## 0.16.0 (TBD) +## 0.18.0 (TBD) + +FEATURES + +* [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 + +## 0.17.1 (May 17, 2018) + +Update to Tendermint v0.19.4 (fixes a consensus bug and improves logging) + +## 0.17.0 (May 15, 2018) + +BREAKING CHANGES + +* [stake] MarshalJSON -> MarshalBinary + +FEATURES + +* [gaiacli] Support queries for candidates, delegator-bonds +* [gaiad] Added `gaiad export` command to export current state to JSON +* [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 + +IMPROVEMENTS + +* [gaiad] Update for Tendermint v0.19.3 (improve `/dump_consensus_state` and add + `/consensus_state`) +* [spec/ibc] Added spec! +* [spec/stake] Cleanup structure, include details about slashing and + auto-unbonding +* [spec/governance] Fixup some names and pseudocode +* NOTE: specs are still a work-in-progress ... + +BUG FIXES + +* Auto-sequencing now works correctly + +## 0.16.0 (May 14th, 2018) BREAKING CHANGES @@ -15,10 +57,10 @@ 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: -* Added `gaiad export` command, which exports genesis information & current state * Gaia stake commands include, DeclareCandidacy, EditCandidacy, Delegate, Unbond * MountStoreWithDB without providing a custom store works. * Repo is now lint compliant / GoMetaLinter with tendermint-lint integrated into CI @@ -30,12 +72,9 @@ FEATURES: * Context now has access to the application-configured logger * Add (non-proof) subspace query helper functions * Add more staking query functions: candidates, delegator-bonds -* Bank module now tags transactions with sender/recipient for indexing & later retrieval -* Stake module now tags transactions with delegator/candidate for delegation & unbonding, and candidate info for declare candidate / edit candidacy BUG FIXES * Gaia now uses stake, ported from github.com/cosmos/gaia -* Auto-sequencing now works correctly ## 0.15.1 (April 29, 2018) diff --git a/Gopkg.lock b/Gopkg.lock index f474e7756..1bb4ec31a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -87,14 +87,14 @@ [[projects]] name = "github.com/gorilla/context" packages = ["."] - revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" - version = "v1.1" + revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" + version = "v1.1.1" [[projects]] name = "github.com/gorilla/mux" packages = ["."] - revision = "53c1911da2b537f792e7cafcb446b05ffe33b996" - version = "v1.6.1" + revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" + version = "v1.6.2" [[projects]] name = "github.com/gorilla/websocket" @@ -159,7 +159,7 @@ branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + revision = "bb74f1db0675b241733089d5a1faa5dd8b0ef57b" [[projects]] name = "github.com/pelletier/go-toml" @@ -183,7 +183,7 @@ branch = "master" name = "github.com/rcrowley/go-metrics" packages = ["."] - revision = "d932a24a8ccb8fcadc993e5c6c58f93dac168294" + revision = "e2704e165165ec55d062f5919b4b29494e9fa790" [[projects]] name = "github.com/spf13/afero" @@ -250,7 +250,7 @@ "leveldb/table", "leveldb/util" ] - revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad" + revision = "9637fa0b2f0db13c99d899b91007edb7df4610b7" [[projects]] name = "github.com/tendermint/abci" @@ -323,7 +323,6 @@ "p2p", "p2p/conn", "p2p/pex", - "p2p/trust", "p2p/upnp", "proxy", "rpc/client", @@ -342,8 +341,8 @@ "types/priv_validator", "version" ] - revision = "26f633ed48441f72895b710f0e87b7b6c6791066" - version = "v0.19.1" + revision = "19ccd1842fe5efffcc2ff32e6cfc127ca0cd1f9b" + version = "v0.19.4-rc0" [[projects]] name = "github.com/tendermint/tmlibs" @@ -360,8 +359,8 @@ "pubsub", "pubsub/query" ] - revision = "d94e312673e16a11ea55d742cefb3e331228f898" - version = "v0.8.2" + revision = "cc5f287c4798ffe88c04d02df219ecb6932080fd" + version = "v0.8.3-rc0" [[projects]] branch = "master" @@ -377,7 +376,7 @@ "ripemd160", "salsa20/salsa" ] - revision = "b49d69b5da943f7ef3c9cf91c8777c1f78a0cc3c" + revision = "2fc4c88bf43f0ea5ea305eae2b7af24b2cc93287" [[projects]] branch = "master" @@ -389,16 +388,15 @@ "http2/hpack", "idna", "internal/timeseries", - "lex/httplex", "trace" ] - revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23" + revision = "2491c5de3490fced2f6cff376127c667efeed857" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "cbbc999da32df943dac6cd71eb3ee39e1d7838b9" + revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b" [[projects]] name = "golang.org/x/text" @@ -422,10 +420,9 @@ version = "v0.3.0" [[projects]] - branch = "master" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] - revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2" + revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200" [[projects]] name = "google.golang.org/grpc" @@ -460,6 +457,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "fad966346d3b6042faf2bf793168b6ce9a8ff59ae207b7ad57008ead0f3ff7d4" + inputs-digest = "8c37fee3e6d3034b865c68185f4796223fe53b1b0ee4a305adbee5f53c50bfce" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d2d4ee2ff..13060b061 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -70,11 +70,16 @@ [[constraint]] name = "github.com/tendermint/tendermint" - version = "0.19.1" + version = "0.19.4-rc0" [[override]] name = "github.com/tendermint/tmlibs" - version = "~0.8.2-rc1" + version = "~0.8.3-rc0" + +# this got updated and broke, so locked to an old working commit ... +[[override]] + name = "google.golang.org/genproto" + revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200" [prune] go-tests = true diff --git a/README.md b/README.md index 640cd2874..3faf2c8af 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ master | [![CircleCI](https://circleci.com/gh/cosmos/cosmos-sdk/tree/master.s **WARNING**: the libraries are still undergoing breaking changes as we get better ideas and start building out the Apps. -**Note**: Requires [Go 1.9+](https://golang.org/dl/) +**Note**: Requires [Go 1.10+](https://golang.org/dl/) ## Overview diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index d3bf85fe8..ef3bbc3c7 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:] } - return queryable.Query(req) + // "/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, + } + } + // "/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 aab64fc20..969a9f9ad 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 c3dc0a4ab..562bde9b4 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 3bca2654b..fd26ce27a 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/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index 9d88953cf..7cb7564dd 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -66,8 +66,9 @@ func GaiaAppInit() server.AppInit { fsAppGenTx := pflag.NewFlagSet("", pflag.ContinueOnError) fsAppGenTx.String(flagName, "", "validator moniker, if left blank, do not add validator") - fsAppGenTx.String(flagClientHome, DefaultCLIHome, "home directory for the client, used for key generation") - fsAppGenTx.Bool(flagOWK, false, "overwrite the for the accounts created") + fsAppGenTx.String(flagClientHome, DefaultCLIHome, + "home directory for the client, used for key generation") + fsAppGenTx.Bool(flagOWK, false, "overwrite the accounts created") return server.AppInit{ FlagsAppGenState: fsAppGenState, diff --git a/docs/sdk/lcd-rest-api.yaml b/docs/sdk/lcd-rest-api.yaml index 7d38274c0..f88d4380f 100644 --- a/docs/sdk/lcd-rest-api.yaml +++ b/docs/sdk/lcd-rest-api.yaml @@ -2,8 +2,8 @@ openapi: 3.0.0 servers: - url: 'http://localhost:8998' info: - version: "1.0.0-oas3" - title: Light client daemon to interface with Cosmos baseserver via REST + version: "1.1.0" + title: Light client daemon to interface with full Gaia node via REST description: Specification for the LCD provided by `gaia rest-server` paths: @@ -13,7 +13,7 @@ paths: description: Get the version of the LCD running locally to compare against expected responses: 200: - description: Plaintext version i.e. "v0.5.0" + description: Plaintext version i.e. "0.16.0-dev-26440095" /node_info: description: Only the node info. Block information can be queried via /block/latest get: @@ -26,25 +26,31 @@ paths: schema: type: object properties: + id: + description: ??? + type: string + listen_addr: + type: string + example: 192.168.56.1:46656 + network: + type: string + example: gaia-5000 + version: + description: Tendermint version + type: string + example: 0.19.1 + channels: + description: ??? + type: string pub_key: $ref: '#/components/schemas/PubKey' moniker: type: string example: 159.89.198.221 - network: - type: string - example: gaia-2 remote_addr: type: string - listen_addr: - type: string - example: 192.168.56.1:46656 - version: - description: Tendermint version - type: string - example: 0.15.0 other: - description: more information on versions + description: more information on versions and options for the node type: array /syncing: get: @@ -204,7 +210,11 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Balance" + type: + description: "???" + type: string + value: + $ref: "#/components/schemas/Balance" 204: description: There is no data for the requested account. This is not a 404 as the account might exist, just does not hold data. /accounts/{address}/send: @@ -226,6 +236,7 @@ paths: type: object properties: name: + description: Name of locally stored key type: string password: type: string @@ -234,6 +245,9 @@ paths: items: $ref: "#/components/schemas/Coins" chain_id: + description: Target chain + type: string + src_chain_id: type: string squence: type: number @@ -242,19 +256,6 @@ paths: description: Tx was send and will probably be added to the next block 400: description: The Tx was malformated - /accounts/{address}/nonce: - parameters: - - in: path - name: address - description: Account address - required: true - schema: - $ref: "#/components/schemas/Address" - get: - summary: Get the nonce for a certain account - responses: - 200: - description: Plaintext nonce i.e. "4" defaults to "0" /blocks/latest: get: summary: Get the latest block @@ -667,15 +668,16 @@ components: Balance: type: object properties: - height: - type: number - example: 123456 + address: + type: string coins: type: array items: $ref: "#/components/schemas/Coins" - credit: - type: array + public_key: + $ref: "#/components/schemas/PubKey" + sequence: + type: number BlockID: type: object properties: diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index d3eae85c0..b1a434fa2 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -70,6 +70,7 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // register message routes app.Router(). + AddRoute("auth", auth.NewHandler(app.accountMapper.(auth.AccountMapper))). AddRoute("bank", bank.NewHandler(app.coinKeeper)). AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)). AddRoute("stake", stake.NewHandler(app.stakeKeeper)) diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index b502aae46..d0b59f331 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{ @@ -208,6 +208,61 @@ func TestGenesis(t *testing.T) { assert.Equal(t, acc, res1) } +func TestMsgChangePubKey(t *testing.T) { + + bapp := newBasecoinApp() + + // Construct some genesis bytes to reflect basecoin/types/AppAccount + // Give 77 foocoin to the first key + coins, err := sdk.ParseCoins("77foocoin") + require.Nil(t, err) + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + + // Construct genesis state + err = setGenesisAccounts(bapp, baseAcc) + assert.Nil(t, err) + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, baseAcc, res1.(*types.AppAccount).BaseAccount) + + // Run a CheckDeliver + SignCheckDeliver(t, bapp, sendMsg1, []int64{0}, true, priv1) + + // Check balances + CheckBalance(t, bapp, addr1, "67foocoin") + CheckBalance(t, bapp, addr2, "10foocoin") + + changePubKeyMsg := auth.MsgChangeKey{ + Address: addr1, + NewPubKey: priv2.PubKey(), + } + + ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) + acc := bapp.accountMapper.GetAccount(ctxDeliver, addr1) + + // send a MsgChangePubKey + SignCheckDeliver(t, bapp, changePubKeyMsg, []int64{1}, true, priv1) + acc = bapp.accountMapper.GetAccount(ctxDeliver, addr1) + + assert.True(t, priv2.PubKey().Equals(acc.GetPubKey())) + + // signing a SendMsg with the old privKey should be an auth error + tx := genTx(sendMsg1, []int64{2}, priv1) + res := bapp.Deliver(tx) + assert.Equal(t, sdk.ToABCICode(sdk.CodespaceRoot, sdk.CodeUnauthorized), res.Code, res.Log) + + // resigning the tx with the new correct priv key should work + SignCheckDeliver(t, bapp, sendMsg1, []int64{2}, true, priv2) + + // Check balances + CheckBalance(t, bapp, addr1, "57foocoin") + CheckBalance(t, bapp, addr2, "20foocoin") +} + func TestMsgSendWithAccounts(t *testing.T) { bapp := newBasecoinApp() diff --git a/examples/democoin/app/app_test.go b/examples/democoin/app/app_test.go index c67782d92..b0f188f10 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 7c84f9a1d..be1d77829 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 329eb250b..7f62234ea 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/server/init.go b/server/init.go index 2d8be85f8..68a1709bc 100644 --- a/server/init.go +++ b/server/init.go @@ -7,6 +7,7 @@ import ( "os" "path" "path/filepath" + "sort" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -210,16 +211,18 @@ func InitCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { func processGenTxs(genTxsDir string, cdc *wire.Codec, appInit AppInit) ( validators []tmtypes.GenesisValidator, appGenTxs []json.RawMessage, persistentPeers string, err error) { - // XXX sort the files by contents just incase people renamed their files var fos []os.FileInfo fos, err = ioutil.ReadDir(genTxsDir) if err != nil { return } + + genTxs := make(map[string]GenesisTx) + var nodeIDs []string for _, fo := range fos { filename := path.Join(genTxsDir, fo.Name()) if !fo.IsDir() && (path.Ext(filename) != ".json") { - return + continue } // get the genTx @@ -234,6 +237,15 @@ func processGenTxs(genTxsDir string, cdc *wire.Codec, appInit AppInit) ( return } + genTxs[genTx.NodeID] = genTx + nodeIDs = append(nodeIDs, genTx.NodeID) + } + + sort.Strings(nodeIDs) + + for _, nodeID := range nodeIDs { + genTx := genTxs[nodeID] + // combine some stuff validators = append(validators, genTx.Validator) appGenTxs = append(appGenTxs, genTx.AppGenTx) diff --git a/server/util.go b/server/util.go index 313d4fc5b..f44cc2d72 100644 --- a/server/util.go +++ b/server/util.go @@ -79,7 +79,6 @@ func AddCommands( ShowNodeIDCmd(ctx), ShowValidatorCmd(ctx), ExportCmd(ctx, cdc, appExport), - UnsafeResetAllCmd(ctx), version.VersionCmd, ) } diff --git a/store/cachemultistore.go b/store/cachemultistore.go index b1a754881..47878fb15 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 000000000..9f50f3444 --- /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 000000000..524dc5323 --- /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 217e8eb14..11cebc22e 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 40651163c..a46e797eb 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 e28523ebe..4ab0a5d09 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 059a0dd74..20d452464 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 000000000..49bfa27ec --- /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 f8367a126..abf02ec07 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/types/tx_msg.go b/types/tx_msg.go index c3a22b60f..e17d152a5 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -77,8 +77,8 @@ func FeePayer(tx Tx) Address { // gas to be used by the transaction. The ratio yields an effective "gasprice", // which must be above some miminum to be accepted into the mempool. type StdFee struct { - Amount Coins `json"amount"` - Gas int64 `json"gas"` + Amount Coins `json:"amount"` + Gas int64 `json:"gas"` } func NewStdFee(gas int64, amount ...Coin) StdFee { diff --git a/version/version.go b/version/version.go index d315bfea1..116f1ff28 100644 --- a/version/version.go +++ b/version/version.go @@ -6,10 +6,10 @@ package version // TODO improve const Maj = "0" -const Min = "16" +const Min = "18" const Fix = "0" -const Version = "0.16.0-dev" +const Version = "0.18.0-dev" // GitCommit set by build flags var GitCommit = "" diff --git a/x/auth/ante.go b/x/auth/ante.go index c7af7e2d9..248083206 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -8,10 +8,14 @@ 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. -func NewAnteHandler(accountMapper sdk.AccountMapper, feeHandler sdk.FeeHandler) sdk.AnteHandler { +func NewAnteHandler(am sdk.AccountMapper, feeHandler sdk.FeeHandler) sdk.AnteHandler { return func( ctx sdk.Context, tx sdk.Tx, ) (_ sdk.Context, _ sdk.Result, abort bool) { @@ -24,7 +28,6 @@ func NewAnteHandler(accountMapper sdk.AccountMapper, feeHandler sdk.FeeHandler) true } - // TODO: can tx just implement message? msg := tx.GetMsg() // TODO: will this always be a stdtx? should that be used in the function signature? @@ -62,7 +65,7 @@ func NewAnteHandler(accountMapper sdk.AccountMapper, feeHandler sdk.FeeHandler) // check signature, return account with incremented nonce signerAcc, res := processSig( - ctx, accountMapper, + ctx, am, signerAddr, sig, signBytes, ) if !res.IsOK() { @@ -82,13 +85,16 @@ func NewAnteHandler(accountMapper sdk.AccountMapper, feeHandler sdk.FeeHandler) } // Save the account. - accountMapper.SetAccount(ctx, signerAcc) + am.SetAccount(ctx, signerAcc) signerAccs[i] = signerAcc } // 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... @@ -135,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/auth/baseaccount.go b/x/auth/baseaccount.go index 6a612689d..ff907fc38 100644 --- a/x/auth/baseaccount.go +++ b/x/auth/baseaccount.go @@ -51,9 +51,6 @@ func (acc BaseAccount) GetPubKey() crypto.PubKey { // Implements sdk.Account. func (acc *BaseAccount) SetPubKey(pubKey crypto.PubKey) error { - if acc.PubKey != nil { - return errors.New("cannot override BaseAccount pubkey") - } acc.PubKey = pubKey return nil } diff --git a/x/auth/baseaccount_test.go b/x/auth/baseaccount_test.go index 8b69b6dfc..d3363e4fb 100644 --- a/x/auth/baseaccount_test.go +++ b/x/auth/baseaccount_test.go @@ -37,10 +37,10 @@ func TestBaseAccountAddressPubKey(t *testing.T) { assert.Nil(t, err) assert.Equal(t, pub1, acc.GetPubKey()) - // can't override pubkey + // can override pubkey err = acc.SetPubKey(pub2) - assert.NotNil(t, err) - assert.Equal(t, pub1, acc.GetPubKey()) + assert.Nil(t, err) + assert.Equal(t, pub2, acc.GetPubKey()) //------------------------------------ diff --git a/x/auth/handler.go b/x/auth/handler.go new file mode 100644 index 000000000..8a0e1061a --- /dev/null +++ b/x/auth/handler.go @@ -0,0 +1,34 @@ +package auth + +import ( + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewHandler returns a handler for "auth" type messages. +func NewHandler(am AccountMapper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MsgChangeKey: + return handleMsgChangeKey(ctx, am, msg) + default: + errMsg := "Unrecognized auth Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// Handle MsgChangeKey +// Should be very expensive, because once this happens, an account is un-prunable +func handleMsgChangeKey(ctx sdk.Context, am AccountMapper, msg MsgChangeKey) sdk.Result { + + err := am.setPubKey(ctx, msg.Address, msg.NewPubKey) + if err != nil { + return err.Result() + } + + return sdk.Result{ + Tags: sdk.NewTags("action", []byte("changePubkey"), "address", msg.Address.Bytes(), "pubkey", msg.NewPubKey.Bytes()), + } +} diff --git a/x/auth/mapper.go b/x/auth/mapper.go index f9e202c8a..3666f13b6 100644 --- a/x/auth/mapper.go +++ b/x/auth/mapper.go @@ -6,14 +6,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" wire "github.com/cosmos/cosmos-sdk/wire" + crypto "github.com/tendermint/go-crypto" ) -var _ sdk.AccountMapper = (*accountMapper)(nil) +var _ sdk.AccountMapper = (*AccountMapper)(nil) // Implements sdk.AccountMapper. // This AccountMapper encodes/decodes accounts using the // go-amino (binary) encoding/decoding library. -type accountMapper struct { +type AccountMapper struct { // The (unexposed) key used to access the store from the Context. key sdk.StoreKey @@ -28,23 +29,23 @@ type accountMapper struct { // NewAccountMapper returns a new sdk.AccountMapper that // uses go-amino to (binary) encode and decode concrete sdk.Accounts. // nolint -func NewAccountMapper(cdc *wire.Codec, key sdk.StoreKey, proto sdk.Account) accountMapper { - return accountMapper{ +func NewAccountMapper(cdc *wire.Codec, key sdk.StoreKey, proto sdk.Account) AccountMapper { + return AccountMapper{ key: key, proto: proto, cdc: cdc, } } -// Implements sdk.AccountMapper. -func (am accountMapper) NewAccountWithAddress(ctx sdk.Context, addr sdk.Address) sdk.Account { +// Implaements sdk.AccountMapper. +func (am AccountMapper) NewAccountWithAddress(ctx sdk.Context, addr sdk.Address) sdk.Account { acc := am.clonePrototype() acc.SetAddress(addr) return acc } // Implements sdk.AccountMapper. -func (am accountMapper) GetAccount(ctx sdk.Context, addr sdk.Address) sdk.Account { +func (am AccountMapper) GetAccount(ctx sdk.Context, addr sdk.Address) sdk.Account { store := ctx.KVStore(am.key) bz := store.Get(addr) if bz == nil { @@ -55,7 +56,7 @@ func (am accountMapper) GetAccount(ctx sdk.Context, addr sdk.Address) sdk.Accoun } // Implements sdk.AccountMapper. -func (am accountMapper) SetAccount(ctx sdk.Context, acc sdk.Account) { +func (am AccountMapper) SetAccount(ctx sdk.Context, acc sdk.Account) { addr := acc.GetAddress() store := ctx.KVStore(am.key) bz := am.encodeAccount(acc) @@ -63,7 +64,7 @@ func (am accountMapper) SetAccount(ctx sdk.Context, acc sdk.Account) { } // Implements sdk.AccountMapper. -func (am accountMapper) IterateAccounts(ctx sdk.Context, process func(sdk.Account) (stop bool)) { +func (am AccountMapper) IterateAccounts(ctx sdk.Context, process func(sdk.Account) (stop bool)) { store := ctx.KVStore(am.key) iter := store.Iterator(nil, nil) for { @@ -79,11 +80,49 @@ func (am accountMapper) IterateAccounts(ctx sdk.Context, process func(sdk.Accoun } } +// Returns the PubKey of the account at address +func (am AccountMapper) GetPubKey(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, sdk.Error) { + acc := am.GetAccount(ctx, addr) + if acc == nil { + return nil, sdk.ErrUnknownAddress(addr.String()) + } + return acc.GetPubKey(), nil +} + +func (am AccountMapper) setPubKey(ctx sdk.Context, addr sdk.Address, newPubKey crypto.PubKey) sdk.Error { + acc := am.GetAccount(ctx, addr) + if acc == nil { + return sdk.ErrUnknownAddress(addr.String()) + } + acc.SetPubKey(newPubKey) + am.SetAccount(ctx, acc) + return nil +} + +// Returns the Sequence of the account at address +func (am AccountMapper) GetSequence(ctx sdk.Context, addr sdk.Address) (int64, sdk.Error) { + acc := am.GetAccount(ctx, addr) + if acc == nil { + return 0, sdk.ErrUnknownAddress(addr.String()) + } + return acc.GetSequence(), nil +} + +func (am AccountMapper) setSequence(ctx sdk.Context, addr sdk.Address, newSequence int64) sdk.Error { + acc := am.GetAccount(ctx, addr) + if acc == nil { + return sdk.ErrUnknownAddress(addr.String()) + } + acc.SetSequence(newSequence) + am.SetAccount(ctx, acc) + return nil +} + //---------------------------------------- // misc. // Creates a new struct (or pointer to struct) from am.proto. -func (am accountMapper) clonePrototype() sdk.Account { +func (am AccountMapper) clonePrototype() sdk.Account { protoRt := reflect.TypeOf(am.proto) if protoRt.Kind() == reflect.Ptr { protoCrt := protoRt.Elem() @@ -106,7 +145,7 @@ func (am accountMapper) clonePrototype() sdk.Account { return clone } -func (am accountMapper) encodeAccount(acc sdk.Account) []byte { +func (am AccountMapper) encodeAccount(acc sdk.Account) []byte { bz, err := am.cdc.MarshalBinaryBare(acc) if err != nil { panic(err) @@ -114,7 +153,7 @@ func (am accountMapper) encodeAccount(acc sdk.Account) []byte { return bz } -func (am accountMapper) decodeAccount(bz []byte) (acc sdk.Account) { +func (am AccountMapper) decodeAccount(bz []byte) (acc sdk.Account) { err := am.cdc.UnmarshalBinaryBare(bz, &acc) if err != nil { panic(err) diff --git a/x/auth/msgs.go b/x/auth/msgs.go new file mode 100644 index 000000000..545b296e5 --- /dev/null +++ b/x/auth/msgs.go @@ -0,0 +1,44 @@ +package auth + +import ( + "encoding/json" + + "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MsgChangeKey - high level transaction of the auth module +type MsgChangeKey struct { + Address sdk.Address `json:"address"` + NewPubKey crypto.PubKey `json:"public_key"` +} + +var _ sdk.Msg = MsgChangeKey{} + +// NewMsgChangeKey - msg to claim an account and set the PubKey +func NewMsgChangeKey(addr sdk.Address, pubkey crypto.PubKey) MsgChangeKey { + return MsgChangeKey{Address: addr, NewPubKey: pubkey} +} + +// Implements Msg. +func (msg MsgChangeKey) Type() string { return "auth" } + +// Implements Msg. +func (msg MsgChangeKey) ValidateBasic() sdk.Error { + return nil +} + +// Implements Msg. +func (msg MsgChangeKey) GetSignBytes() []byte { + b, err := json.Marshal(msg) // XXX: ensure some canonical form + if err != nil { + panic(err) + } + return b +} + +// Implements Msg. +func (msg MsgChangeKey) GetSigners() []sdk.Address { + return []sdk.Address{msg.Address} +} diff --git a/x/auth/msgs_test.go b/x/auth/msgs_test.go new file mode 100644 index 000000000..30c98b073 --- /dev/null +++ b/x/auth/msgs_test.go @@ -0,0 +1,47 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestNewMsgChangeKey(t *testing.T) {} + +func TestMsgChangeKeyType(t *testing.T) { + addr1 := sdk.Address([]byte("input")) + newPubKey := crypto.GenPrivKeyEd25519().PubKey() + + var msg = MsgChangeKey{ + Address: addr1, + NewPubKey: newPubKey, + } + + assert.Equal(t, msg.Type(), "auth") +} + +func TestMsgChangeKeyValidation(t *testing.T) { + + addr1 := sdk.Address([]byte("input")) + + // emptyPubKey := crypto.PubKeyEd25519{} + // var msg = MsgChangeKey{ + // Address: addr1, + // NewPubKey: emptyPubKey, + // } + + // // fmt.Println(msg.NewPubKey.Empty()) + // fmt.Println(msg.NewPubKey.Bytes()) + + // assert.NotNil(t, msg.ValidateBasic()) + + newPubKey := crypto.GenPrivKeyEd25519().PubKey() + msg := MsgChangeKey{ + Address: addr1, + NewPubKey: newPubKey, + } + assert.Nil(t, msg.ValidateBasic()) +} diff --git a/x/bank/keeper.go b/x/bank/keeper.go index d23167c3c..6ef73c68b 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 3db16c5f9..117c69e7a 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}}) diff --git a/x/bank/msgs.go b/x/bank/msgs.go index f37f720f1..de7f2a8b1 100644 --- a/x/bank/msgs.go +++ b/x/bank/msgs.go @@ -119,7 +119,7 @@ func (msg MsgIssue) GetSigners() []sdk.Address { //---------------------------------------- // Input -// Transaction Output +// Transaction Input type Input struct { Address sdk.Address `json:"address"` Coins sdk.Coins `json:"coins"`