diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0853dc0..b3db2c75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 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.2 *May 20, 2018* @@ -52,6 +65,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: @@ -64,7 +78,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/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/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/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/version/version.go b/version/version.go index ea365bda9..116f1ff28 100644 --- a/version/version.go +++ b/version/version.go @@ -6,10 +6,10 @@ package version // TODO improve const Maj = "0" -const Min = "17" -const Fix = "2" +const Min = "18" +const Fix = "0" -const Version = "0.17.2" +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}})