diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000000..6754ebc05bc3 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,103 @@ +version: 2 + +defaults: &defaults + working_directory: /go/src/github.com/cosmos/cosmos-sdk + docker: + - image: circleci/golang:1.10.0 + environment: + GOBIN: /tmp/workspace/bin + +jobs: + setup_dependencies: + <<: *defaults + steps: + - run: mkdir -p /tmp/workspace/bin + - run: mkdir -p /tmp/workspace/profiles + - checkout + - restore_cache: + keys: + - v1-pkg-cache + - run: + name: tools + command: | + export PATH="$GOBIN:$PATH" + make get_tools + - run: + name: dependencies + command: | + export PATH="$GOBIN:$PATH" + make get_vendor_deps + - run: + name: binaries + command: | + export PATH="$GOBIN:$PATH" + make build + - persist_to_workspace: + root: /tmp/workspace + paths: + - bin + - profiles + - save_cache: + key: v1-pkg-cache + paths: + - /go/pkg + - save_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + paths: + - /go/src/github.com/cosmos/cosmos-sdk + + test_cover: + <<: *defaults + parallelism: 4 + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Run tests + command: | + for pkg in $(go list github.com/cosmos/cosmos-sdk/... | grep -v /vendor/ | circleci tests split --split-by=timings); do + id=$(basename "$pkg") + + go test -timeout 5m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" + done + - persist_to_workspace: + root: /tmp/workspace + paths: + - "profiles/*" + + upload_coverage: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: gather + command: | + set -ex + + echo "mode: atomic" > coverage.txt + for prof in $(ls /tmp/workspace/profiles/); do + tail -n +2 /tmp/workspace/profiles/"$prof" >> coverage.txt + done + - run: + name: upload + command: bash <(curl -s https://codecov.io/bash) -f coverage.txt + +workflows: + version: 2 + test-suite: + jobs: + - setup_dependencies + - test_cover: + requires: + - setup_dependencies + - upload_coverage: + requires: + - test_cover + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..902469b41cc2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ + + +* [ ] Updated all relevant documentation in docs +* [ ] Updated all code comments where relevant +* [ ] Wrote tests +* [ ] Updated CHANGELOG.md diff --git a/.gitignore b/.gitignore index c97bf48ddadc..707ded5508b5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,13 @@ examples/basecoin/glide.lock examples/basecoin/app/data baseapp/data/* docs/_build +.DS_Store +coverage.txt +profile.out +.vscode +coverage.txt +profile.out +client/lcd/keys.db/ ### Vagrant ### .vagrant/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f71b5cc703d..01d342ef6435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## 0.12.0 (March 27 2018) + +BREAKING CHANGES + +* Revert to old go-wire for now +* glide -> godep +* [types] ErrBadNonce -> ErrInvalidSequence +* [types] Replace tx.GetFeePayer with FeePayer(tx) - returns the first signer +* [types] NewStdTx takes the Fee +* [types] ParseAccount -> AccountDecoder; ErrTxParse -> ErrTxDecoder +* [auth] AnteHandler deducts fees +* [bank] Move some errors to `types` +* [bank] Remove sequence and signature from Input + +FEATURES + +* [examples/basecoin] New cool module to demonstrate use of state and custom transactions +* [basecoind] `show_node_id` command +* [lcd] Implement the Light Client Daemon and endpoints +* [types/stdlib] Queue functionality +* [store] Subspace iterator on IAVLTree +* [types] StdSignDoc is the document that gets signed (chainid, msg, sequence, fee) +* [types] CodeInvalidPubKey +* [types] StdFee, and StdTx takes the StdFee +* [specs] Progression of MVPs for IBC +* [x/ibc] Initial shell of IBC functionality (no proofs) +* [x/staking] Simple staking module with bonding/unbonding + +IMPROVEMENTS + +* Lots more tests! +* [client/builder] Helpers for forming and signing transactions +* [types] sdk.Address +* [specs] Staking + +BUG FIXES +* [auth] Fix setting pubkey on new account +* [auth] Require signatures to include the sequences +* [baseapp] Dont panic on nil handler +* [basecoin] Check for empty bytes in account and tx + ## 0.11.0 (March 1, 2017) BREAKING CHANGES diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 000000000000..46cb95893f9e --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,463 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/bgentry/speakeasy" + packages = ["."] + revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" + version = "v0.1.0" + +[[projects]] + branch = "master" + name = "github.com/btcsuite/btcd" + packages = ["btcec"] + revision = "2be2f12b358dc57d70b8f501b00be450192efbc3" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/ebuchman/fail-test" + packages = ["."] + revision = "95f809107225be108efcf10a3509e4ea6ceef3c4" + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + name = "github.com/go-kit/kit" + packages = [ + "log", + "log/level", + "log/term" + ] + revision = "4dc7be5d2d12881735283bcab7352178e190fc71" + version = "v0.6.0" + +[[projects]] + name = "github.com/go-logfmt/logfmt" + packages = ["."] + revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" + version = "v0.3.0" + +[[projects]] + name = "github.com/go-stack/stack" + packages = ["."] + revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" + version = "v1.7.0" + +[[projects]] + name = "github.com/gogo/protobuf" + packages = [ + "gogoproto", + "jsonpb", + "proto", + "protoc-gen-gogo/descriptor", + "sortkeys", + "types" + ] + revision = "1adfc126b41513cc696b209667c8656ea7aac67c" + version = "v1.0.0" + +[[projects]] + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp" + ] + revision = "925541529c1fa6821df4e44ce2723319eb2be768" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/golang/snappy" + packages = ["."] + revision = "553a641470496b2327abcac10b36396bd98e45c9" + +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" + version = "v1.1" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "53c1911da2b537f792e7cafcb446b05ffe33b996" + version = "v1.6.1" + +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" + +[[projects]] + branch = "master" + name = "github.com/howeyc/crc16" + packages = ["."] + revision = "2b2a61e366a66d3efb279e46176e7291001e0354" + +[[projects]] + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + +[[projects]] + branch = "master" + name = "github.com/jmhodges/levigo" + packages = ["."] + revision = "c42d9e0ca023e2198120196f842701bb4c55d7b9" + +[[projects]] + branch = "master" + name = "github.com/kr/logfmt" + packages = ["."] + revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" + +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" + +[[projects]] + name = "github.com/mattn/go-isatty" + packages = ["."] + revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" + version = "v0.0.3" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/rcrowley/go-metrics" + packages = ["."] + revision = "8732c616f52954686704c8645fe1a9d59e9df7c1" + +[[projects]] + name = "github.com/spf13/afero" + packages = [ + ".", + "mem" + ] + revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" + version = "v1.0.2" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + +[[projects]] + name = "github.com/spf13/cobra" + packages = ["."] + revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" + version = "v0.0.1" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" + version = "v1.0.0" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" + +[[projects]] + name = "github.com/stretchr/testify" + packages = [ + "assert", + "require" + ] + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" + +[[projects]] + branch = "master" + name = "github.com/syndtr/goleveldb" + packages = [ + "leveldb", + "leveldb/cache", + "leveldb/comparer", + "leveldb/errors", + "leveldb/filter", + "leveldb/iterator", + "leveldb/journal", + "leveldb/memdb", + "leveldb/opt", + "leveldb/storage", + "leveldb/table", + "leveldb/util" + ] + revision = "169b1b37be738edb2813dab48c97a549bcf99bb5" + +[[projects]] + name = "github.com/tendermint/abci" + packages = [ + "client", + "example/code", + "example/kvstore", + "server", + "types" + ] + revision = "46686763ba8ea595ede16530ed4a40fb38f49f94" + version = "v0.10.2" + +[[projects]] + branch = "master" + name = "github.com/tendermint/ed25519" + packages = [ + ".", + "edwards25519", + "extra25519" + ] + revision = "d8387025d2b9d158cf4efb07e7ebf814bcce2057" + +[[projects]] + name = "github.com/tendermint/go-crypto" + packages = [ + ".", + "keys", + "keys/bcrypt", + "keys/words", + "keys/words/wordlist" + ] + revision = "c3e19f3ea26f5c3357e0bcbb799b0761ef923755" + version = "v0.5.0" + +[[projects]] + name = "github.com/tendermint/go-wire" + packages = [ + ".", + "data" + ] + revision = "fa721242b042ecd4c6ed1a934ee740db4f74e45c" + source = "github.com/tendermint/go-amino" + version = "v0.7.3" + +[[projects]] + name = "github.com/tendermint/iavl" + packages = ["."] + revision = "fd37a0fa3a7454423233bc3d5ea828f38e0af787" + version = "v0.7.0" + +[[projects]] + name = "github.com/tendermint/tendermint" + packages = [ + "blockchain", + "cmd/tendermint/commands", + "config", + "consensus", + "consensus/types", + "evidence", + "lite", + "lite/client", + "lite/errors", + "lite/files", + "lite/proxy", + "mempool", + "node", + "p2p", + "p2p/conn", + "p2p/pex", + "p2p/trust", + "p2p/upnp", + "proxy", + "rpc/client", + "rpc/core", + "rpc/core/types", + "rpc/grpc", + "rpc/lib", + "rpc/lib/client", + "rpc/lib/server", + "rpc/lib/types", + "state", + "state/txindex", + "state/txindex/kv", + "state/txindex/null", + "types", + "types/priv_validator", + "version", + "wire" + ] + revision = "6f9956990c444d53f62f2a3905ed410cfe9afe77" + version = "v0.17.1" + +[[projects]] + name = "github.com/tendermint/tmlibs" + packages = [ + "autofile", + "cli", + "cli/flags", + "clist", + "common", + "db", + "flowrate", + "log", + "merkle", + "pubsub", + "pubsub/query" + ] + revision = "24da7009c3d8c019b40ba4287495749e3160caca" + version = "v0.7.1" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "blowfish", + "curve25519", + "nacl/box", + "nacl/secretbox", + "openpgp/armor", + "openpgp/errors", + "poly1305", + "ripemd160", + "salsa20/salsa" + ] + revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "context", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "lex/httplex", + "trace" + ] + revision = "6078986fec03a1dcc236c34816c71b0e05018fda" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix"] + revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + revision = "ab0870e398d5dd054b868c0db1481ab029b9a9f2" + +[[projects]] + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "codes", + "connectivity", + "credentials", + "grpclb/grpc_lb_v1/messages", + "grpclog", + "internal", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "stats", + "status", + "tap", + "transport" + ] + revision = "5b3c4e850e90a4cf6a20ebd46c8b32a0a3afcb9e" + version = "v1.7.5" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb" + version = "v2.2.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "0eb39694057c8ab8c9ecbaeb25bc43cbf1d2422976a09a67392a62dcef149a7b" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 000000000000..e3df3d6946c7 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,83 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + +[[constraint]] + name = "github.com/bgentry/speakeasy" + version = "~0.1.0" + +[[constraint]] + name = "github.com/golang/protobuf" + version = "~1.0.0" + +[[constraint]] + name = "github.com/mattn/go-isatty" + version = "~0.0.3" + +[[constraint]] + name = "github.com/pkg/errors" + version = "~0.8.0" + +[[constraint]] + name = "github.com/spf13/cobra" + version = "~0.0.1" + +[[constraint]] + name = "github.com/spf13/viper" + version = "~1.0.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "~1.2.1" + +[[constraint]] + version = "~0.10.2" + name = "github.com/tendermint/abci" + +[[constraint]] + version = "~0.5.0" + name = "github.com/tendermint/go-crypto" + +[[constraint]] + version = "~0.7.3" + source = "github.com/tendermint/go-amino" + name = "github.com/tendermint/go-wire" + +[[constraint]] + version = "~0.7.0" + name = "github.com/tendermint/iavl" + +[[constraint]] + version = "~0.17.1" + name = "github.com/tendermint/tendermint" + +[[constraint]] + version = "~0.7.1" + name = "github.com/tendermint/tmlibs" + +[prune] + go-tests = true + unused-packages = true + diff --git a/Makefile b/Makefile index 395af306ac35..447d051c02d7 100644 --- a/Makefile +++ b/Makefile @@ -29,18 +29,18 @@ dist: ### Tools & dependencies check_tools: - cd tools && $(MAKE) check + cd tools && $(MAKE) check_tools update_tools: - cd tools && $(MAKE) glide_update + cd tools && $(MAKE) update_tools get_tools: - cd tools && $(MAKE) + cd tools && $(MAKE) get_tools get_vendor_deps: @rm -rf vendor/ - @echo "--> Running glide install" - @glide install + @echo "--> Running dep ensure" + @dep ensure -v draw_deps: @# requires brew install graphviz or apt-get install graphviz @@ -72,7 +72,9 @@ test_unit: test_cover: @rm -rf examples/basecoin/vendor/ + @rm -rf client/lcd/keys.db ~/.tendermint_test @bash tests/test_cover.sh + @rm -rf client/lcd/keys.db ~/.tendermint_test benchmark: @go test -bench=. $(PACKAGES) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index c6ca46e0f830..3dad0483a304 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -371,6 +371,13 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk } } + // Match route. + msgType := msg.Type() + handler := app.router.Route(msgType) + if handler == nil { + return sdk.ErrUnknownRequest("Unrecognized Msg type: " + msgType).Result() + } + // Get the correct cache var msCache sdk.CacheMultiStore if isCheckTx == true { @@ -384,9 +391,6 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk } - // Match and run route. - msgType := msg.Type() - handler := app.router.Route(msgType) result = handler(ctx, msg) // If result was successful, write to app.checkState.ms or app.deliverState.ms diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index 8114ef93caee..de9a0253c519 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -327,8 +327,7 @@ func (tx testUpdatePowerTx) Get(key interface{}) (value interface{}) { return ni func (tx testUpdatePowerTx) GetMsg() sdk.Msg { return tx } func (tx testUpdatePowerTx) GetSignBytes() []byte { return nil } func (tx testUpdatePowerTx) ValidateBasic() sdk.Error { return nil } -func (tx testUpdatePowerTx) GetSigners() []crypto.Address { return nil } -func (tx testUpdatePowerTx) GetFeePayer() crypto.Address { return nil } +func (tx testUpdatePowerTx) GetSigners() []sdk.Address { return nil } func (tx testUpdatePowerTx) GetSignatures() []sdk.StdSignature { return nil } func TestValidatorChange(t *testing.T) { @@ -430,7 +429,7 @@ func makePubKey(secret string) crypto.PubKey { func makePrivKey(secret string) crypto.PrivKey { privKey := crypto.GenPrivKeyEd25519FromSecret([]byte(secret)) - return privKey + return privKey.Wrap() } func secret(index int) string { diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c13494bf6875..000000000000 --- a/circle.yml +++ /dev/null @@ -1,23 +0,0 @@ -machine: - environment: - GOPATH: "$HOME/.go_workspace" - PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" - REPO: "$PROJECT_PARENT_PATH/$CIRCLE_PROJECT_REPONAME" - PATH: "$GOPATH/bin:$PATH" - hosts: - circlehost: 127.0.0.1 - localhost: 127.0.0.1 - -dependencies: - override: - - go version - - mkdir -p "$PROJECT_PARENT_PATH" - - ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$REPO" - - env - -test: - override: - - "cd $REPO && make ci" - - ls $GOPATH/bin - - bash <(curl -s https://codecov.io/bash) -f coverage.txt - diff --git a/client/builder/builder.go b/client/builder/builder.go new file mode 100644 index 000000000000..2fb1c824e811 --- /dev/null +++ b/client/builder/builder.go @@ -0,0 +1,134 @@ +package builder + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/wire" + rpcclient "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Broadcast the transaction bytes to Tendermint +func BroadcastTx(tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { + + node, err := client.GetNode() + if err != nil { + return nil, err + } + + res, err := node.BroadcastTxCommit(tx) + if err != nil { + return res, err + } + + if res.CheckTx.Code != uint32(0) { + return res, errors.Errorf("CheckTx failed: (%d) %s", + res.CheckTx.Code, + res.CheckTx.Log) + } + if res.DeliverTx.Code != uint32(0) { + return res, errors.Errorf("DeliverTx failed: (%d) %s", + res.DeliverTx.Code, + res.DeliverTx.Log) + } + return res, err +} + +// Query from Tendermint with the provided key and storename +func Query(key cmn.HexBytes, storeName string) (res []byte, err error) { + + path := fmt.Sprintf("/%s/key", storeName) + node, err := client.GetNode() + if err != nil { + return res, err + } + + opts := rpcclient.ABCIQueryOptions{ + Height: viper.GetInt64(client.FlagHeight), + Trusted: viper.GetBool(client.FlagTrustNode), + } + result, err := node.ABCIQueryWithOptions(path, key, opts) + if err != nil { + return res, err + } + resp := result.Response + if resp.Code != uint32(0) { + return res, errors.Errorf("Query failed: (%d) %s", resp.Code, resp.Log) + } + return resp.Value, nil +} + +// Get the from address from the name flag +func GetFromAddress() (from sdk.Address, err error) { + + keybase, err := keys.GetKeyBase() + if err != nil { + return nil, err + } + + name := viper.GetString(client.FlagName) + if name == "" { + return nil, errors.Errorf("must provide a name using --name") + } + + info, err := keybase.Get(name) + if err != nil { + return nil, errors.Errorf("No key for: %s", name) + } + + return info.PubKey.Address(), nil +} + +// sign and build the transaction from the msg +func SignAndBuild(name, passphrase string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { + + // build the Sign Messsage from the Standard Message + chainID := viper.GetString(client.FlagChainID) + sequence := int64(viper.GetInt(client.FlagSequence)) + signMsg := sdk.StdSignMsg{ + ChainID: chainID, + Sequences: []int64{sequence}, + Msg: msg, + } + + keybase, err := keys.GetKeyBase() + if err != nil { + return nil, err + } + + // sign and build + bz := signMsg.Bytes() + + sig, pubkey, err := keybase.Sign(name, passphrase, bz) + if err != nil { + return nil, err + } + sigs := []sdk.StdSignature{{ + PubKey: pubkey, + Signature: sig, + Sequence: viper.GetInt64(client.FlagSequence), + }} + + // marshal bytes + tx := sdk.NewStdTx(signMsg.Msg, signMsg.Fee, sigs) + + return cdc.MarshalBinary(tx) +} + +// sign and build the transaction from the msg +func SignBuildBroadcast(name string, passphrase string, msg sdk.Msg, cdc *wire.Codec) (*ctypes.ResultBroadcastTxCommit, error) { + txBytes, err := SignAndBuild(name, passphrase, msg, cdc) + if err != nil { + return nil, err + } + + return BroadcastTx(txBytes) +} diff --git a/client/flags.go b/client/flags.go index 843cb52d1e93..ceaf5a3a9dde 100644 --- a/client/flags.go +++ b/client/flags.go @@ -9,6 +9,8 @@ const ( FlagHeight = "height" FlagTrustNode = "trust-node" FlagName = "name" + FlagSequence = "sequence" + FlagFee = "fee" ) // LineBreak can be included in a command list to provide a blank line @@ -31,6 +33,8 @@ func GetCommands(cmds ...*cobra.Command) []*cobra.Command { func PostCommands(cmds ...*cobra.Command) []*cobra.Command { for _, c := range cmds { c.Flags().String(FlagName, "", "Name of private key with which to sign") + c.Flags().Int64(FlagSequence, 0, "Sequence number to sign the tx") + c.Flags().String(FlagFee, "", "Fee to pay along with transaction") c.Flags().String(FlagChainID, "", "Chain ID of tendermint node") c.Flags().String(FlagNode, "tcp://localhost:46657", ": to tendermint rpc interface for this chain") } diff --git a/client/helpers.go b/client/helpers.go index 10ffcc88e639..f383b95f7ddc 100644 --- a/client/helpers.go +++ b/client/helpers.go @@ -1,14 +1,10 @@ package client import ( - "fmt" - "github.com/pkg/errors" "github.com/spf13/viper" rpcclient "github.com/tendermint/tendermint/rpc/client" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - cmn "github.com/tendermint/tmlibs/common" ) // GetNode prepares a simple rpc.Client from the flags @@ -19,53 +15,3 @@ func GetNode() (rpcclient.Client, error) { } return rpcclient.NewHTTP(uri, "/websocket"), nil } - -// Broadcast the transaction bytes to Tendermint -func BroadcastTx(tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { - - node, err := GetNode() - if err != nil { - return nil, err - } - - res, err := node.BroadcastTxCommit(tx) - if err != nil { - return res, err - } - - if res.CheckTx.Code != uint32(0) { - return res, errors.Errorf("CheckTx failed: (%d) %s", - res.CheckTx.Code, - res.CheckTx.Log) - } - if res.DeliverTx.Code != uint32(0) { - return res, errors.Errorf("DeliverTx failed: (%d) %s", - res.DeliverTx.Code, - res.DeliverTx.Log) - } - return res, err -} - -// Query from Tendermint with the provided key and storename -func Query(key cmn.HexBytes, storeName string) (res []byte, err error) { - - path := fmt.Sprintf("/%s/key", storeName) - node, err := GetNode() - if err != nil { - return res, err - } - - opts := rpcclient.ABCIQueryOptions{ - Height: viper.GetInt64(FlagHeight), - Trusted: viper.GetBool(FlagTrustNode), - } - result, err := node.ABCIQueryWithOptions(path, key, opts) - if err != nil { - return res, err - } - resp := result.Response - if resp.Code != uint32(0) { - return res, errors.Errorf("Query failed: (%d) %s", resp.Code, resp.Log) - } - return resp.Value, nil -} diff --git a/client/keys.go b/client/keys.go index 42eb00b7eb98..47eb0b9c957c 100644 --- a/client/keys.go +++ b/client/keys.go @@ -6,7 +6,8 @@ import ( dbm "github.com/tendermint/tmlibs/db" ) -// GetKeyBase initializes a keybase based on the configuration +// GetKeyBase initializes a keybase based on the given db. +// The KeyBase manages all activity requiring access to a key. func GetKeyBase(db dbm.DB) keys.Keybase { keybase := keys.New( db, diff --git a/client/keys/add.go b/client/keys/add.go index 7472dce27f9e..356c7369c4a2 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -1,9 +1,13 @@ package keys import ( + "encoding/json" "fmt" + "io/ioutil" + "net/http" "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -120,3 +124,89 @@ func printCreate(info keys.Info, seed string) { panic(fmt.Sprintf("I can't speak: %s", output)) } } + +// REST + +type NewKeyBody struct { + Name string `json:"name"` + Password string `json:"password"` + Seed string `json:"seed"` +} + +func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { + var kb keys.Keybase + var m NewKeyBody + + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + body, err := ioutil.ReadAll(r.Body) + err = json.Unmarshal(body, &m) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + if m.Name == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a name for the locally stored account.")) + return + } + if m.Password == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a password for the locally stored account.")) + return + } + if m.Seed == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a seed for the locally stored account.")) + return + } + + // check if already exists + infos, err := kb.List() + for _, i := range infos { + if i.Name == m.Name { + w.WriteHeader(http.StatusConflict) + w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name))) + return + } + } + + // create account + info, err := kb.Recover(m.Name, m.Password, m.Seed) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + w.Write([]byte(info.PubKey.Address().String())) +} + +// function to just a new seed to display in the UI before actually persisting it in the keybase +func getSeed(algo keys.CryptoAlgo) string { + kb := client.MockKeyBase() + pass := "throwing-this-key-away" + name := "inmemorykey" + + _, seed, _ := kb.Create(name, pass, algo) + return seed +} + +func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + algoType := vars["type"] + // algo type defaults to ed25519 + if algoType == "" { + algoType = "ed25519" + } + algo := keys.CryptoAlgo(algoType) + + seed := getSeed(algo) + w.Write([]byte(seed)) +} diff --git a/client/keys/delete.go b/client/keys/delete.go index 65a9513272ca..b0327771b18a 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -1,10 +1,14 @@ package keys import ( + "encoding/json" "fmt" + "net/http" "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" "github.com/spf13/cobra" ) @@ -43,3 +47,41 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { fmt.Println("Password deleted forever (uh oh!)") return nil } + +// REST + +type DeleteKeyBody struct { + Password string `json:"password"` +} + +func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + var kb keys.Keybase + var m DeleteKeyBody + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&m) + if err != nil { + w.WriteHeader(400) + w.Write([]byte(err.Error())) + return + } + + kb, err = GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + // TODO handle error if key is not available or pass is wrong + err = kb.Delete(name, m.Password) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(200) +} diff --git a/client/keys/list.go b/client/keys/list.go index 14c4408781d0..97ccf8a90ca5 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -1,6 +1,13 @@ package keys -import "github.com/spf13/cobra" +import ( + "encoding/json" + "net/http" + + "github.com/spf13/cobra" +) + +// CMD // listKeysCmd represents the list command var listKeysCmd = &cobra.Command{ @@ -23,3 +30,36 @@ func runListCmd(cmd *cobra.Command, args []string) error { } return err } + +//REST + +func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) { + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + infos, err := kb.List() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + // an empty list will be JSONized as null, but we want to keep the empty list + if len(infos) == 0 { + w.Write([]byte("[]")) + return + } + keysOutput := make([]KeyOutput, len(infos)) + for i, info := range infos { + keysOutput[i] = KeyOutput{Name: info.Name, Address: info.PubKey.Address().String()} + } + output, err := json.MarshalIndent(keysOutput, "", " ") + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) +} diff --git a/client/keys/root.go b/client/keys/root.go index 962986c91a64..48b50d5e9787 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -2,6 +2,7 @@ package keys import ( "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" "github.com/spf13/cobra" ) @@ -27,3 +28,12 @@ func Commands() *cobra.Command { ) return cmd } + +func RegisterRoutes(r *mux.Router) { + r.HandleFunc("/keys", QueryKeysRequestHandler).Methods("GET") + r.HandleFunc("/keys", AddNewKeyRequestHandler).Methods("POST") + r.HandleFunc("/keys/seed", SeedRequestHandler).Methods("GET") + r.HandleFunc("/keys/{name}", GetKeyRequestHandler).Methods("GET") + r.HandleFunc("/keys/{name}", UpdateKeyRequestHandler).Methods("PUT") + r.HandleFunc("/keys/{name}", DeleteKeyRequestHandler).Methods("DELETE") +} diff --git a/client/keys/show.go b/client/keys/show.go index a22cb4bc887d..bb60b5bc0bbe 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -1,7 +1,12 @@ package keys import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" "github.com/spf13/cobra" ) @@ -13,20 +18,51 @@ var showKeysCmd = &cobra.Command{ RunE: runShowCmd, } +func getKey(name string) (keys.Info, error) { + kb, err := GetKeyBase() + if err != nil { + return keys.Info{}, err + } + + return kb.Get(name) +} + +// CMD + func runShowCmd(cmd *cobra.Command, args []string) error { if len(args) != 1 || len(args[0]) == 0 { return errors.New("You must provide a name for the key") } name := args[0] - kb, err := GetKeyBase() - if err != nil { - return err - } - - info, err := kb.Get(name) + info, err := getKey(name) if err == nil { printInfo(info) } return err } + +// REST + +func GetKeyRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + + info, err := getKey(name) + // TODO check for the error if key actually does not exist, instead of assuming this as the reason + if err != nil { + w.WriteHeader(404) + w.Write([]byte(err.Error())) + return + } + + keyOutput := KeyOutput{Name: info.Name, Address: info.PubKey.Address().String()} + output, err := json.MarshalIndent(keyOutput, "", " ") + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + w.Write(output) +} diff --git a/client/keys/update.go b/client/keys/update.go index 0e0f881c6e24..d95be78bfaa2 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -1,10 +1,14 @@ package keys import ( + "encoding/json" "fmt" + "net/http" "github.com/cosmos/cosmos-sdk/client" + "github.com/gorilla/mux" "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" "github.com/spf13/cobra" ) @@ -48,3 +52,42 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { fmt.Println("Password successfully updated!") return nil } + +// REST + +type UpdateKeyBody struct { + NewPassword string `json:"new_password"` + OldPassword string `json:"old_password"` +} + +func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + var kb keys.Keybase + var m UpdateKeyBody + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&m) + if err != nil { + w.WriteHeader(400) + w.Write([]byte(err.Error())) + return + } + + kb, err = GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + // TODO check if account exists and if password is correct + err = kb.Update(name, m.OldPassword, m.NewPassword) + if err != nil { + w.WriteHeader(401) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(200) +} diff --git a/client/keys/utils.go b/client/keys/utils.go index 19e63d74827e..c6239002ca78 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -20,6 +20,14 @@ var ( keybase keys.Keybase ) +// used for outputting keys.Info over REST +type KeyOutput struct { + Name string `json:"name"` + Address string `json:"address"` + // TODO add pubkey? + // Pubkey string `json:"pubkey"` +} + // GetKeyBase initializes a keybase based on the configuration func GetKeyBase() (keys.Keybase, error) { if keybase == nil { @@ -33,6 +41,11 @@ func GetKeyBase() (keys.Keybase, error) { return keybase, nil } +// used to set the keybase manually in test +func SetKeyBase(kb keys.Keybase) { + keybase = kb +} + func printInfo(info keys.Info) { switch viper.Get(cli.OutputFlag) { case "text": diff --git a/client/keys/wire.go b/client/keys/wire.go index 5f7c15344e67..225e60ae71ec 100644 --- a/client/keys/wire.go +++ b/client/keys/wire.go @@ -1,15 +1,14 @@ package keys import ( - crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" + "github.com/cosmos/cosmos-sdk/wire" ) var cdc *wire.Codec func init() { cdc = wire.NewCodec() - crypto.RegisterWire(cdc) + wire.RegisterCrypto(cdc) } func MarshalJSON(o interface{}) ([]byte, error) { diff --git a/client/lcd/.gitignore b/client/lcd/.gitignore new file mode 100644 index 000000000000..ae906702549b --- /dev/null +++ b/client/lcd/.gitignore @@ -0,0 +1 @@ +tmp-base* \ No newline at end of file diff --git a/client/lcd/helpers.go b/client/lcd/helpers.go new file mode 100644 index 000000000000..71278fca33ae --- /dev/null +++ b/client/lcd/helpers.go @@ -0,0 +1,69 @@ +package lcd + +// NOTE: COPIED VERBATIM FROM tendermint/tendermint/rpc/test/helpers.go + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + cmn "github.com/tendermint/tmlibs/common" + + cfg "github.com/tendermint/tendermint/config" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpcclient "github.com/tendermint/tendermint/rpc/lib/client" +) + +var globalConfig *cfg.Config + +func waitForRPC() { + laddr := GetConfig().RPC.ListenAddress + fmt.Println("LADDR", laddr) + client := rpcclient.NewJSONRPCClient(laddr) + result := new(ctypes.ResultStatus) + for { + _, err := client.Call("status", map[string]interface{}{}, result) + if err == nil { + return + } + } +} + +// f**ing long, but unique for each test +func makePathname() string { + // get path + p, err := os.Getwd() + if err != nil { + panic(err) + } + // fmt.Println(p) + sep := string(filepath.Separator) + return strings.Replace(p, sep, "_", -1) +} + +func randPort() int { + return int(cmn.RandUint16()/2 + 10000) +} + +func makeAddrs() (string, string, string) { + start := randPort() + return fmt.Sprintf("tcp://0.0.0.0:%d", start), + fmt.Sprintf("tcp://0.0.0.0:%d", start+1), + fmt.Sprintf("tcp://0.0.0.0:%d", start+2) +} + +// GetConfig returns a config for the test cases as a singleton +func GetConfig() *cfg.Config { + if globalConfig == nil { + pathname := makePathname() + globalConfig = cfg.ResetTestRoot(pathname) + + // and we use random ports to run in parallel + tm, rpc, _ := makeAddrs() + globalConfig.P2P.ListenAddress = tm + globalConfig.RPC.ListenAddress = rpc + globalConfig.TxIndex.IndexTags = "app.creator" // see kvstore application + } + return globalConfig +} diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go new file mode 100644 index 000000000000..285cf4d6d780 --- /dev/null +++ b/client/lcd/lcd_test.go @@ -0,0 +1,443 @@ +package lcd + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "regexp" + "testing" + "time" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" + cryptoKeys "github.com/tendermint/go-crypto/keys" + tmcfg "github.com/tendermint/tendermint/config" + nm "github.com/tendermint/tendermint/node" + p2p "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/proxy" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + tmrpc "github.com/tendermint/tendermint/rpc/lib/server" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + client "github.com/cosmos/cosmos-sdk/client" + keys "github.com/cosmos/cosmos-sdk/client/keys" + bapp "github.com/cosmos/cosmos-sdk/examples/basecoin/app" + btypes "github.com/cosmos/cosmos-sdk/examples/basecoin/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +var ( + coinDenom = "mycoin" + coinAmount = int64(10000000) + + // XXX bad globals + port string // XXX: but it's the int ... + name string = "test" + password string = "0123456789" + seed string + sendAddr string +) + +func TestKeys(t *testing.T) { + + // empty keys + // XXX: the test comes with a key setup + /* + res, body := request(t, port, "GET", "/keys", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + assert.Equal(t, "[]", body, "Expected an empty array") + */ + + // get seed + res, body := request(t, port, "GET", "/keys/seed", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + newSeed := body + reg, err := regexp.Compile(`([a-z]+ ){12}`) + require.Nil(t, err) + match := reg.MatchString(seed) + assert.True(t, match, "Returned seed has wrong foramt", seed) + + newName := "test_newname" + newPassword := "0987654321" + + // add key + var jsonStr = []byte(fmt.Sprintf(`{"name":"test_fail", "password":"%s"}`, password)) + res, body = request(t, port, "POST", "/keys", jsonStr) + + assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Account creation should require a seed") + + jsonStr = []byte(fmt.Sprintf(`{"name":"%s", "password":"%s", "seed": "%s"}`, newName, newPassword, newSeed)) + res, body = request(t, port, "POST", "/keys", jsonStr) + + assert.Equal(t, http.StatusOK, res.StatusCode, body) + addr := body + assert.Len(t, addr, 40, "Returned address has wrong format", addr) + + // existing keys + res, body = request(t, port, "GET", "/keys", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var m [2]keys.KeyOutput + err = json.Unmarshal([]byte(body), &m) + require.Nil(t, err) + + assert.Equal(t, m[0].Name, name, "Did not serve keys name correctly") + assert.Equal(t, m[0].Address, sendAddr, "Did not serve keys Address correctly") + assert.Equal(t, m[1].Name, newName, "Did not serve keys name correctly") + assert.Equal(t, m[1].Address, addr, "Did not serve keys Address correctly") + + // select key + keyEndpoint := fmt.Sprintf("/keys/%s", newName) + res, body = request(t, port, "GET", keyEndpoint, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var m2 keys.KeyOutput + err = json.Unmarshal([]byte(body), &m2) + require.Nil(t, err) + + assert.Equal(t, newName, m2.Name, "Did not serve keys name correctly") + assert.Equal(t, addr, m2.Address, "Did not serve keys Address correctly") + + // update key + jsonStr = []byte(fmt.Sprintf(`{"old_password":"%s", "new_password":"12345678901"}`, newPassword)) + res, body = request(t, port, "PUT", keyEndpoint, jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // here it should say unauthorized as we changed the password before + res, body = request(t, port, "PUT", keyEndpoint, jsonStr) + require.Equal(t, http.StatusUnauthorized, res.StatusCode, body) + + // delete key + jsonStr = []byte(`{"password":"12345678901"}`) + res, body = request(t, port, "DELETE", keyEndpoint, jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) +} + +func TestVersion(t *testing.T) { + + // node info + res, body := request(t, port, "GET", "/version", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + reg, err := regexp.Compile(`\d+\.\d+\.\d+(-dev)?`) + require.Nil(t, err) + match := reg.MatchString(body) + assert.True(t, match, body) +} + +func TestNodeStatus(t *testing.T) { + + // node info + res, body := request(t, port, "GET", "/node_info", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var nodeInfo p2p.NodeInfo + err := json.Unmarshal([]byte(body), &nodeInfo) + require.Nil(t, err, "Couldn't parse node info") + + assert.NotEqual(t, p2p.NodeInfo{}, nodeInfo, "res: %v", res) + + // syncing + res, body = request(t, port, "GET", "/syncing", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // we expect that there is no other node running so the syncing state is "false" + // we c + assert.Equal(t, "false", body) +} + +func TestBlock(t *testing.T) { + + time.Sleep(time.Second * 2) // TODO: LOL -> wait for blocks + + var resultBlock ctypes.ResultBlock + + res, body := request(t, port, "GET", "/blocks/latest", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err := json.Unmarshal([]byte(body), &resultBlock) + require.Nil(t, err, "Couldn't parse block") + + assert.NotEqual(t, ctypes.ResultBlock{}, resultBlock) + + // -- + + res, body = request(t, port, "GET", "/blocks/1", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err = json.Unmarshal([]byte(body), &resultBlock) + require.Nil(t, err, "Couldn't parse block") + + assert.NotEqual(t, ctypes.ResultBlock{}, resultBlock) + + // -- + + res, body = request(t, port, "GET", "/blocks/1000000000", nil) + require.Equal(t, http.StatusNotFound, res.StatusCode, body) +} + +func TestValidators(t *testing.T) { + + var resultVals ctypes.ResultValidators + + res, body := request(t, port, "GET", "/validatorsets/latest", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err := json.Unmarshal([]byte(body), &resultVals) + require.Nil(t, err, "Couldn't parse validatorset") + + assert.NotEqual(t, ctypes.ResultValidators{}, resultVals) + + // -- + + res, body = request(t, port, "GET", "/validatorsets/1", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err = json.Unmarshal([]byte(body), &resultVals) + require.Nil(t, err, "Couldn't parse validatorset") + + assert.NotEqual(t, ctypes.ResultValidators{}, resultVals) + + // -- + + res, body = request(t, port, "GET", "/validatorsets/1000000000", nil) + require.Equal(t, http.StatusNotFound, res.StatusCode) +} + +func TestCoinSend(t *testing.T) { + + // query empty + res, body := request(t, port, "GET", "/accounts/8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6", nil) + require.Equal(t, http.StatusNoContent, res.StatusCode, body) + + // create TX + receiveAddr, resultTx := doSend(t, port, seed) + + time.Sleep(time.Second * 2) // T + + // check if tx was commited + assert.Equal(t, uint32(0), resultTx.CheckTx.Code) + assert.Equal(t, uint32(0), resultTx.DeliverTx.Code) + + // query sender + res, body = request(t, port, "GET", "/accounts/"+sendAddr, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var m auth.BaseAccount + err := json.Unmarshal([]byte(body), &m) + require.Nil(t, err) + coins := m.Coins + mycoins := coins[0] + assert.Equal(t, coinDenom, mycoins.Denom) + assert.Equal(t, coinAmount-1, mycoins.Amount) + + // query receiver + res, body = request(t, port, "GET", "/accounts/"+receiveAddr, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err = json.Unmarshal([]byte(body), &m) + require.Nil(t, err) + coins = m.Coins + mycoins = coins[0] + assert.Equal(t, coinDenom, mycoins.Denom) + assert.Equal(t, int64(1), mycoins.Amount) +} + +func TestTxs(t *testing.T) { + + // TODO: re-enable once we can get txs by tag + + // query wrong + // res, body := request(t, port, "GET", "/txs", nil) + // require.Equal(t, http.StatusBadRequest, res.StatusCode, body) + + // query empty + // res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.sender='%s'", "8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6"), nil) + // require.Equal(t, http.StatusOK, res.StatusCode, body) + + // assert.Equal(t, "[]", body) + + // create TX + _, resultTx := doSend(t, port, seed) + + time.Sleep(time.Second * 2) // TO + + // check if tx is findable + res, body := request(t, port, "GET", fmt.Sprintf("/txs/%s", resultTx.Hash), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // // query sender + // res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.sender='%s'", addr), nil) + // require.Equal(t, http.StatusOK, res.StatusCode, body) + + // assert.NotEqual(t, "[]", body) + + // // query receiver + // res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.receiver='%s'", receiveAddr), nil) + // require.Equal(t, http.StatusOK, res.StatusCode, body) + + // assert.NotEqual(t, "[]", body) +} + +//__________________________________________________________ +// helpers + +// strt TM and the LCD in process, listening on their respective sockets +func startTMAndLCD() (*nm.Node, net.Listener, error) { + + kb, err := keys.GetKeyBase() // dbm.NewMemDB()) // :( + if err != nil { + return nil, nil, err + } + var info cryptoKeys.Info + info, seed, err = kb.Create(name, password, cryptoKeys.AlgoEd25519) // XXX global seed + if err != nil { + return nil, nil, err + } + + pubKey := info.PubKey + sendAddr = pubKey.Address().String() // XXX global + + config := GetConfig() + config.Consensus.TimeoutCommit = 1000 + config.Consensus.SkipTimeoutCommit = false + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger = log.NewFilter(logger, log.AllowError()) + privValidatorFile := config.PrivValidatorFile() + privVal := tmtypes.LoadOrGenPrivValidatorFS(privValidatorFile) + app := bapp.NewBasecoinApp(logger, dbm.NewMemDB()) + + genesisFile := config.GenesisFile() + genDoc, err := tmtypes.GenesisDocFromFile(genesisFile) + if err != nil { + return nil, nil, err + } + + coins := sdk.Coins{{coinDenom, coinAmount}} + appState := btypes.GenesisState{ + Accounts: []*btypes.GenesisAccount{ + { + Name: "tester", + Address: pubKey.Address(), + Coins: coins, + }, + }, + } + stateBytes, err := json.Marshal(appState) + if err != nil { + return nil, nil, err + } + genDoc.AppStateJSON = stateBytes + + cdc := wire.NewCodec() + + // LCD listen address + port = fmt.Sprintf("%d", 17377) // XXX + listenAddr := fmt.Sprintf("tcp://localhost:%s", port) // XXX + + // XXX: need to set this so LCD knows the tendermint node address! + viper.Set(client.FlagNode, config.RPC.ListenAddress) + viper.Set(client.FlagChainID, genDoc.ChainID) + + node, err := startTM(config, logger, genDoc, privVal, app) + if err != nil { + return nil, nil, err + } + lcd, err := startLCD(cdc, logger, listenAddr) + if err != nil { + return nil, nil, err + } + + time.Sleep(time.Second * 2) + + return node, lcd, nil +} + +// Create & start in-process tendermint node with memdb +// and in-process abci application. +// TODO: need to clean up the WAL dir or enable it to be not persistent +func startTM(cfg *tmcfg.Config, logger log.Logger, genDoc *tmtypes.GenesisDoc, privVal tmtypes.PrivValidator, app abci.Application) (*nm.Node, error) { + genDocProvider := func() (*tmtypes.GenesisDoc, error) { return genDoc, nil } + dbProvider := func(*nm.DBContext) (dbm.DB, error) { return dbm.NewMemDB(), nil } + n, err := nm.NewNode(cfg, + privVal, + proxy.NewLocalClientCreator(app), + genDocProvider, + dbProvider, + logger.With("module", "node")) + if err != nil { + return nil, err + } + + err = n.Start() + if err != nil { + return nil, err + } + + // wait for rpc + waitForRPC() + + logger.Info("Tendermint running!") + return n, err +} + +// start the LCD. note this blocks! +func startLCD(cdc *wire.Codec, logger log.Logger, listenAddr string) (net.Listener, error) { + handler := createHandler(cdc) + return tmrpc.StartHTTPServer(listenAddr, handler, logger) +} + +func request(t *testing.T, port, method, path string, payload []byte) (*http.Response, string) { + var res *http.Response + var err error + url := fmt.Sprintf("http://localhost:%v%v", port, path) + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + require.Nil(t, err) + res, err = http.DefaultClient.Do(req) + // res, err = http.Post(url, "application/json", bytes.NewBuffer(payload)) + require.Nil(t, err) + + output, err := ioutil.ReadAll(res.Body) + require.Nil(t, err) + + return res, string(output) +} + +func doSend(t *testing.T, port, seed string) (receiveAddr string, resultTx ctypes.ResultBroadcastTxCommit) { + + // create receive address + kb := client.MockKeyBase() + receiveInfo, _, err := kb.Create("receive_address", "1234567890", cryptoKeys.CryptoAlgo("ed25519")) + require.Nil(t, err) + receiveAddr = receiveInfo.PubKey.Address().String() + + // get the account to get the sequence + res, body := request(t, port, "GET", "/accounts/"+sendAddr, nil) + // require.Equal(t, http.StatusOK, res.StatusCode, body) + acc := auth.BaseAccount{} + err = json.Unmarshal([]byte(body), &acc) + require.Nil(t, err) + fmt.Println("BODY", body) + fmt.Println("ACC", acc) + sequence := acc.Sequence + + // send + jsonStr := []byte(fmt.Sprintf(`{ "name":"%s", "password":"%s", "sequence":%d, "amount":[{ "denom": "%s", "amount": 1 }] }`, name, password, sequence, coinDenom)) + res, body = request(t, port, "POST", "/accounts/"+receiveAddr+"/send", jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err = json.Unmarshal([]byte(body), &resultTx) + require.Nil(t, err) + + return receiveAddr, resultTx +} diff --git a/client/lcd/main_test.go b/client/lcd/main_test.go new file mode 100644 index 000000000000..9f0e2bd4f0de --- /dev/null +++ b/client/lcd/main_test.go @@ -0,0 +1,38 @@ +package lcd + +import ( + "fmt" + "os" + "testing" + + nm "github.com/tendermint/tendermint/node" +) + +var node *nm.Node + +// See https://golang.org/pkg/testing/#hdr-Main +// for more details +func TestMain(m *testing.M) { + // start a basecoind node and LCD server in the background to test against + + // run all the tests against a single server instance + node, lcd, err := startTMAndLCD() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + code := m.Run() + + // tear down + // TODO: cleanup + // TODO: it would be great if TM could run without + // persiting anything in the first place + node.Stop() + node.Wait() + + // just a listener ... + lcd.Close() + + os.Exit(code) +} diff --git a/client/lcd/root.go b/client/lcd/root.go index 9187af9a1a8d..7f18af59dc75 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -1,36 +1,82 @@ package lcd import ( - "errors" + "net/http" + "os" + "github.com/gorilla/mux" "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/tmlibs/log" - "github.com/cosmos/cosmos-sdk/client" + tmserver "github.com/tendermint/tendermint/rpc/lib/server" + cmn "github.com/tendermint/tmlibs/common" + + client "github.com/cosmos/cosmos-sdk/client" + keys "github.com/cosmos/cosmos-sdk/client/keys" + rpc "github.com/cosmos/cosmos-sdk/client/rpc" + tx "github.com/cosmos/cosmos-sdk/client/tx" + version "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/wire" + auth "github.com/cosmos/cosmos-sdk/x/auth/rest" + bank "github.com/cosmos/cosmos-sdk/x/bank/rest" ) const ( - flagBind = "bind" - flagCORS = "cors" + flagListenAddr = "laddr" + flagCORS = "cors" ) -// XXX: remove this when not needed -func todoNotImplemented(_ *cobra.Command, _ []string) error { - return errors.New("TODO: Command not yet implemented") -} - // ServeCommand will generate a long-running rest server // (aka Light Client Daemon) that exposes functionality similar // to the cli, but over rest -func ServeCommand() *cobra.Command { +func ServeCommand(cdc *wire.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "serve", + Use: "rest-server", Short: "Start LCD (light-client daemon), a local REST server", - RunE: todoNotImplemented, + RunE: startRESTServerFn(cdc), } - // TODO: handle unix sockets also? - cmd.Flags().StringP(flagBind, "b", "localhost:1317", "Interface and port that server binds to") + cmd.Flags().StringP(flagListenAddr, "a", "tcp://localhost:1317", "Address for server to listen on") cmd.Flags().String(flagCORS, "", "Set to domains that can make CORS requests (* for all)") cmd.Flags().StringP(client.FlagChainID, "c", "", "ID of chain we connect to") cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") return cmd } + +func startRESTServerFn(cdc *wire.Codec) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + listenAddr := viper.GetString(flagListenAddr) + handler := createHandler(cdc) + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "rest-server") + listener, err := tmserver.StartHTTPServer(listenAddr, handler, logger) + if err != nil { + return err + } + + // Wait forever and cleanup + cmn.TrapSignal(func() { + err := listener.Close() + logger.Error("Error closing listener", "err", err) + }) + return nil + } +} + +func createHandler(cdc *wire.Codec) http.Handler { + r := mux.NewRouter() + r.HandleFunc("/version", version.VersionRequestHandler).Methods("GET") + + kb, err := keys.GetKeyBase() //XXX + if err != nil { + panic(err) + } + + // TODO make more functional? aka r = keys.RegisterRoutes(r) + keys.RegisterRoutes(r) + rpc.RegisterRoutes(r) + tx.RegisterRoutes(r, cdc) + auth.RegisterRoutes(r, cdc, "main") + bank.RegisterRoutes(r, cdc, kb) + return r +} diff --git a/client/rpc/block.go b/client/rpc/block.go index c701061a2048..7f197051a94f 100644 --- a/client/rpc/block.go +++ b/client/rpc/block.go @@ -1,13 +1,15 @@ package rpc import ( + "encoding/json" "fmt" + "net/http" "strconv" + "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" - tmwire "github.com/tendermint/tendermint/wire" ) const ( @@ -18,7 +20,7 @@ func blockCommand() *cobra.Command { cmd := &cobra.Command{ Use: "block [height]", Short: "Get verified data for a the block at given height", - RunE: getBlock, + RunE: printBlock, } cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") // TODO: change this to false when we can @@ -27,7 +29,47 @@ func blockCommand() *cobra.Command { return cmd } -func getBlock(cmd *cobra.Command, args []string) error { +func getBlock(height *int64) ([]byte, error) { + // get the node + node, err := client.GetNode() + if err != nil { + return nil, err + } + + // TODO: actually honor the --select flag! + // header -> BlockchainInfo + // header, tx -> Block + // results -> BlockResults + res, err := node.Block(height) + if err != nil { + return nil, err + } + + // TODO move maarshalling into cmd/rest functions + // output, err := tmwire.MarshalJSON(res) + output, err := json.MarshalIndent(res, "", " ") + if err != nil { + return nil, err + } + return output, nil +} + +func GetChainHeight() (int64, error) { + node, err := client.GetNode() + if err != nil { + return -1, err + } + status, err := node.Status() + if err != nil { + return -1, err + } + height := status.LatestBlockHeight + return height, nil +} + +// CMD + +func printBlock(cmd *cobra.Command, args []string) error { var height *int64 // optional height if len(args) > 0 { @@ -41,26 +83,51 @@ func getBlock(cmd *cobra.Command, args []string) error { } } - // get the node - node, err := client.GetNode() + output, err := getBlock(height) if err != nil { return err } + fmt.Println(string(output)) + return nil +} - // TODO: actually honor the --select flag! - // header -> BlockchainInfo - // header, tx -> Block - // results -> BlockResults - res, err := node.Block(height) +// REST + +func BlockRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + height, err := strconv.ParseInt(vars["height"], 10, 64) if err != nil { - return err + w.WriteHeader(400) + w.Write([]byte("ERROR: Couldn't parse block height. Assumed format is '/block/{height}'.")) + return + } + chainHeight, err := GetChainHeight() + if height > chainHeight { + w.WriteHeader(404) + w.Write([]byte("ERROR: Requested block height is bigger then the chain length.")) + return } + output, err := getBlock(&height) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) +} - output, err := tmwire.MarshalJSON(res) - // output, err := json.MarshalIndent(res, " ", "") +func LatestBlockRequestHandler(w http.ResponseWriter, r *http.Request) { + height, err := GetChainHeight() if err != nil { - return err + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return } - fmt.Println(string(output)) - return nil + output, err := getBlock(&height) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) } diff --git a/client/rpc/root.go b/client/rpc/root.go index 8acf8ddad125..8b04c044f404 100644 --- a/client/rpc/root.go +++ b/client/rpc/root.go @@ -1,6 +1,7 @@ package rpc import ( + "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -42,3 +43,12 @@ func initClientCommand() *cobra.Command { cmd.Flags().String(flagValHash, "", "Hash of trusted validator set (hex-encoded)") return cmd } + +func RegisterRoutes(r *mux.Router) { + r.HandleFunc("/node_info", NodeInfoRequestHandler).Methods("GET") + r.HandleFunc("/syncing", NodeSyncingRequestHandler).Methods("GET") + r.HandleFunc("/blocks/latest", LatestBlockRequestHandler).Methods("GET") + r.HandleFunc("/blocks/{height}", BlockRequestHandler).Methods("GET") + r.HandleFunc("/validatorsets/latest", LatestValidatorsetRequestHandler).Methods("GET") + r.HandleFunc("/validatorsets/{height}", ValidatorsetRequestHandler).Methods("GET") +} diff --git a/client/rpc/status.go b/client/rpc/status.go index c5888d99f5cc..e5da94869255 100644 --- a/client/rpc/status.go +++ b/client/rpc/status.go @@ -1,40 +1,88 @@ package rpc import ( + "encoding/json" "fmt" + "net/http" + "strconv" "github.com/spf13/cobra" + wire "github.com/tendermint/go-wire" "github.com/cosmos/cosmos-sdk/client" - tmwire "github.com/tendermint/tendermint/wire" + ctypes "github.com/tendermint/tendermint/rpc/core/types" ) func statusCommand() *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "Query remote node for status", - RunE: checkStatus, + RunE: printNodeStatus, } cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") return cmd } -func checkStatus(cmd *cobra.Command, args []string) error { +func getNodeStatus() (*ctypes.ResultStatus, error) { // get the node node, err := client.GetNode() if err != nil { - return err + return &ctypes.ResultStatus{}, err } - res, err := node.Status() + return node.Status() +} + +// CMD + +func printNodeStatus(cmd *cobra.Command, args []string) error { + status, err := getNodeStatus() if err != nil { return err } - output, err := tmwire.MarshalJSON(res) + output, err := wire.MarshalJSON(status) // output, err := json.MarshalIndent(res, " ", "") if err != nil { return err } + fmt.Println(string(output)) return nil } + +// REST + +func NodeInfoRequestHandler(w http.ResponseWriter, r *http.Request) { + status, err := getNodeStatus() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + nodeInfo := status.NodeInfo + output, err := json.MarshalIndent(nodeInfo, "", " ") + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) +} + +func NodeSyncingRequestHandler(w http.ResponseWriter, r *http.Request) { + status, err := getNodeStatus() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + syncing := status.Syncing + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write([]byte(strconv.FormatBool(syncing))) +} diff --git a/client/rpc/validators.go b/client/rpc/validators.go index 8eebda8dd690..32c7680ec663 100644 --- a/client/rpc/validators.go +++ b/client/rpc/validators.go @@ -1,20 +1,22 @@ package rpc import ( + "encoding/json" "fmt" + "net/http" "strconv" + "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" - tmwire "github.com/tendermint/tendermint/wire" ) func validatorCommand() *cobra.Command { cmd := &cobra.Command{ Use: "validatorset ", Short: "Get the full validator set at given height", - RunE: getValidators, + RunE: printValidators, } cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") // TODO: change this to false when we can @@ -22,7 +24,28 @@ func validatorCommand() *cobra.Command { return cmd } -func getValidators(cmd *cobra.Command, args []string) error { +func getValidators(height *int64) ([]byte, error) { + // get the node + node, err := client.GetNode() + if err != nil { + return nil, err + } + + res, err := node.Validators(height) + if err != nil { + return nil, err + } + + output, err := json.MarshalIndent(res, "", " ") + if err != nil { + return nil, err + } + return output, nil +} + +// CMD + +func printValidators(cmd *cobra.Command, args []string) error { var height *int64 // optional height if len(args) > 0 { @@ -36,22 +59,52 @@ func getValidators(cmd *cobra.Command, args []string) error { } } - // get the node - node, err := client.GetNode() + output, err := getValidators(height) if err != nil { return err } - res, err := node.Validators(height) + fmt.Println(string(output)) + return nil +} + +// REST + +func ValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + height, err := strconv.ParseInt(vars["height"], 10, 64) if err != nil { - return err + w.WriteHeader(400) + w.Write([]byte("ERROR: Couldn't parse block height. Assumed format is '/validatorsets/{height}'.")) + return + } + chainHeight, err := GetChainHeight() + if height > chainHeight { + w.WriteHeader(404) + w.Write([]byte("ERROR: Requested block height is bigger then the chain length.")) + return } + output, err := getValidators(&height) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) +} - output, err := tmwire.MarshalJSON(res) - // output, err := json.MarshalIndent(res, " ", "") +func LatestValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) { + height, err := GetChainHeight() if err != nil { - return err + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return } - fmt.Println(string(output)) - return nil + output, err := getValidators(&height) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) } diff --git a/client/tx/broadcast.go b/client/tx/broadcast.go new file mode 100644 index 000000000000..b9367645fefa --- /dev/null +++ b/client/tx/broadcast.go @@ -0,0 +1,33 @@ +package tx + +import ( + "encoding/json" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/builder" +) + +type BroadcastTxBody struct { + TxBytes string `json="tx"` +} + +func BroadcastTxRequestHandler(w http.ResponseWriter, r *http.Request) { + var m BroadcastTxBody + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&m) + if err != nil { + w.WriteHeader(400) + w.Write([]byte(err.Error())) + return + } + + res, err := builder.BroadcastTx([]byte(m.TxBytes)) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + w.Write([]byte(string(res.Height))) +} diff --git a/client/tx/tx.go b/client/tx/query.go similarity index 62% rename from client/tx/tx.go rename to client/tx/query.go index f9ac0631bfeb..7c8c4d124fbe 100644 --- a/client/tx/tx.go +++ b/client/tx/query.go @@ -4,16 +4,19 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net/http" + "strconv" + "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" + abci "github.com/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" - abci "github.com/tendermint/abci/types" - wire "github.com/tendermint/go-wire" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/cosmos/cosmos-sdk/wire" ) // Get the default command for a tx query @@ -21,7 +24,7 @@ func QueryTxCmd(cmdr commander) *cobra.Command { cmd := &cobra.Command{ Use: "tx [hash]", Short: "Matches this txhash over all committed blocks", - RunE: cmdr.queryTxCmd, + RunE: cmdr.queryAndPrintTx, } cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") // TODO: change this to false when we can @@ -29,42 +32,28 @@ func QueryTxCmd(cmdr commander) *cobra.Command { return cmd } -// command to query for a transaction -func (c commander) queryTxCmd(cmd *cobra.Command, args []string) error { - if len(args) != 1 || len(args[0]) == 0 { - return errors.New("You must provide a tx hash") - } - - // find the key to look up the account - hexStr := args[0] - hash, err := hex.DecodeString(hexStr) +func (c commander) queryTx(hashHexStr string, trustNode bool) ([]byte, error) { + hash, err := hex.DecodeString(hashHexStr) if err != nil { - return err + return nil, err } // get the node node, err := client.GetNode() if err != nil { - return err + return nil, err } - prove := !viper.GetBool(client.FlagTrustNode) - res, err := node.Tx(hash, prove) + res, err := node.Tx(hash, !trustNode) if err != nil { - return err + return nil, err } info, err := formatTxResult(c.cdc, res) if err != nil { - return err - } - - output, err := json.MarshalIndent(info, "", " ") - if err != nil { - return err + return nil, err } - fmt.Println(string(output)) - return nil + return json.MarshalIndent(info, "", " ") } func formatTxResult(cdc *wire.Codec, res *ctypes.ResultTx) (txInfo, error) { @@ -97,3 +86,47 @@ func parseTx(cdc *wire.Codec, txBytes []byte) (sdk.Tx, error) { } return tx, nil } + +// CMD + +// command to query for a transaction +func (c commander) queryAndPrintTx(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a tx hash") + } + + // find the key to look up the account + hashHexStr := args[0] + trustNode := viper.GetBool(client.FlagTrustNode) + + output, err := c.queryTx(hashHexStr, trustNode) + if err != nil { + return err + } + fmt.Println(string(output)) + + return nil +} + +// REST + +func QueryTxRequestHandler(cdc *wire.Codec) func(http.ResponseWriter, *http.Request) { + c := commander{cdc} + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + hashHexStr := vars["hash"] + trustNode, err := strconv.ParseBool(r.FormValue("trust_node")) + // trustNode defaults to true + if err != nil { + trustNode = true + } + + output, err := c.queryTx(hashHexStr, trustNode) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) + } +} diff --git a/client/tx/root.go b/client/tx/root.go index 2099fb2112ac..f7d2cf945e5b 100644 --- a/client/tx/root.go +++ b/client/tx/root.go @@ -1,8 +1,10 @@ package tx import ( + "github.com/gorilla/mux" "github.com/spf13/cobra" - wire "github.com/tendermint/go-wire" + + "github.com/cosmos/cosmos-sdk/wire" ) // type used to pass around the provided cdc @@ -18,3 +20,10 @@ func AddCommands(cmd *cobra.Command, cdc *wire.Codec) { QueryTxCmd(cmdr), ) } + +func RegisterRoutes(r *mux.Router, cdc *wire.Codec) { + // r.HandleFunc("/txs", SearchTxRequestHandler(cdc)).Methods("GET") + r.HandleFunc("/txs/{hash}", QueryTxRequestHandler(cdc)).Methods("GET") + // r.HandleFunc("/txs/sign", SignTxRequstHandler).Methods("POST") + // r.HandleFunc("/txs/broadcast", BroadcastTxRequestHandler).Methods("POST") +} diff --git a/client/tx/search.go b/client/tx/search.go index ffe0ca323d94..2790750ebe87 100644 --- a/client/tx/search.go +++ b/client/tx/search.go @@ -3,14 +3,16 @@ package tx import ( "errors" "fmt" + "net/http" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/cosmos/cosmos-sdk/client" - wire "github.com/tendermint/go-wire" ctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/wire" ) const ( @@ -23,7 +25,7 @@ func SearchTxCmd(cmdr commander) *cobra.Command { cmd := &cobra.Command{ Use: "txs", Short: "Search for all transactions that match the given tags", - RunE: cmdr.searchTxCmd, + RunE: cmdr.searchAndPrintTx, } cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") // TODO: change this to false once proofs built in @@ -33,10 +35,9 @@ func SearchTxCmd(cmdr commander) *cobra.Command { return cmd } -func (c commander) searchTxCmd(cmd *cobra.Command, args []string) error { - tags := viper.GetStringSlice(flagTags) +func (c commander) searchTx(tags []string) ([]byte, error) { if len(tags) == 0 { - return errors.New("Must declare at least one tag to search") + return nil, errors.New("Must declare at least one tag to search") } // XXX: implement ANY query := strings.Join(tags, " AND ") @@ -44,27 +45,25 @@ func (c commander) searchTxCmd(cmd *cobra.Command, args []string) error { // get the node node, err := client.GetNode() if err != nil { - return err + return nil, err } prove := !viper.GetBool(client.FlagTrustNode) res, err := node.TxSearch(query, prove) if err != nil { - return err + return nil, err } info, err := formatTxResults(c.cdc, res) if err != nil { - return err + return nil, err } output, err := c.cdc.MarshalJSON(info) if err != nil { - return err + return nil, err } - fmt.Println(string(output)) - - return nil + return output, nil } func formatTxResults(cdc *wire.Codec, res []*ctypes.ResultTx) ([]txInfo, error) { @@ -78,3 +77,40 @@ func formatTxResults(cdc *wire.Codec, res []*ctypes.ResultTx) ([]txInfo, error) } return out, nil } + +// CMD + +func (c commander) searchAndPrintTx(cmd *cobra.Command, args []string) error { + tags := viper.GetStringSlice(flagTags) + + output, err := c.searchTx(tags) + if err != nil { + return err + } + + fmt.Println(string(output)) + return nil +} + +// REST + +func SearchTxRequestHandler(cdc *wire.Codec) func(http.ResponseWriter, *http.Request) { + c := commander{cdc} + return func(w http.ResponseWriter, r *http.Request) { + tag := r.FormValue("tag") + if tag == "" { + w.WriteHeader(400) + w.Write([]byte("You need to provide a tag to search for.")) + return + } + + tags := []string{tag} + output, err := c.searchTx(tags) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + w.Write(output) + } +} diff --git a/client/tx/sign.go b/client/tx/sign.go new file mode 100644 index 000000000000..3a3fff4a0caf --- /dev/null +++ b/client/tx/sign.go @@ -0,0 +1,45 @@ +package tx + +import ( + "encoding/json" + "net/http" + + keybase "github.com/cosmos/cosmos-sdk/client/keys" + keys "github.com/tendermint/go-crypto/keys" +) + +type SignTxBody struct { + Name string `json="name"` + Password string `json="password"` + TxBytes string `json="tx"` +} + +func SignTxRequstHandler(w http.ResponseWriter, r *http.Request) { + var kb keys.Keybase + var m SignTxBody + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&m) + if err != nil { + w.WriteHeader(400) + w.Write([]byte(err.Error())) + return + } + + kb, err = keybase.GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + return + } + + //TODO check if account exists + sig, _, err := kb.Sign(m.Name, m.Password, []byte(m.TxBytes)) + if err != nil { + w.WriteHeader(403) + w.Write([]byte(err.Error())) + return + } + + w.Write(sig.Bytes()) +} diff --git a/docs/guide.md b/docs/guide.md index 8741ada865c3..5c31d2e27165 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -42,7 +42,7 @@ type Msg interface { // Signers returns the addrs of signers that must sign. // CONTRACT: All signatures must be present to be valid. // CONTRACT: Returns addrs in some deterministic order. - GetSigners() []crypto.Address + GetSigners() []Address } ``` @@ -75,7 +75,7 @@ type SendMsg struct { } type IssueMsg struct { - Banker crypto.Address `json:"banker"` + Banker sdk.Address `json:"banker"` Outputs []Output `json:"outputs"` } ``` @@ -83,16 +83,16 @@ type IssueMsg struct { Each specifies the addresses that must sign the message: ```golang -func (msg SendMsg) GetSigners() []crypto.Address { - addrs := make([]crypto.Address, len(msg.Inputs)) +func (msg SendMsg) GetSigners() []sdk.Address { + addrs := make([]sdk.Address, len(msg.Inputs)) for i, in := range msg.Inputs { addrs[i] = in.Address } return addrs } -func (msg IssueMsg) GetSigners() []crypto.Address { - return []crypto.Address{msg.Banker} +func (msg IssueMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Banker} } ``` @@ -105,14 +105,6 @@ type Tx interface { GetMsg() Msg - // The address that pays the base fee for this message. The fee is - // deducted before the Msg is processed. - GetFeePayer() crypto.Address - - // Get the canonical byte representation of the Tx. - // Includes any signatures (or empty slots). - GetTxBytes() []byte - // Signatures returns the signature of signers who signed the Msg. // CONTRACT: Length returned is same as length of // pubkeys returned from MsgKeySigners, and the order @@ -148,8 +140,9 @@ case of Basecoin, the public key only needs to be included in the first transaction send by a given account - after that, the public key is forever stored by the application and can be left out of transactions. -Transactions can also specify the address responsible for paying the -transaction's fees using the `tx.GetFeePayer()` method. +The address responsible for paying the transactions fee is the first address +returned by msg.GetSigners(). The convenience function `FeePayer(tx Tx)` is provided +to return this. The standard way to create a transaction from a message is to use the `StdTx`: diff --git a/docs/sdk/install.rst b/docs/sdk/install.rst index 4857f63e79c9..c5a82475a315 100644 --- a/docs/sdk/install.rst +++ b/docs/sdk/install.rst @@ -13,7 +13,7 @@ Usually, Cosmos SDK can be installed like a normal Go program: go get -u github.com/cosmos/cosmos-sdk If the dependencies have been updated with breaking changes, or if -another branch is required, ``glide`` is used for dependency management. +another branch is required, ``dep`` is used for dependency management. Thus, assuming you've already run ``go get`` or otherwise cloned the repo, the correct way to install is: @@ -24,12 +24,12 @@ repo, the correct way to install is: make all This will create the ``basecoin`` binary in ``$GOPATH/bin``. -``make all`` implies ``make get_vendor_deps`` and uses ``glide`` to +``make all`` implies ``make get_vendor_deps`` and uses ``dep`` to install the correct version of all dependencies. It also tests the code, including some cli tests to make sure your binary behaves properly. If you need another branch, make sure to run ``git checkout `` before ``make all``. And if you switch branches a lot, especially touching other tendermint repos, you may need to ``make fresh`` -sometimes so glide doesn't get confused with all the branches and +sometimes so dep doesn't get confused with all the branches and versions lying around. diff --git a/docs/sdk/lcd-rest-api.yaml b/docs/sdk/lcd-rest-api.yaml index 08264d56460d..eb9ec714bb5a 100644 --- a/docs/sdk/lcd-rest-api.yaml +++ b/docs/sdk/lcd-rest-api.yaml @@ -46,6 +46,14 @@ paths: other: description: more information on versions type: array + /syncing: + get: + summary: Syncing state of node + description: Get if the node is currently syning with other nodes + responses: + 200: + description: "true" or "false" + /keys: get: summary: List of accounts stored locally @@ -62,7 +70,7 @@ paths: summary: Create a new account locally responses: 200: - description: OK + description: Returns address of the account created requestBody: content: application/json: @@ -117,9 +125,12 @@ paths: schema: type: object required: - - password + - new_password + - old_password properties: - password: + new_password: + type: string + old_password: type: string responses: 200: @@ -147,35 +158,35 @@ paths: description: Password is wrong 404: description: Account is not available - /accounts/send: - post: - summary: Send coins (build -> sign -> send) - security: - - sign: [] - requestBody: - content: - application/json: - schema: - type: object - properties: - fees: - $ref: "#/components/schemas/Coins" - outputs: - type: array - items: - type: object - properties: - pub_key: - $ref: "#/components/schemas/PubKey" - amount: - type: array - items: - $ref: "#/components/schemas/Coins" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated + # /accounts/send: + # post: + # summary: Send coins (build -> sign -> send) + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # type: object + # properties: + # fees: + # $ref: "#/components/schemas/Coins" + # outputs: + # type: array + # items: + # type: object + # properties: + # pub_key: + # $ref: "#/components/schemas/PubKey" + # amount: + # type: array + # items: + # $ref: "#/components/schemas/Coins" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated /accounts/{address}: parameters: @@ -214,12 +225,18 @@ paths: schema: type: object properties: - fees: - $ref: "#/components/schemas/Coins" + name: + type: string + password: + type: string amount: type: array items: $ref: "#/components/schemas/Coins" + chain_id: + type: string + squence: + type: number responses: 202: description: Tx was send and will probably be added to the next block @@ -300,72 +317,72 @@ paths: $ref: "#/components/schemas/Delegate" 404: description: Block at height not available - /txs: - parameters: - - in: query - name: tag - schema: - type: string - example: "coin.sender=EE5F3404034C524501629B56E0DDC38FAD651F04" - required: true - - in: query - name: page - description: Pagination page - schema: - type: number - default: 0 - - in: query - name: size - description: Pagination size - schema: - type: number - default: 50 - get: - summary: Query Tx - responses: - 200: - description: All Tx matching the provided tags - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Tx" - 404: - description: Pagination is out of bounds - /txs/sign: - post: - summary: Sign a Tx - description: Sign a Tx providing locally stored account and according password - security: - - sign: [] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/TxBuild" - responses: - 200: - description: The signed Tx - content: - application/json: - schema: - $ref: "#/components/schemas/TxSigned" - 401: - description: Account name and/or password where wrong - /txs/broadcast: - post: - summary: Send signed Tx - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/TxSigned" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated + # /txs: + # parameters: + # - in: query + # name: tag + # schema: + # type: string + # example: "coin.sender=EE5F3404034C524501629B56E0DDC38FAD651F04" + # required: true + # - in: query + # name: page + # description: Pagination page + # schema: + # type: number + # default: 0 + # - in: query + # name: size + # description: Pagination size + # schema: + # type: number + # default: 50 + # get: + # summary: Query Tx + # responses: + # 200: + # description: All Tx matching the provided tags + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: "#/components/schemas/Tx" + # 404: + # description: Pagination is out of bounds + # /txs/sign: + # post: + # summary: Sign a Tx + # description: Sign a Tx providing locally stored account and according password + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # $ref: "#/components/schemas/TxBuild" + # responses: + # 200: + # description: The signed Tx + # content: + # application/json: + # schema: + # $ref: "#/components/schemas/TxSigned" + # 401: + # description: Account name and/or password where wrong + # /txs/broadcast: + # post: + # summary: Send signed Tx + # requestBody: + # content: + # application/json: + # schema: + # $ref: "#/components/schemas/TxSigned" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated /txs/{hash}: parameters: - in: path @@ -385,140 +402,140 @@ paths: $ref: "#/components/schemas/Tx" 404: description: Tx not available for provided hash - /delegates: - parameters: - - in: query - name: delegator - description: Query for all delegates a delegator has stake with - schema: - $ref: "#/components/schemas/Address" - get: - summary: Get a list of canidates/delegates/validators (optionally filtered by delegator) - responses: - 200: - description: List of delegates, filtered by provided delegator address - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Delegate" - /delegates/bond: - post: - summary: Bond atoms (build -> sign -> send) - security: - - sign: [] - requestBody: - content: - application/json: - schema: - type: array - items: - type: object - properties: - amount: - $ref: "#/components/schemas/Coins" - pub_key: - $ref: "#/components/schemas/PubKey" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated - /delegates/unbond: - post: - summary: Unbond atoms (build -> sign -> send) - security: - - sign: [] - requestBody: - content: - application/json: - schema: - type: array - items: - type: object - properties: - amount: - $ref: "#/components/schemas/Coins" - pub_key: - $ref: "#/components/schemas/PubKey" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated - /delegates/{pubkey}: - parameters: - - in: path - name: pubkey - description: Pubkey of a delegate - required: true - schema: - type: string - example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 - get: - summary: Get a certain canidate/delegate/validator - responses: - 200: - description: Delegate for specified pub_key - content: - application/json: - schema: - $ref: "#/components/schemas/Delegate" - 404: - description: No delegate found for provided pub_key - /delegates/{pubkey}/bond: - parameters: - - in: path - name: pubkey - description: Pubkey of a delegate - required: true - schema: - type: string - example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 - post: - summary: Bond atoms (build -> sign -> send) - security: - - sign: [] - requestBody: - content: - application/json: - schema: - type: object - properties: - amount: - $ref: "#/components/schemas/Coins" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated - /delegates/{pubkey}/unbond: - parameters: - - in: path - name: pubkey - description: Pubkey of a delegate - required: true - schema: - type: string - example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 - post: - summary: Unbond atoms (build -> sign -> send) - security: - - sign: [] - requestBody: - content: - application/json: - schema: - type: object - properties: - amount: - $ref: "#/components/schemas/Coins" - responses: - 202: - description: Tx was send and will probably be added to the next block - 400: - description: The Tx was malformated + # /delegates: + # parameters: + # - in: query + # name: delegator + # description: Query for all delegates a delegator has stake with + # schema: + # $ref: "#/components/schemas/Address" + # get: + # summary: Get a list of canidates/delegates/validators (optionally filtered by delegator) + # responses: + # 200: + # description: List of delegates, filtered by provided delegator address + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: "#/components/schemas/Delegate" + # /delegates/bond: + # post: + # summary: Bond atoms (build -> sign -> send) + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # type: array + # items: + # type: object + # properties: + # amount: + # $ref: "#/components/schemas/Coins" + # pub_key: + # $ref: "#/components/schemas/PubKey" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated + # /delegates/unbond: + # post: + # summary: Unbond atoms (build -> sign -> send) + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # type: array + # items: + # type: object + # properties: + # amount: + # $ref: "#/components/schemas/Coins" + # pub_key: + # $ref: "#/components/schemas/PubKey" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated + # /delegates/{pubkey}: + # parameters: + # - in: path + # name: pubkey + # description: Pubkey of a delegate + # required: true + # schema: + # type: string + # example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 + # get: + # summary: Get a certain canidate/delegate/validator + # responses: + # 200: + # description: Delegate for specified pub_key + # content: + # application/json: + # schema: + # $ref: "#/components/schemas/Delegate" + # 404: + # description: No delegate found for provided pub_key + # /delegates/{pubkey}/bond: + # parameters: + # - in: path + # name: pubkey + # description: Pubkey of a delegate + # required: true + # schema: + # type: string + # example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 + # post: + # summary: Bond atoms (build -> sign -> send) + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # type: object + # properties: + # amount: + # $ref: "#/components/schemas/Coins" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated + # /delegates/{pubkey}/unbond: + # parameters: + # - in: path + # name: pubkey + # description: Pubkey of a delegate + # required: true + # schema: + # type: string + # example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958 + # post: + # summary: Unbond atoms (build -> sign -> send) + # security: + # - sign: [] + # requestBody: + # content: + # application/json: + # schema: + # type: object + # properties: + # amount: + # $ref: "#/components/schemas/Coins" + # responses: + # 202: + # description: Tx was send and will probably be added to the next block + # 400: + # description: The Tx was malformated components: schemas: diff --git a/docs/sdk/overview.rst b/docs/sdk/overview.rst index 25ed2b132b8e..9e79dd04ff7c 100644 --- a/docs/sdk/overview.rst +++ b/docs/sdk/overview.rst @@ -56,7 +56,7 @@ Strictly speaking, Golang does not implement object capabilities completely, bec * pervasive ability to override module vars https://github.com/golang/go/issues/23161 * data-race vulnerability where 2+ goroutines can create illegal interface values -The first is easy to catch by auditing imports and using a proper dependency version control system like Glide. The second and third are unfortunate but it can be audited with some cost. +The first is easy to catch by auditing imports and using a proper dependency version control system like Dep. The second and third are unfortunate but it can be audited with some cost. Perhaps `Go2 will implement the object capability model `__. @@ -156,7 +156,7 @@ implementing the ``Msg`` interface: // Signers returns the addrs of signers that must sign. // CONTRACT: All signatures must be present to be valid. // CONTRACT: Returns addrs in some deterministic order. - GetSigners() []crypto.Address + GetSigners() []Address } Messages must specify their type via the ``Type()`` method. The type should @@ -188,7 +188,7 @@ For instance, the ``Basecoin`` message types are defined in ``x/bank/tx.go``: } type IssueMsg struct { - Banker crypto.Address `json:"banker"` + Banker sdk.Address `json:"banker"` Outputs []Output `json:"outputs"` } @@ -196,16 +196,16 @@ Each specifies the addresses that must sign the message: :: - func (msg SendMsg) GetSigners() []crypto.Address { - addrs := make([]crypto.Address, len(msg.Inputs)) + func (msg SendMsg) GetSigners() []sdk.Address { + addrs := make([]sdk.Address, len(msg.Inputs)) for i, in := range msg.Inputs { addrs[i] = in.Address } return addrs } - func (msg IssueMsg) GetSigners() []crypto.Address { - return []crypto.Address{msg.Banker} + func (msg IssueMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Banker} } Transactions @@ -219,14 +219,6 @@ A transaction is a message with additional information for authentication: GetMsg() Msg - // The address that pays the base fee for this message. The fee is - // deducted before the Msg is processed. - GetFeePayer() crypto.Address - - // Get the canonical byte representation of the Tx. - // Includes any signatures (or empty slots). - GetTxBytes() []byte - // Signatures returns the signature of signers who signed the Msg. // CONTRACT: Length returned is same as length of // pubkeys returned from MsgKeySigners, and the order @@ -261,9 +253,6 @@ case of Basecoin, the public key only needs to be included in the first transaction send by a given account - after that, the public key is forever stored by the application and can be left out of transactions. -Transactions can also specify the address responsible for paying the -transaction's fees using the ``tx.GetFeePayer()`` method. - The standard way to create a transaction from a message is to use the ``StdTx``: :: diff --git a/docs/spec/README.md b/docs/spec/README.md index 6a507dc03191..5f3942ff9bc8 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -1,13 +1,18 @@ # Cosmos Hub Spec -This directory contains specifications for the application level components of the Cosmos Hub. +This directory contains specifications for the application level components of +the Cosmos Hub. NOTE: the specifications are not yet complete and very much a work in progress. -- [Basecoin](basecoin) - Cosmos SDK related specifications and transactions for sending tokens. -- [Staking](staking) - Proof of Stake related specifications including bonding and delegation transactions, inflation, fees, etc. -- [Governance](governance) - Governance related specifications including proposals and voting. -- [Other](other) - Other components of the Cosmos Hub, including the reserve pool, All in Bits vesting, etc. +- [Basecoin](basecoin) - Cosmos SDK related specifications and transactions for + sending tokens. +- [Staking](staking) - Proof of Stake related specifications including bonding + and delegation transactions, inflation, fees, etc. +- [Governance](governance) - Governance related specifications including + proposals and voting. +- [Other](other) - Other components of the Cosmos Hub, including the reserve + pool, All in Bits vesting, etc. The [specification for Tendermint](https://github.com/tendermint/tendermint/tree/develop/docs/specification/new-spec), i.e. the underlying blockchain, can be found elsewhere. diff --git a/docs/spec/governance/README.md b/docs/spec/governance/README.md new file mode 100644 index 000000000000..0ddbc7cfdcff --- /dev/null +++ b/docs/spec/governance/README.md @@ -0,0 +1,35 @@ +# Governance module specification + +## Abstract + +This paper specifies the Governance module of the Cosmos-SDK, which was first described in the [Cosmos Whitepaper](https://cosmos.network/about/whitepaper) in June 2016. + +The module enables Cosmos-SDK based blockchain to support an on-chain governance system. In this system, holders of the native staking token of the chain can vote on proposals on a 1 token 1 vote basis. Next is a list of features the module currently supports: + +- **Proposal submission:** Users can submit proposals with a deposit. Once the minimum deposit is reached, proposal enters voting period +- **Vote:** Participants can vote on proposals that reached MinDeposit +- **Inheritance and penalties:** Delegators inherit their validator's vote if they don't vote themselves. If validators do not vote, they get partially slashed. +- **Signal and switch:** If a proposal of type `SoftwareUpgradeProposal` is accepted, validators can signal it and switch once enough validators have signalled. +- **Claiming deposit:** Users that deposited on proposals can recover their deposits if the proposal was accepted OR if the proposal never entered voting period. + +Features that may be added in the future are described in [Future improvements](future_improvements.md) + +This module will be used in the Cosmos Hub, the first Hub in the Cosmos network. + + +## Contents + +The following specification uses *Atom* as the native staking token. The module can be adapted to any Proof-Of-Stake blockchain by replacing *Atom* with the native staking token of the chain. + +1. **[Design overview](overview.md)** +2. **Implementation** + 1. **[State](state.md)** + 1. Procedures + 2. Proposals + 3. Proposal Processing Queue + 2. **[Transactions](transactions.md)** + 1. Proposal Submission + 2. Deposit + 3. Claim Deposit + 4. Vote +3. **[Future improvements](future_improvements.md)** diff --git a/docs/spec/governance/future_improvements.md b/docs/spec/governance/future_improvements.md new file mode 100644 index 000000000000..9e0b0f4dfbb6 --- /dev/null +++ b/docs/spec/governance/future_improvements.md @@ -0,0 +1,30 @@ +# Future improvements (not in scope for MVP) + +The current documentation only describes the minimum viable product for the +governance module. Future improvements may include: + +* **`BountyProposals`:** If accepted, a `BountyProposal` creates an open + bounty. The `BountyProposal` specifies how many Atoms will be given upon + completion. These Atoms will be taken from the `reserve pool`. After a + `BountyProposal` is accepted by governance, anybody can submit a + `SoftwareUpgradeProposal` with the code to claim the bounty. Note that once a + `BountyProposal` is accepted, the corresponding funds in the `reserve pool` + are locked so that payment can always be honored. In order to link a + `SoftwareUpgradeProposal` to an open bounty, the submitter of the + `SoftwareUpgradeProposal` will use the `Proposal.LinkedProposal` attribute. + If a `SoftwareUpgradeProposal` linked to an open bounty is accepted by + governance, the funds that were reserved are automatically transferred to the + submitter. +* **Complex delegation:** Delegators could choose other representatives than + their validators. Ultimately, the chain of representatives would always end + up to a validator, but delegators could inherit the vote of their chosen + representative before they inherit the vote of their validator. In other + words, they would only inherit the vote of their validator if their other + appointed representative did not vote. +* **`ParameterProposals` and `WhitelistProposals`:** These proposals would + automatically change pre-defined parameters and whitelists. Upon acceptance, + these proposals would not require validators to do the signal and switch + process. +* **Better process for proposal review:** There would be two parts to + `proposal.Deposit`, one for anti-spam (same as in MVP) and an other one to + reward third party auditors. \ No newline at end of file diff --git a/docs/spec/governance/governance.md b/docs/spec/governance/governance.md index c8560cee6f54..2e40e92dd439 100644 --- a/docs/spec/governance/governance.md +++ b/docs/spec/governance/governance.md @@ -10,27 +10,27 @@ The governance process is divided in a few steps that are outlined below: - **Proposal submission:** Proposal is submitted to the blockchain with a deposit - **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is confirmed and vote opens. Bonded Atom holders can then send `TxGovVote` transactions to vote on the proposal -- If the proposal involves a software upgrade - - **Signal:** Validator start signaling that they are ready to switch to the new version - - **Switch:** Once more than 2/3rd validators have signaled their readiness to switch, their software automatically flips to the new version +- If the proposal involves a software upgrade: + - **Signal:** Validators start signaling that they are ready to switch to the new version + - **Switch:** Once more than 75% of validators have signaled that they are ready to switch, their software automatically flips to the new version ## Proposal submission ### Right to submit a proposal -Any Atom holder, whether bonded or unbonded, can submit proposals by sending a `TxProposal` transaction. Once a proposal is submitted, it is identified by its unique `proposalID`. +Any Atom holder, whether bonded or unbonded, can submit proposals by sending a `TxGovProposal` transaction. Once a proposal is submitted, it is identified by its unique `proposalID`. ### Proposal filter (minimum deposit) -To prevent spam, proposals must be submitted with a `deposit` in Atoms such that `0 < deposit < MinDeposit`. -Other Atom holders can increase the proposal's deposit by sending a `TxGovDeposit` transaction. -Once the proposals's deposit reaches `MinDeposit`, it enters voting period. +To prevent spam, proposals must be submitted with a deposit in Atoms. Voting period will not start as long as the proposal's deposit is smaller than the minimum deposit `MinDeposit`. + +When a proposal is submitted, it has to be accompagnied by a deposit that must be strictly positive but can be inferior to `MinDeposit`. Indeed, the submitter need not pay for the entire deposit on its own. If a proposal's deposit is strictly inferior to `MinDeposit`, other Atom holders can increase the proposal's deposit by sending a `TxGovDeposit` transaction. Once the proposals's deposit reaches `MinDeposit`, it enters voting period. ### Deposit refund There are two instances where Atom holders that deposited can claim back their deposit: - If the proposal is accepted -- If the proposal's deposit does not reach `MinDeposit` for a period longer than `mMxDepositPeriod` (initial value: 2 months). Then the proposal is considered closed and nobody can deposit on it anymore. +- If the proposal's deposit does not reach `MinDeposit` for a period longer than `MaxDepositPeriod` (initial value: 2 months). Then the proposal is considered closed and nobody can deposit on it anymore. In such instances, Atom holders that deposited can send a `TxGovClaimDeposit` transaction to retrieve their share of the deposit. @@ -52,7 +52,13 @@ These two categories are strictly identical except that `Urgent` proposals can b ### Participants -*Participants* are users that have the right to vote. On the Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and other users do not get the right to participate in governance. However, they can submit and deposit on proposals. +*Participants* are users that have the right to vote on proposals. On the Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and other users do not get the right to participate in governance. However, they can submit and deposit on proposals. + +Note that some *participants* can be forbidden to vote on a proposal under a certain validator if: +- *participant* bonded or unbonded Atoms to said validator after proposal entered voting period +- *participant* became validator after proposal entered voting period + +This does not prevent *participant* to vote with Atoms bonded to other validators. For example, if a *participant* bonded some Atoms to validator A before a proposal entered voting period and other Atoms to validator B after proposal entered voting period, only the vote under validator B will be forbidden. ### Voting period @@ -68,7 +74,7 @@ The initial option set includes the following options: - `NoWithVeto` - `Abstain` -`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` allows voters to signal that they do not intend to vote in favor or against the proposal but accept the result of the vote. +`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option allows voters to signal that they do not intend to vote in favor or against the proposal but accept the result of the vote. *Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ option that casts a `NoWithVeto` vote.* @@ -80,32 +86,32 @@ In the initial version of the governance module, there will be no quorum enforce ### Threshold -Threshold is defined as the minimum ratio of `Yes` votes to `No` votes for the proposal to be accepted. +Threshold is defined as the minimum proportion of `Yes` votes (excluding `Abstain` votes) for the proposal to be accepted. -Initially, the threshold is set at 50% with a possibility to veto if more than 1/3rd of votes (excluding `Abstain` votes) are `NoWithVeto` votes. This means that proposals are accepted is the ratio of `Yes` votes to `No` votes at the end of the voting period is superior to 50% and if the number of `NoWithVeto` votes is inferior to 1/3rd of total votes (excluding `Abstain`). +Initially, the threshold is set at 50% with a possibility to veto if more than 1/3rd of votes (excluding `Abstain` votes) are `NoWithVeto` votes. This means that proposals are accepted if the proportion of `Yes` votes (excluding `Abstain` votes) at the end of the voting period is superior to 50% and if the proportion of `NoWithVeto` votes is inferior to 1/3 (excluding `Abstain` votes). -`Urgent` proposals also work with the aforementioned threshold, except there is another condition that can accelerate the acceptance of the proposal. Namely, if the ratio of `Yes` votes to `InitTotalVotingPower` exceeds 2/3, `UrgentProposal` will be immediately accepted, even if the `Voting period` is not finished. `InitTotalVotingPower` is the total voting power of all bonded Atom holders at the moment when the vote opens. +`Urgent` proposals also work with the aforementioned threshold, except there is another condition that can accelerate the acceptance of the proposal. Namely, if the ratio of `Yes` votes to `InitTotalVotingPower` exceeds 2:3, `UrgentProposal` will be immediately accepted, even if the `Voting period` is not finished. `InitTotalVotingPower` is the total voting power of all bonded Atom holders at the moment when the vote opens. ### Inheritance If a delegator does not vote, it will inherit its validator vote. - If the delegator votes before its validator, it will not inherit from the validator's vote. -- If the delegator votes after its validaotor, it will override its validator vote with its own vote. If the proposal is a `Urgent` proposal, it is possible that the vote will close before delegators have a chance to react and override their validator's vote. This is not a problem, as `Urgent` proposals require more than 2/3rd of the total voting power to pass before the end of the voting period. If more than 2/3rd of validators collude, they can censor the votes of delegators anyway. +- If the delegator votes after its validator, it will override its validator vote with its own. If the proposal is a `Urgent` proposal, it is possible that the vote will close before delegators have a chance to react and override their validator's vote. This is not a problem, as `Urgent` proposals require more than 2/3rd of the total voting power to pass before the end of the voting period. If more than 2/3rd of validators collude, they can censor the votes of delegators anyway. ### Validator’s punishment for non-voting Validators are required to vote on all proposals to ensure that results have legitimacy. Voting is part of validators' directives and failure to do it will result in a penalty. -If a validator’s address is not in the list of addresses that voted on a proposal and if the vote is closed (i.e. `MinDeposit` was reached and `Voting period` is over), then this validator will automatically be partially slashed of `GovernancePenalty`. +If a validator’s address is not in the list of addresses that voted on a proposal and the vote is closed (i.e. `MinDeposit` was reached and `Voting period` is over), then the validator will automatically be partially slashed of `GovernancePenalty`. *Note: Need to define values for `GovernancePenalty`* -**Exception:** If a proposal is a `Urgent` proposal and is accepted via the special condition of having a ratio of `Yes` votes to `InitTotalVotingPower` that exceeds 2/3, validators cannot be punished for not having voted on it. That is because the proposal will close as soon as the ratio exceeds 2/3, making it mechanically impossible for some validators to vote on it. +**Exception:** If a proposal is a `Urgent` proposal and is accepted via the special condition of having a ratio of `Yes` votes to `InitTotalVotingPower` that exceeds 2:3, validators cannot be punished for not having voted on it. That is because the proposal will close as soon as the ratio exceeds 2:3, making it mechanically impossible for some validators to vote on it. ### Governance key and governance address -Validators can make use of an additional slot where they can designate a `Governance PubKey`. By default, a validator's `Governance PubKey` will be the same as its main PubKey. Validators can change this `Governance PubKey` by sending a `Change Governance PubKey` transaction signed by their main `Consensus PubKey`. From there, they will be able to sign vote using the `Governance PrivKey` associated with their `Governance PubKey`. The `Governance PubKey` can be changed at any moment. +Validators can make use of a slot where they can designate a `Governance PubKey`. By default, a validator's `Governance PubKey` will be the same as its main PubKey. Validators can change this `Governance PubKey` by sending a `Change Governance PubKey` transaction signed by their main `Consensus PrivKey`. From there, they will be able to sign votes using the `Governance PrivKey` associated with their `Governance PubKey`. The `Governance PubKey` can be changed at any moment. ## Software Upgrade @@ -129,29 +135,124 @@ Once a block contains more than 2/3rd *precommits* where a common `SoftwareUpgra *Disclaimer: This is a suggestion. Only structs and pseudocode. Actual logic and implementation might widely differ* -### Procedures +### State + +#### Procedures `Procedures` define the rule according to which votes are run. There can only be one active procedure at any given time. If governance wants to change a procedure, either to modify a value or add/remove a parameter, a new procedure has to be created and the previous one rendered inactive. ```Go type Procedure struct { - VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks - MinDeposit int64 // Minimum deposit for a proposal to enter voting period. + VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks + MinDeposit int64 // Minimum deposit for a proposal to enter voting period. OptionSet []string // Options available to voters. {Yes, No, NoWithVeto, Abstain} ProposalTypes []string // Types available to submitters. {PlainTextProposal, SoftwareUpgradeProposal} - Threshold int64 // Minimum value of Yes votes to No votes ratio for proposal to pass. Initial value: 0.5 + Threshold rational.Rational // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5 Veto rational.Rational // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 - MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months - GovernancePenalty int64 // Penalty if validator does not vote + MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months + GovernancePenalty int64 // Penalty if validator does not vote - ProcedureNumber int16 // Incremented each time a new procedure is created IsActive bool // If true, procedure is active. Only one procedure can have isActive true. } ``` -### Proposals +**Store**: +- `Procedures`: a mapping `map[int16]Procedure` of procedures indexed by their `ProcedureNumber` +- `ActiveProcedureNumber`: returns current procedure number + +#### Proposals + +`Proposals` are item to be voted on. + +```Go +type Proposal struct { + Title string // Title of the proposal + Description string // Description of the proposal + Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + Category bool // false=regular, true=urgent + Deposit int64 // Current deposit on this proposal. Initial value is set at InitialDeposit + SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included + + VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached + InitTotalVotingPower int64 // Total voting power when proposal enters voting period (default 0) + InitProcedureNumber int16 // Procedure number of the active procedure when proposal enters voting period (default -1) + Votes map[string]int64 // Votes for each option (Yes, No, NoWithVeto, Abstain) +} +``` -`Proposals` are item to be voted on. They can be submitted by any Atom holder via a `TxGovSubmitProposal` transaction. +We also introduce a type `ValidatorGovInfo` + +```Go +type ValidatorGovInfo struct { + InitVotingPower int64 // Voting power of validator when proposal enters voting period + Minus int64 // Minus of validator, used to compute validator's voting power +} +``` + +**Store:** + +- `Proposals`: A mapping `map[int64]Proposal` of proposals indexed by their `proposalID` +- `Deposits`: A mapping `map[[]byte]int64` of deposits indexed by `:` as `[]byte`. Given a `proposalID` and a `PubKey`, returns deposit (`nil` if `PubKey` has not deposited on the proposal) +- `Options`: A mapping `map[[]byte]string` of options indexed by `::` as `[]byte`. Given a `proposalID`, a `PubKey` and a validator's `PubKey`, returns option chosen by this `PubKey` for this validator (`nil` if `PubKey` has not voted under this validator) +- `ValidatorGovInfos`: A mapping `map[[]byte]ValidatorGovInfo` of validator's governance infos indexed by `:`. Returns `nil` if proposal has not entered voting period or if `PubKey` was not the governance public key of a validator when proposal entered voting period. + + +#### Proposal Processing Queue + +**Store:** +- `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the `ProposalIDs` of proposals that reached `MinDeposit`. Each round, the oldest element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, then the application checks if validators in `InitVotingPowerList` have voted and, if not, applies `GovernancePenalty`. After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. Note that if a proposal is urgent and accepted under the special condition, its `ProposalID` must be ejected from `ProposalProcessingQueue`. + +And the pseudocode for the `ProposalProcessingQueue`: + +``` + in BeginBlock do + + checkProposal() // First call of the recursive function + + + // Recursive function. First call in BeginBlock + func checkProposal() + if (ProposalProcessingQueue.Peek() == nil) + return + + else + proposalID = ProposalProcessingQueue.Peek() + proposal = load(store, Proposals, proposalID) + initProcedure = load(store, Procedures, proposal.InitProcedureNumber) + + if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) + + // proposal was urgent and accepted under the special condition + // no punishment + + ProposalProcessingQueue.pop() + checkProposal() + + else if (CurrentBlock == proposal.VotingStartBlock + initProcedure.VotingPeriod) + + activeProcedure = load(store, Procedures, ActiveProcedureNumber) + + for each validator in CurrentBondedValidators + validatorGovInfo = load(store, ValidatorGovInfos, validator.GovPubKey) + + if (validatorGovInfo.InitVotingPower != nil) + // validator was bonded when vote started + + validatorOption = load(store, Options, validator.GovPubKey) + if (validatorOption == nil) + // validator did not vote + slash validator by activeProcedure.GovernancePenalty + + ProposalProcessingQueue.pop() + checkProposal() +``` + + +### Transactions + +#### Proposal Submission + +Proposals can be submitted by any Atom holder via a `TxGovSubmitProposal` transaction. ```Go type TxGovSubmitProposal struct { @@ -161,30 +262,17 @@ type TxGovSubmitProposal struct { Category bool // false=regular, true=urgent InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive. } - -type Proposal struct { - Title string // Title of the proposal - Description string // Description of the proposal - Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} - Category bool // false=regular, true=urgent - Deposit int64 // Current deposit on this proposal. Initial value is set at InitialDeposit - SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included - VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached. - Votes map[string]int64 // Votes for each option (Yes, No, NoWithVeto, Abstain) -} ``` -Each `Proposal` is identified by its unique `proposalID`. - -Additionaly, four lists will be linked to each proposal: -- `DepositorList`: List of addresses that deposited on the proposal with their associated deposit -- `VotersList`: List of addresses that voted **under each validator** with their associated option -- `InitVotingPowerList`: Snapshot of validators' voting power **when proposal enters voting period** (only saves validators whose voting power is >0). -- `MinusesList`: List of minuses for each validator. Used to compute validators' voting power when they cast a vote. - -Two final parameters, `InitTotalVotingPower` and `InitProcedureNumber` associated with `proposalID` will be saved when proposal enters voting period. - -We also introduce `ProposalProcessingQueue` which lists all the `ProposalIDs` of proposals that reached `MinDeposit` from oldest to newest. Each round, the oldest element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, then the application checks if validators in `InitVotingPowerList` have voted and, if not, applies `GovernancePenalty`. After that proposal is ejected from `ProposalProcessingQueue` and the new first element of the queue is evaluated. Note that if a proposal is urgent and accepted under the special condition, its `ProposalID` must be ejected from `ProposalProcessingQueue`. +**State modifications:** +- Generate new `proposalID` +- Create new `Proposal` +- Initialise `Proposals` attributes +- Store sender's deposit in `Deposits` +- Decrease balance of sender by `InitialDeposit` +- If `MinDeposit` is reached: + - Push `proposalID` in `ProposalProcessingQueueEnd` + - Store each validator's voting power in `ValidatorGovInfos` A `TxGovSubmitProposal` transaction can be handled according to the following pseudocode @@ -193,9 +281,10 @@ A `TxGovSubmitProposal` transaction can be handled according to the following ps // Check if TxGovSubmitProposal is valid. If it is, create proposal // upon receiving txGovSubmitProposal from sender do - // check if proposal is correctly formatted. Includes fee payment. if !correctlyFormatted(txGovSubmitProposal) then + // check if proposal is correctly formatted. Includes fee payment. + throw else @@ -205,10 +294,10 @@ upon receiving txGovSubmitProposal from sender do throw else - sender.AtomBalance -= InitialDeposit + sender.AtomBalance -= txGovSubmitProposal.InitialDeposit proposalID = generate new proposalID - proposal = create new Proposal from proposalID + proposal = NewProposal() proposal.Title = txGovSubmitProposal.Title proposal.Description = txGovSubmitProposal.Description @@ -217,79 +306,59 @@ upon receiving txGovSubmitProposal from sender do proposal.Deposit = txGovSubmitProposal.InitialDeposit proposal.SubmitBlock = CurrentBlock - create depositorsList from proposalID - initiate deposit of sender in depositorsList at txGovSubmitProposal.InitialDeposit + store(Deposits, :, txGovSubmitProposal.InitialDeposit) + activeProcedure = load(store, Procedures, ActiveProcedureNumber) - if (txGovSubmitProposal.InitialDeposit < ActiveProcedure.MinDeposit) then + if (txGovSubmitProposal.InitialDeposit < activeProcedure.MinDeposit) then // MinDeposit is not reached proposal.VotingStartBlock = -1 + proposal.InitTotalVotingPower = 0 + proposal.InitProcedureNumber = -1 else // MinDeposit is reached proposal.VotingStartBlock = CurrentBlock + proposal.InitTotalVotingPower = TotalVotingPower + proposal.InitProcedureNumber = ActiveProcedureNumber - create votersList, - initVotingPowerList, - minusesList, - initProcedureNumber, - initTotalVotingPower from proposalID - - snapshot(ActiveProcedure.ProcedureNumber) // Save current procedure number in initProcedureNumber - snapshot(TotalVotingPower) // Save total voting power in initTotalVotingPower - snapshot(ValidatorVotingPower) // Save validators' voting power in initVotingPowerList + for each validator in CurrentBondedValidators + // Store voting power of each bonded validator + + validatorGovInfo = NewValidatorGovInfo() + validatorGovInfo.InitVotingPower = validator.VotingPower + validatorGovInfo.Minus = 0 + + store(ValidatorGovInfos, :, validatorGovInfo) - ProposalProcessingQueueEnd++ - ProposalProcessingQueue[ProposalProcessingQueueEnd] = proposalID + ProposalProcessingQueue.push(proposalID) + store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping return proposalID ``` -And the pseudocode for the `ProposalProcessingQueue`: -``` - in BeginBlock do - - checkProposal() - - - - func checkProposal() - if (ProposalProcessingQueueBeginning == ProposalProcessingQueueEnd) - return - - else - retrieve proposalID from ProposalProcessingQueue[ProposalProcessingQueueBeginning] - retrieve proposal from proposalID - retrieve initProcedureNumber from proposalID - retrieve initProcedure from initProcedureNumber - - if (CurrentBlock == proposal.VotingStartBlock + initProcedure.VotingPeriod) - retrieve initVotingPowerList from proposalID - retrieve votersList from proposalID - retrieve validators from initVotingPowerList - - for each validator in validators - if validator is not in votersList - slash validator by ActiveProcedure.GovernancePenalty - ProposalProcessingQueueBeginning++ // ProposalProcessingQueue will have a new element - checkProposal() - - else - return -``` +#### Deposit Once a proposal is submitted, if `Proposal.Deposit < ActiveProcedure.MinDeposit`, Atom holders can send `TxGovDeposit` transactions to increase the proposal's deposit. ```Go type TxGovDeposit struct { - ProposalID int64 // ID of the proposal - Deposit int64 // Number of Atoms to add to the proposal's deposit + ProposalID int64 // ID of the proposal + Deposit int64 // Number of Atoms to add to the proposal's deposit } ``` +**State modifications:** +- Decrease balance of sender by `deposit` +- Initialize or increase `deposit` of sender in `Deposits` +- Increase `proposal.Deposit` by sender's `deposit` +- If `MinDeposit` is reached: + - Push `proposalID` in `ProposalProcessingQueueEnd` + - Store each validator's voting power in `ValidatorGovInfos` + A `TxGovDeposit` transaction has to go through a number of checks to be valid. These checks are outlined in the following pseudocode. ``` @@ -303,27 +372,28 @@ upon receiving txGovDeposit from sender do throw else - if !exist(txGovDeposit.proposalID) then + proposal = load(store, Proposals, txGovDeposit.ProposalID) + + if (proposal == nil) then // There is no proposal for this proposalID throw else - if (txGovDeposit.Deposit <= 0 OR sender.AtomBalance < txGovDeposit.Deposit) + if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit) // deposit is negative or null OR sender has insufficient funds throw - else - retrieve proposal from txGovDeposit.ProposalID // retrieve throws if it fails - - if (proposal.Deposit >= ActiveProcedure.MinDeposit) then + else + activeProcedure = load(store, Procedures, ActiveProcedureNumber) + if (proposal.Deposit >= activeProcedure.MinDeposit) then // MinDeposit was reached throw else - if (CurrentBlock >= proposal.SubmitBlock + ActiveProcedure.MaxDepositPeriod) then + if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then // Maximum deposit period reached throw @@ -331,36 +401,43 @@ upon receiving txGovDeposit from sender do else // sender can deposit - retrieve depositorsList from txGovDeposit.ProposalID sender.AtomBalance -= txGovDeposit.Deposit + deposit = load(store, Deposits, :) - if sender is in depositorsList - increase deposit of sender in depositorsList by txGovDeposit.Deposit + if (deposit == nil) + // sender has never deposited on this proposal + + store(Deposits, :, deposit) else - initialise deposit of sender in depositorsList at txGovDeposit.Deposit - + // sender has already deposited on this proposal + + newDeposit = deposit + txGovDeposit.Deposit + store(Deposits, :, newDeposit) + proposal.Deposit += txGovDeposit.Deposit - if (proposal.Deposit >= ActiveProcedure.MinDeposit) then + if (proposal.Deposit >= activeProcedure.MinDeposit) then // MinDeposit is reached, vote opens proposal.VotingStartBlock = CurrentBlock + proposal.InitTotalVotingPower = TotalVotingPower + proposal.InitProcedureNumber = ActiveProcedureNumber - create votersList, - initVotingPowerList, - minusesList, - initProcedureNumber, - initTotalVotingPower from proposalID - - snapshot(ActiveProcedure.ProcedureNumber) // Save current procedure number in InitProcedureNumber - snapshot(TotalVotingPower) // Save total voting power in InitTotalVotingPower - snapshot(ValidatorVotingPower) // Save validators' voting power in InitVotingPowerList + for each validator in CurrentBondedValidators + // Store voting power of each bonded validator + + validatorGovInfo = NewValidatorGovInfo() + validatorGovInfo.InitVotingPower = validator.VotingPower + validatorGovInfo.Minus = 0 + + store(ValidatorGovInfos, :, validatorGovInfo) - ProposalProcessingQueueEnd++ // ProposalProcessingQueue will have a new element - ProposalProcessingQueue[ProposalProcessingQueueEnd] = txGovDeposit.ProposalID + ProposalProcessingQueue.push(txGovDeposit.ProposalID) ``` +#### Claiming deposit + Finally, if the proposal is accepted or `MinDeposit` was not reached before the end of the `MaximumDepositPeriod`, then Atom holders can send `TxGovClaimDeposit` transaction to claim their deposits. ```Go @@ -369,6 +446,11 @@ Finally, if the proposal is accepted or `MinDeposit` was not reached before the } ``` +**State modifications:** +If conditions are met, reimburse the deposit, i.e. +- Increase `AtomBalance` of sender by `deposit` +- Set `deposit` of sender in `DepositorsList` to 0 + And the associated pseudocode ``` @@ -382,66 +464,63 @@ And the associated pseudocode throw else - if !exists(txGovClaimDeposit.ProposalID) then + proposal = load(store, Proposals, txGovDeposit.ProposalID) + + if (proposal == nil) then // There is no proposal for this proposalID throw else - retrieve depositorsList from txGovClaimDeposit.ProposalID + deposit = load(store, Deposits, :) - - if sender is not in depositorsList then + if (deposit == nil) + // sender has not deposited on this proposal + throw - else - retrieve deposit from sender in depositorsList - - if deposit <= 0 + else + if (deposit <= 0) // deposit has already been claimed throw - else - retrieve proposal from txGovClaimDeposit.ProposalID - - if proposal.VotingStartBlock <= 0 + else + if (proposal.VotingStartBlock <= 0) // Vote never started - - if (CurrentBlock <= proposal.SubmitBlock + ActiveProcedure.MaxDepositPeriod) + + activeProcedure = load(store, Procedures, ActiveProcedureNumber) + if (CurrentBlock <= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) // MaxDepositPeriod is not reached throw else // MaxDepositPeriod is reached - - set deposit of sender in depositorsList to 0 + // Set sender's deposit to 0 and refund + + store(Deposits, :, 0) sender.AtomBalance += deposit else // Vote started + + initProcedure = load(store, Procedures, proposal.InitProcedureNumber) - retrieve initTotalVotingPower from txGovClaimDeposit.ProposalID - retrieve initProcedureNumber from txGovClaimDeposit.ProposalID - retrieve initProcedure from initProcedureNumber // get procedure that was active when vote opened - - if (proposal.Category AND proposal.Votes['Yes']/initTotalVotingPower >= 2/3) OR - ((CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) AND (proposal.Votes['NoWithVeto']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) < 1/3) AND (proposal.Votes['Yes']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) > 1/2)) then + if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) OR + ((CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) AND (proposal.Votes['NoWithVeto']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) < 1/3) AND (proposal.Votes['Yes']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) > 1/2)) then // Proposal was accepted either because // Proposal was urgent and special condition was met // Voting period ended and vote satisfies threshold - set deposit of sender in depositorsList to 0 + store(Deposits, :, 0) sender.AtomBalance += deposit - - else - throw + ``` -### Vote +#### Vote Once `ActiveProcedure.MinDeposit` is reached, voting period starts. From there, bonded Atom holders are able to send `TxGovVote` transactions to cast their vote on the proposal. @@ -453,9 +532,17 @@ Once `ActiveProcedure.MinDeposit` is reached, voting period starts. From there, } ``` +**State modifications:** +- If sender is not a validator and validator has not voted, initialize or increase minus of validator by sender's `voting power` +- If sender is not a validator and validator has voted, decrease `proposal.Votes['validatorOption']` by sender's `voting power` +- If sender is not a validator, increase `[proposal.Votes['txGovVote.Option']` by sender's `voting power` +- If sender is a validator, increase `proposal.Votes['txGovVote.Option']` by validator's `InitialVotingPower - minus` (`minus` can be equal to 0) + Votes need to be tied to a validator in order to compute validator's voting power. If a delegator is bonded to multiple validators, it will have to send one transaction per validator (the UI should facilitate this so that multiple transactions can be sent in one "vote flow"). If the sender is the validator itself, then it will input its own GovernancePubKey as `ValidatorPubKey` + + Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled: ``` @@ -469,19 +556,19 @@ Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled throw else - if !exists(txGovVote.proposalID) OR - - // Throws if - // proposalID does not exist - + proposal = load(store, Proposals, txGovDeposit.ProposalID) + + if (proposal == nil) then + // There is no proposal for this proposalID + throw else - retrieve initProcedureNumber from txGovVote.ProposalID - retrieve initProcedure from initProcedureNumber // get procedure that was active when vote opened + initProcedure = load(store, Procedures, proposal.InitProcedureNumber) // get procedure that was active when vote opened + validator = load(store, Validators, txGovVote.ValidatorPubKey) if !initProcedure.OptionSet.includes(txGovVote.Option) OR - !isValid(txGovVote.ValidatorPubKey) then + (validator == nil) then // Throws if // Option is not in Option Set of procedure that was active when vote opened OR if @@ -490,22 +577,19 @@ Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled throw else - retrieve votersList from txGovVote.ProposalID + option = load(store, Options, ::) - if sender is in votersList under txGovVote.ValidatorPubKey then + if (option != nil) // sender has already voted with the Atoms bonded to ValidatorPubKey throw else - retrieve proposal from txGovVote.ProposalID - retrieve InitTotalVotingPower from txGovVote.ProposalID - if (proposal.VotingStartBlock < 0) OR (CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) OR (proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.ValidatorPubKey) OR (proposal.VotingStartBlock < lastUnbondingBlock(sender, txGovVote.ValidatorPubKey) OR - (proposal.Category AND proposal.Votes['Yes']/InitTotalVotingPower >= 2/3) then + (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) then // Throws if // Vote has not started OR if @@ -517,66 +601,51 @@ Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled throw else - // sender can vote, check if sender == validator and add sender to voter list - - add sender to votersList under txGovVote.ValidatorPubKey - - if (sender is not equal to GovPubKey that corresponds to txGovVote.ValidatorPubKey) - // Here, sender is not the Governance PubKey of the validator whose PubKey is txGovVote.ValidatorPubKey + validatorGovInfo = load(store, ValidatorGovInfos, :) - if sender does not have bonded Atoms to txGovVote.ValidatorPubKey then - throw + if (validatorGovInfo == nil) + // validator became validator after proposal entered voting period - else - if txGovVote.ValidatorPubKey is not in votersList under txGovVote.ValidatorPubKey then - // Validator has not voted already + throw - if exists(MinusesList[txGovVote.ValidatorPubKey]) then - // a minus already exists for this validator's PubKey, increase minus - // by the amount of Atoms sender has bonded to ValidatorPubKey + else + // sender can vote, check if sender == validator and store sender's option in Options + + store(Options, ::, txGovVote.Option) - MinusesList[txGovVote.ValidatorPubKey] += sender.bondedAmountTo(txGovVote.ValidatorPubKey) + if (sender != validator.GovPubKey) + // Here, sender is not the Governance PubKey of the validator whose PubKey is txGovVote.ValidatorPubKey - else - // a minus does not already exist for this validator's PubKey, initialise minus - // at the amount of Atoms sender has bonded to ValidatorPubKey + if sender does not have bonded Atoms to txGovVote.ValidatorPubKey then + // check in Staking module - MinusesList[txGovVote.ValidatorPubKey] = sender.bondedAmountTo(txGovVote.ValidatorPubKey) + throw else - // Validator has already voted - // Reduce option count chosen by validator by sender's bonded Amount + validatorOption = load(store, Options, :::, validatorGovInfo) - else - // sender is the Governance PubKey of the validator whose main PubKey is txGovVote.ValidatorPubKey - // i.e. sender == validator + else + // Validator has already voted + // Reduce votes of option chosen by validator by sender's bonded Amount - retrieve initialVotingPower from InitVotingPowerList using txGovVote.ValidatorPubKey - - - if exists(MinusesList[txGovVote.ValidatorPubKey]) then - // a minus exists for this validator's PubKey, decrease vote of validator by minus + proposal.Votes['validatorOption'] -= sender.bondedAmountTo(txGovVote.ValidatorPubKey) - proposal.Votes['txGovVote.Option'] += (initialVotingPower - MinusesList[txGovVote.ValidatorPubKey]) + // increase votes of option chosen by sender by bonded Amount + proposal.Votes['txGovVote.Option'] += sender.bondedAmountTo(txGovVote.ValidatorPubKey) - else - // a minus does not exist for this validator's PubKey, validator votes with full voting power + else + // sender is the Governance PubKey of the validator whose main PubKey is txGovVote.ValidatorPubKey + // i.e. sender == validator + + proposal.Votes['txGovVote.Option'] += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus) - proposal.Votes['txGovVote.Option'] += initialVotingPower - - if (proposal.Category AND proposal.Votes['Yes']/InitTotalVotingPower >= 2/3) - // after vote is counted, if proposal is urgent and special condition is met - // remove proposalID from ProposalProcessingQueue - - remove txGovVote.ProposalID from ProposalProcessingQueue - Rearrange ProposalProcessingQueue - ProposalProcessingQueueEnd-- + ``` diff --git a/docs/spec/governance/overview.md b/docs/spec/governance/overview.md new file mode 100644 index 000000000000..53f7d7b08191 --- /dev/null +++ b/docs/spec/governance/overview.md @@ -0,0 +1,192 @@ +# Design Overview + +*Disclaimer: This is work in progress. Mechanisms are susceptible to change.* + +The governance process is divided in a few steps that are outlined below: + +* **Proposal submission:** Proposal is submitted to the blockchain with a + deposit. +* **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is + confirmed and vote opens. Bonded Atom holders can then send `TxGovVote` + transactions to vote on the proposal. +* If the proposal involves a software upgrade: + * **Signal:** Validators start signaling that they are ready to switch to the + new version. + * **Switch:** Once more than 75% of validators have signaled that they are + ready to switch, their software automatically flips to the new version. + +## Proposal submission + +### Right to submit a proposal + +Any Atom holder, whether bonded or unbonded, can submit proposals by sending a +`TxGovProposal` transaction. Once a proposal is submitted, it is identified by +its unique `proposalID`. + +### Proposal filter (minimum deposit) + +To prevent spam, proposals must be submitted with a deposit in Atoms. Voting +period will not start as long as the proposal's deposit is smaller than the +minimum deposit `MinDeposit`. + +When a proposal is submitted, it has to be accompanied by a deposit that must +be strictly positive but can be inferior to `MinDeposit`. Indeed, the submitter +need not pay for the entire deposit on its own. If a proposal's deposit is +strictly inferior to `MinDeposit`, other Atom holders can increase the +proposal's deposit by sending a `TxGovDeposit` transaction. Once the proposal's deposit reaches `MinDeposit`, it enters voting period. + +If proposal's deposit does not reach `MinDeposit` before `MaxDepositPeriod`, proposal closes and nobody can deposit on it anymore. + +### Deposit refund + +There is one instance where Atom holders that deposits can be refunded: +* If the proposal is accepted. + +Then, deposits will automatically be refunded to their respective depositer. + +### Proposal types + +In the initial version of the governance module, there are two types of +proposal: +* `PlainTextProposal` All the proposals that do not involve a modification of + the source code go under this type. For example, an opinion poll would use a + proposal of type `PlainTextProposal`. +* `SoftwareUpgradeProposal`. If accepted, validators are expected to update + their software in accordance with the proposal. They must do so by following + a 2-steps process described in the [Software Upgrade](#software-upgrade) + section below. Software upgrade roadmap may be discussed and agreed on via + `PlainTextProposals`, but actual software upgrades must be performed via + `SoftwareUpgradeProposals`. + + +## Vote + +### Participants + +*Participants* are users that have the right to vote on proposals. On the +Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and +other users do not get the right to participate in governance. However, they +can submit and deposit on proposals. + +Note that some *participants* can be forbidden to vote on a proposal under a +certain validator if: +* *participant* bonded or unbonded Atoms to said validator after proposal + entered voting period. +* *participant* became validator after proposal entered voting period. + +This does not prevent *participant* to vote with Atoms bonded to other +validators. For example, if a *participant* bonded some Atoms to validator A +before a proposal entered voting period and other Atoms to validator B after +proposal entered voting period, only the vote under validator B will be +forbidden. + +### Voting period + +Once a proposal reaches `MinDeposit`, it immediately enters `Voting period`. We +define `Voting period` as the interval between the moment the vote opens and +the moment the vote closes. `Voting period` should always be shorter than +`Unbonding period` to prevent double voting. The initial value of +`Voting period` is 2 weeks. + +### Option set + +The option set of a proposal refers to the set of choices a participant can +choose from when casting its vote. + +The initial option set includes the following options: +- `Yes` +- `No` +- `NoWithVeto` +- `Abstain` + +`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option +allows voters to signal that they do not intend to vote in favor or against the +proposal but accept the result of the vote. + +*Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ +option that casts a `NoWithVeto` vote.* + +### Quorum + +Quorum is defined as the minimum percentage of voting power that needs to be +casted on a proposal for the result to be valid. + +In the initial version of the governance module, there will be no quorum +enforced by the protocol. Participation is ensured via the combination of +inheritance and validator's punishment for non-voting. + +### Threshold + +Threshold is defined as the minimum proportion of `Yes` votes (excluding +`Abstain` votes) for the proposal to be accepted. + +Initially, the threshold is set at 50% with a possibility to veto if more than +1/3rd of votes (excluding `Abstain` votes) are `NoWithVeto` votes. This means +that proposals are accepted if the proportion of `Yes` votes (excluding +`Abstain` votes) at the end of the voting period is superior to 50% and if the +proportion of `NoWithVeto` votes is inferior to 1/3 (excluding `Abstain` +votes). + +Proposals can be accepted before the end of the voting period if they meet a special condtion. Namely, if the ratio of `Yes` votes to `InitTotalVotingPower`exceeds 2:3, the proposal will be immediately accepted, even if the `Voting period` is not finished. `InitTotalVotingPower` is the total voting power of all bonded Atom holders at the moment when the vote opens. +This condition exists so that the network can react quickly in case of urgency. + +### Inheritance + +If a delegator does not vote, it will inherit its validator vote. + +* If the delegator votes before its validator, it will not inherit from the + validator's vote. +* If the delegator votes after its validator, it will override its validator + vote with its own. If the proposal is urgent, it is possible + that the vote will close before delegators have a chance to react and + override their validator's vote. This is not a problem, as proposals require more than 2/3rd of the total voting power to pass before the end of the voting period. If more than 2/3rd of validators collude, they can censor the votes of delegators anyway. + +### Validator’s punishment for non-voting + +Validators are required to vote on all proposals to ensure that results have +legitimacy. Voting is part of validators' directives and failure to do it will +result in a penalty. + +If a validator’s address is not in the list of addresses that voted on a +proposal and the vote is closed (i.e. `MinDeposit` was reached and `Voting +period` is over), then the validator will automatically be partially slashed by +`GovernancePenalty`. + +*Note: Need to define values for `GovernancePenalty`* + +**Exception:** If a proposal is accepted via the special condition of having a ratio of `Yes` votes to `InitTotalVotingPower` that exceeds 2:3, validators cannot be punished for not having voted on it. +That is because the proposal will close as soon as the ratio exceeds 2:3, +making it mechanically impossible for some validators to vote on it. + +### Governance address + +Later, we may add permissionned keys that could only sign txs from certain modules. For the MVP, the `Governance address` will be the main validator address generated at account creation. This address corresponds to a different PrivKey than the Tendermint PrivKey which is responsible for signing consensus messages. Validators thus do not have to sign governance transactions with the sensitive Tendermint PrivKey. + +## Software Upgrade + +If proposals are of type `SoftwareUpgradeProposal`, then nodes need to upgrade +their software to the new version that was voted. This process is divided in +two steps. + +### Signal + +After a `SoftwareUpgradeProposal` is accepted, validators are expected to +download and install the new version of the software while continuing to run +the previous version. Once a validator has downloaded and installed the +upgrade, it will start signaling to the network that it is ready to switch by +including the proposal's `proposalID` in its *precommits*.(*Note: Confirmation +that we want it in the precommit?*) + +Note: There is only one signal slot per *precommit*. If several +`SoftwareUpgradeProposals` are accepted in a short timeframe, a pipeline will +form and they will be implemented one after the other in the order that they +were accepted. + +### Switch + +Once a block contains more than 2/3rd *precommits* where a common +`SoftwareUpgradeProposal` is signaled, all the nodes (including validator +nodes, non-validating full nodes and light-nodes) are expected to switch to the +new version of the software. + +*Note: Not clear how the flip is handled programatically* \ No newline at end of file diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md new file mode 100644 index 000000000000..b6f0d3a61c0a --- /dev/null +++ b/docs/spec/governance/state.md @@ -0,0 +1,182 @@ +# Implementation (1/2) + +## State + +### Procedures + +`Procedures` define the rule according to which votes are run. There can only +be one active procedure at any given time. If governance wants to change a +procedure, either to modify a value or add/remove a parameter, a new procedure +has to be created and the previous one rendered inactive. + +```go +type Procedure struct { + VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks + MinDeposit int64 // Minimum deposit for a proposal to enter voting period. + OptionSet []string // Options available to voters. {Yes, No, NoWithVeto, Abstain} + ProposalTypes []string // Types available to submitters. {PlainTextProposal, SoftwareUpgradeProposal} + Threshold rational.Rational // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5 + Veto rational.Rational // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 + MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months + GovernancePenalty int64 // Penalty if validator does not vote + + IsActive bool // If true, procedure is active. Only one procedure can have isActive true. +} +``` + +The current active procedure is stored in a global `params` KVStore. + +### Deposit + +```go + type Deposit struct { + Amount sdk.Coins // sAmount of coins deposited by depositer + Depositer crypto.address // Address of depositer + } +``` + +### Votes + +```go + type Votes struct { + YesVotes int64 + NoVote int64 + NoWithVetoVotes int64 + AbstainVotes int64 + } +``` + + +### Proposals + +`Proposals` are item to be voted on. + +```go +type Proposal struct { + Title string // Title of the proposal + Description string // Description of the proposal + Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + TotalDeposit sdk.Coins // Current deposit on this proposal. Initial value is set at InitialDeposit + Deposits []Deposit // List of deposits on the proposal + SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included + Submitter crypto.address // Address of the submitter + + VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached + InitTotalVotingPower int64 // Total voting power when proposal enters voting period (default 0) + InitProcedure Procedure // Active Procedure when proposal enters voting period + + Votes Votes // Total votes for each option +} +``` + +We also introduce a type `ValidatorGovInfo` + +```go +type ValidatorGovInfo struct { + InitVotingPower int64 // Voting power of validator when proposal enters voting period + Minus int64 // Minus of validator, used to compute validator's voting power +} +``` + +### Stores + +*Stores are KVStores in the multistore. The key to find the store is the first parameter in the list* + + +* `Proposals`: A mapping `map[int64]Proposal` of proposals indexed by their + `proposalID` +* `Options`: A mapping `map[[]byte]string` of options indexed by + `::` as `[]byte`. Given a + `proposalID`, an `address` and a validator's `address`, returns option chosen by this `address` for this validator (`nil` if `address` has not voted under this validator) +* `ValidatorGovInfos`: A mapping `map[[]byte]ValidatorGovInfo` of validator's + governance infos indexed by `:`. Returns + `nil` if proposal has not entered voting period or if `address` was not the + address of a validator when proposal entered voting period. + +For pseudocode purposes, here are the two function we will use to read or write in stores: + +* `load(StoreKey, Key)`: Retrieve item stored at key `Key` in store found at key `StoreKey` in the multistore +* `store(StoreKey, Key, value)`: Write value `Value` at key `Key` in store found at key `StoreKey` in the multistore + +### Proposal Processing Queue + +**Store:** +* `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the + `ProposalIDs` of proposals that reached `MinDeposit`. Each round, the oldest + element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if + `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, + then the application checks if validators in `InitVotingPowerList` have voted + and, if not, applies `GovernancePenalty`. If the proposal is accepted, deposits are refunded. + After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. + Note that if a proposal is accepted under the special condition, + its `ProposalID` must be ejected from `ProposalProcessingQueue`. + +And the pseudocode for the `ProposalProcessingQueue`: + +```go + in BeginBlock do + + checkProposal() // First call of the recursive function + + + // Recursive function. First call in BeginBlock + func checkProposal() + if (ProposalProcessingQueue.Peek() == nil) + return + + else + proposalID = ProposalProcessingQueue.Peek() + proposal = load(Proposals, proposalID) + + if (proposal.Votes.YesVotes/proposal.InitTotalVotingPower >= 2/3) + + // proposal was urgent and accepted under the special condition + // no punishment + // refund deposits + + ProposalProcessingQueue.pop() + + newDeposits = new []Deposits + + for each (amount, depositer) in proposal.Deposits + newDeposits.append[{0, depositer}] + depositer.AtomBalance += amount + + proposal.Deposits = newDeposits + store(Proposals, , proposal) + + checkProposal() + + else if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod) + + ProposalProcessingQueue.pop() + activeProcedure = load(params, 'ActiveProcedure') + + for each validator in CurrentBondedValidators + validatorGovInfo = load(ValidatorGovInfos, :) + + if (validatorGovInfo.InitVotingPower != nil) + // validator was bonded when vote started + + validatorOption = load(Options, :) + if (validatorOption == nil) + // validator did not vote + slash validator by activeProcedure.GovernancePenalty + + + if((proposal.Votes.YesVotes/(proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes)) > 0.5 AND (proposal.Votes.NoWithVetoVotes/(proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes) < 1/3)) + + // proposal was accepted at the end of the voting period + // refund deposits + + newDeposits = new []Deposits + + for each (amount, depositer) in proposal.Deposits + newDeposits.append[{0, depositer}] + depositer.AtomBalance += amount + + proposal.Deposits = newDeposits + store(Proposals, , proposal) + + checkProposal() +``` \ No newline at end of file diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md new file mode 100644 index 000000000000..3e0af9ed666d --- /dev/null +++ b/docs/spec/governance/transactions.md @@ -0,0 +1,327 @@ +# Implementation (2/2) + +## Transactions + +### Proposal Submission + +Proposals can be submitted by any Atom holder via a `TxGovSubmitProposal` +transaction. + +```go +type TxGovSubmitProposal struct { + Title string // Title of the proposal + Description string // Description of the proposal + Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive. +} +``` + +**State modifications:** +* Generate new `proposalID` +* Create new `Proposal` +* Initialise `Proposals` attributes +* Decrease balance of sender by `InitialDeposit` +* If `MinDeposit` is reached: + * Push `proposalID` in `ProposalProcessingQueueEnd` + * Store each validator's voting power in `ValidatorGovInfos` + +A `TxGovSubmitProposal` transaction can be handled according to the following +pseudocode. + +```go +// PSEUDOCODE // +// Check if TxGovSubmitProposal is valid. If it is, create proposal // + +upon receiving txGovSubmitProposal from sender do + + if !correctlyFormatted(txGovSubmitProposal) then + // check if proposal is correctly formatted. Includes fee payment. + + throw + + else + if (txGovSubmitProposal.InitialDeposit <= 0) OR (sender.AtomBalance < InitialDeposit) then + // InitialDeposit is negative or null OR sender has insufficient funds + + throw + + else + sender.AtomBalance -= txGovSubmitProposal.InitialDeposit + + proposalID = generate new proposalID + proposal = NewProposal() + + proposal.Title = txGovSubmitProposal.Title + proposal.Description = txGovSubmitProposal.Description + proposal.Type = txGovSubmitProposal.Type + proposal.TotalDeposit = txGovSubmitProposal.InitialDeposit + proposal.SubmitBlock = CurrentBlock + proposal.Deposits.append({InitialDeposit, sender}) + proposal.Submitter = sender + proposal.Votes.YesVotes = 0 + proposal.Votes.NoVotes = 0 + proposal.Votes.NoWithVetoVotes = 0 + proposal.Votes.AbstainVotes = 0 + + activeProcedure = load(params, 'ActiveProcedure') + + if (txGovSubmitProposal.InitialDeposit < activeProcedure.MinDeposit) then + // MinDeposit is not reached + + proposal.VotingStartBlock = -1 + proposal.InitTotalVotingPower = 0 + + else + // MinDeposit is reached + + proposal.VotingStartBlock = CurrentBlock + proposal.InitTotalVotingPower = TotalVotingPower + proposal.InitProcedure = activeProcedure + + for each validator in CurrentBondedValidators + // Store voting power of each bonded validator + + validatorGovInfo = new ValidatorGovInfo + validatorGovInfo.InitVotingPower = validator.VotingPower + validatorGovInfo.Minus = 0 + + store(ValidatorGovInfos, :, validatorGovInfo) + + ProposalProcessingQueue.push(proposalID) + + store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping + return proposalID +``` + +### Deposit + +Once a proposal is submitted, if +`Proposal.TotalDeposit < ActiveProcedure.MinDeposit`, Atom holders can send +`TxGovDeposit` transactions to increase the proposal's deposit. + +```go +type TxGovDeposit struct { + ProposalID int64 // ID of the proposal + Deposit int64 // Number of Atoms to add to the proposal's deposit +} +``` + +**State modifications:** +* Decrease balance of sender by `deposit` +* Add `deposit` of sender in `proposal.Deposits` +* Increase `proposal.TotalDeposit` by sender's `deposit` +* If `MinDeposit` is reached: + * Push `proposalID` in `ProposalProcessingQueueEnd` + * Store each validator's voting power in `ValidatorGovInfos` + +A `TxGovDeposit` transaction has to go through a number of checks to be valid. +These checks are outlined in the following pseudocode. + +```go +// PSEUDOCODE // +// Check if TxGovDeposit is valid. If it is, increase deposit and check if MinDeposit is reached + +upon receiving txGovDeposit from sender do + // check if proposal is correctly formatted. Includes fee payment. + + if !correctlyFormatted(txGovDeposit) then + throw + + else + proposal = load(Proposals, txGovDeposit.ProposalID) + + if (proposal == nil) then + // There is no proposal for this proposalID + + throw + + else + if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit) + // deposit is negative or null OR sender has insufficient funds + + throw + + else + activeProcedure = load(params, 'ActiveProcedure') + + if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then + // MinDeposit was reached + + throw + + else + if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then + // Maximum deposit period reached + + throw + + else + // sender can deposit + + sender.AtomBalance -= txGovDeposit.Deposit + + proposal.Deposits.append({txGovVote.Deposit, sender}) + proposal.TotalDeposit += txGovDeposit.Deposit + + if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then + // MinDeposit is reached, vote opens + + proposal.VotingStartBlock = CurrentBlock + proposal.InitTotalVotingPower = TotalVotingPower + proposal.InitProcedure = activeProcedure + + for each validator in CurrentBondedValidators + // Store voting power of each bonded validator + + validatorGovInfo = NewValidatorGovInfo() + validatorGovInfo.InitVotingPower = validator.VotingPower + validatorGovInfo.Minus = 0 + + store(ValidatorGovInfos, :, validatorGovInfo) + + ProposalProcessingQueue.push(txGovDeposit.ProposalID) + + store(Proposals, txGovVote.ProposalID, proposal) +``` + +### Vote + +Once `ActiveProcedure.MinDeposit` is reached, voting period starts. From there, +bonded Atom holders are able to send `TxGovVote` transactions to cast their +vote on the proposal. + +```go + type TxGovVote struct { + ProposalID int64 // proposalID of the proposal + Option string // option from OptionSet chosen by the voter + ValidatorAddress crypto.address // Address of the validator voter wants to tie its vote to + } +``` + +**State modifications:** +* If sender is not a validator and validator has not voted, initialize or + increase minus of validator by sender's `voting power` +* If sender is not a validator and validator has voted, decrease + votes of `validatorOption` by sender's `voting power` +* If sender is not a validator, increase votes of `txGovVote.Option` + by sender's `voting power` +* If sender is a validator, increase votes of `txGovVote.Option` by + validator's `InitVotingPower - minus` (`minus` can be equal to 0) + +Votes need to be tied to a validator in order to compute validator's voting +power. If a delegator is bonded to multiple validators, it will have to send +one transaction per validator (the UI should facilitate this so that multiple +transactions can be sent in one "vote flow"). If the sender is the validator +itself, then it will input its own address as `ValidatorAddress` + +Next is a pseudocode proposal of the way `TxGovVote` transactions are +handled: + +```go + // PSEUDOCODE // + // Check if TxGovVote is valid. If it is, count vote// + + upon receiving txGovVote from sender do + // check if proposal is correctly formatted. Includes fee payment. + + if !correctlyFormatted(txGovDeposit) then + throw + + else + proposal = load(Proposals, txGovDeposit.ProposalID) + + if (proposal == nil) then + // There is no proposal for this proposalID + + throw + + else + validator = load(CurrentValidators, txGovVote.ValidatorAddress) + + if !proposal.InitProcedure.OptionSet.includes(txGovVote.Option) OR + (validator == nil) then + + // Throws if + // Option is not in Option Set of procedure that was active when vote opened OR if + // ValidatorAddress is not the address of a current validator + + throw + + else + option = load(Options, ::) + + if (option != nil) + // sender has already voted with the Atoms bonded to ValidatorAddress + + throw + + else + if (proposal.VotingStartBlock < 0) OR + (CurrentBlock > proposal.VotingStartBlock + proposal.InitProcedure.VotingPeriod) OR + (proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.ValidatorAddress) OR + (proposal.VotingStartBlock < lastUnbondingBlock(sender, txGovVote.Address) OR + (proposal.Votes.YesVotes/proposal.InitTotalVotingPower >= 2/3) then + + // Throws if + // Vote has not started OR if + // Vote had ended OR if + // sender bonded Atoms to ValidatorAddress after start of vote OR if + // sender unbonded Atoms from ValidatorAddress after start of vote OR if + // special condition is met, i.e. proposal is accepted and closed + + throw + + else + validatorGovInfo = load(ValidatorGovInfos, :) + + if (validatorGovInfo == nil) + // validator became validator after proposal entered voting period + + throw + + else + // sender can vote, check if sender == validator and store sender's option in Options + + store(Options, ::, txGovVote.Option) + + if (sender != validator.address) + // Here, sender is not the Address of the validator whose Address is txGovVote.ValidatorAddress + + if sender does not have bonded Atoms to txGovVote.ValidatorAddress then + // check in Staking module + + throw + + else + validatorOption = load(Options, ::) + + if (validatorOption == nil) + // Validator has not voted already + + validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorAddress) + store(ValidatorGovInfos, :, validatorGovInfo) + + else + // Validator has already voted + // Reduce votes of option chosen by validator by sender's bonded Amount + + proposal.Votes.validatorOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress) + + // increase votes of option chosen by sender by bonded Amount + + senderOption = txGovVote.Option + propoal.Votes.senderOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress) + + store(Proposals, txGovVote.ProposalID, proposal) + + + else + // sender is the address of the validator whose main Address is txGovVote.ValidatorAddress + // i.e. sender == validator + + proposal.Votes.validatorOption += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus) + + store(Proposals, txGovVote.ProposalID, proposal) + + +``` \ No newline at end of file diff --git a/docs/spec/ibc/ibc.md b/docs/spec/ibc/ibc.md new file mode 100644 index 000000000000..dedbdc3016f4 --- /dev/null +++ b/docs/spec/ibc/ibc.md @@ -0,0 +1,31 @@ +# IBC Specification + +IBC(Inter-Blockchain Communication) protocol is used by multiple zones on Cosmos. Using IBC, the zones can send coins or arbitrary data to other zones. + +## Terms + +How IBC module treats incoming IBC packets is simillar with how BaseApp treats incoming transactions. Therefore, the components of IBC module have their corresponding pair in BaseApp. + +| BaseApp Terms | IBC Terms | +| ------------- | ---------- | +| Router | Dispatcher | +| Tx | Packet | +| Msg | Payload | + +## MVP Specifications + +### [MVP1](./mvp1.md) + +MVP1 will contain the basic functionalities, including packet generation and packet receivement. There will be no security check for incoming packets. + +### [MVP2](./mvp2.md) + +IBC module will be more modular in MVP2. Indivisual modules can register custom handlers to IBC module. + +### [MVP3](./mvp3.md) + +Light client verification is added to verify the message from the other chain. Registering chains with their ROT(Root Of Trust) is needed. + +### [MVP4](./mvp4.md) + +ACK verification / timeout handler helper functions and messaging queue are implemented to make it failsafe. Callbacks will be registered to the dispatcher to handle failure when they register handlers. diff --git a/docs/spec/ibc/mvp1.md b/docs/spec/ibc/mvp1.md new file mode 100644 index 000000000000..3cc7d4336660 --- /dev/null +++ b/docs/spec/ibc/mvp1.md @@ -0,0 +1,76 @@ +# IBC Spec + +*This is a living document and should be edited as the IBC spec and +implementation change* + +## MVP1 + +The initial implementation of IBC will include just enough for simple coin +transfers between chains, with safety features such as ACK messages being added +later. + +It is a complete stand-alone module. It includes the commands to send IBC +packets as well as to post them to the destination chain. + +### IBC Module + +```go +// User facing API + +type IBCPacket struct { + SrcAddr sdk.Address + DestAddr sdk.Address + Coins sdk.Coins + SrcChain string + DestChain string +} + +// Implements sdk.Msg +type IBCTransferMsg struct { + IBCPacket +} + +// Implements sdk.Msg +type IBCReceiveMsg struct { + IBCPacket + Relayer sdk.Address + Sequence int64 +} + +// Internal API + +type IBCMapper struct { + ibcKey sdk.StoreKey // IngressKey / EgressKey => Value + // Ingress: Source Chain ID => last income msg's sequence + // Egress: (Dest chain ID, Msg index) => length / indexed msg +} + +type IngressKey struct { + SrcChain string +} + +type EgressKey struct { + DestChain string + Index int64 +} + +``` + +`egressKey` stores the outgoing `IBCTransfer`s as a list. Its getter takes an +`EgressKey` and returns the length if `egressKey.Index == -1`, an element if +`egressKey.Index > 0`. + +`ingressKey` stores the latest income `IBCTransfer`'s sequence. It's getter +takes an `IngressKey`. + +## Relayer + +**Packets** +- Connect to 2 Tendermint RPC endpoints +- Query for IBC outgoing `IBCOutMsg` queue (can poll on a certain time interval, or check after each new block, etc) +- For any new `IBCOutMsg`, build `IBCInMsg` and post to destination chain + +## CLI + +- Load relay process +- Execute `IBCOutMsg` diff --git a/docs/spec/ibc/mvp2.md b/docs/spec/ibc/mvp2.md new file mode 100644 index 000000000000..61590007d845 --- /dev/null +++ b/docs/spec/ibc/mvp2.md @@ -0,0 +1,89 @@ +# IBC Spec + +*This is a living document and should be edited as the IBC spec and implementation change* + +## MVP2 + +IBC module will store its own router for handling custom incoming msgs. `IBCPush` are made for inter-module communication. `IBCRegisterMsg` adds a handler in the router of the module. + +### IBC Module + +```golang +// User facing API + +type Packet struct { + Data Payload + SrcChain string + DestChain string +} + +type Payload interface { + Type() string + ValidateBasic() sdk.Error +} + +type TransferPayload struct { + SrcAddr sdk.Address + DestAddr sdk.Address + Coins sdk.Coins +} + +// Implements sdk.Msg +type IBCTransferMsg struct { + Packet +} + +// Implements sdk.Msg +type IBCReceiveMsg struct { + Packet + Relayer sdk.Address + Sequence int64 +} + +// Internal API + +type rule struct { + r string + f func(sdk.Context, IBCPacket) sdk.Result +} + +type Dispatcher struct { + rules []rule +} + +func NewHandler(dispatcher Dispatcher, ibcm IBCMapper) sdk.Handler + +type IBCMapper struct { + ibcKey sdk.StoreKey // IngressKey / EgressKey => Value + // Ingress: Source Chain ID => last income msg's sequence + // Egress: (Dest chain ID, Msg index) => length / indexed msg +} + +type IngressKey struct { + SrcChain string +} + +type EgressKey struct { + DestChain string + Index int64 +} + +// Used by other modules +func (ibcm IBCMapper) PushPacket(ctx sdk.Context, dest string, payload Payload) +``` + +`egressKey` stores the outgoing `IBCTransfer`s as a list. Its getter takes an `EgressKey` and returns the length if `egressKey.Index == -1`, an element if `egressKey.Index > 0`. + +`ingressKey` stores the last income `IBCTransfer`'s sequence. Its getter takes an `IngressKey`. + +## Relayer + +**Packets** +- Connect to 2 Tendermint RPC endpoints +- Query for IBC outgoing `IBCOutMsg` queue (can poll on a certain time interval, or check after each new block, etc) +- For any new `IBCOutMsg`, build `IBCInMsg` and post to destination chain + +## CLI + +- Load relay process +- Execute `IBCOutMsg` diff --git a/docs/spec/ibc/mvp3.md b/docs/spec/ibc/mvp3.md new file mode 100644 index 000000000000..bcbf39a9214f --- /dev/null +++ b/docs/spec/ibc/mvp3.md @@ -0,0 +1,111 @@ +# IBC Spec + +## MVP3 + +`IBCOpenMsg` is added to open the connection between two chains. Also, `IBCUpdateMsg` is added, making it able to prove the header. + +### IBC Module + + +// Implements sdk.Msg +type IBCTransferMsg struct { + Packet +} + +// Implements sdk.Msg +type IBCReceiveMsg struct { + Packet +} + +// Internal API + + + +```golang +// User facing API + +type Packet struct { + Data Payload + SrcChain string + DestChain string +} + +type Payload interface { + Type() string + ValidateBasic() sdk.Error +} + +type TransferPayload struct { + SrcAddr sdk.Address + DestAddr sdk.Address + Coins sdk.Coins +} + +// Implements sdk.Msg +type IBCTransferMsg struct { + Packet +} + +// Implements sdk.Msg +type IBCReceiveMsg struct { + Packet + Proof iavl.Proof + FromChainID string + FromChainHeight uint64 +} + +type RootOfTrust struct { + // +} + +// Implements sdk.Msg +type IBCOpenMsg struct { + ROT RootOfTrust + Chain string +} + +// Implements sdk.Msg +type IBCUpdateMsg struct { + Header tm.Header + Commit tm.Commit +} + +// Internal API + +type rule struct { + r string + f func(sdk.Context, IBCPacket) sdk.Result +} + +type Dispatcher struct { + rules []rule +} + +func NewHandler(dispatcher Dispatcher, ibcm IBCMapper) sdk.Handler + +type IBCMapper struct { + ibcKey sdk.StoreKey // IngressKey / EgressKey / HeaderKey => Value + // ChannelID => last income msg's sequence + // (ChannelID, Msg index) => length / indexed msg + // ChannelID => last known header +} + +type IngressKey struct { + ChannelID uint64 +} + +type EgressKey struct { + ChannelID uint64 + Index int64 +} + +type HeaderKey struct { + ChannelID uint64 +} + +// Used by other modules +func (ibcm IBCMapper) PushPacket(ctx sdk.Context, dest string, payload Payload) + +``` + + diff --git a/docs/spec/staking/definitions and examples.md b/docs/spec/staking/definitions and examples.md index 72dbc3a3d4e9..ba4c1563e2fc 100644 --- a/docs/spec/staking/definitions and examples.md +++ b/docs/spec/staking/definitions and examples.md @@ -2,113 +2,183 @@ ## Basic Terms and Definitions -- Cosmsos Hub - a Tendermint-based Proof of Stake blockchain system -- Atom - native token of the Cosmsos Hub -- Atom holder - an entity that holds some amount of Atoms -- Candidate - an Atom holder that is actively involved in Tendermint blockchain protocol (running Tendermint Full Node -TODO: add link to Full Node definition) and is competing with other candidates to be elected as a Validator -(TODO: add link to Validator definition)) -- Validator - a candidate that is currently selected among a set of candidates to be able to sign protocol messages -in the Tendermint consensus protocol -- Delegator - an Atom holder that has bonded any of its Atoms by delegating them to a validator (or a candidate) -- Bonding Atoms - a process of locking Atoms in a bond deposit (putting Atoms under protocol control). -Atoms are always bonded through a validator (or candidate) process. Bonded atoms can be slashed (burned) in case a -validator process misbehaves (does not behave according to a protocol specification). Atom holder can regain access -to its bonded Atoms (if they are not slashed in the meantime), i.e., they can be moved to its account, -after Unbonding period has expired. -- Unbonding period - a period of time after which Atom holder gains access to its bonded Atoms (they can be withdrawn -to a user account) or they can re-delegate -- Inflationary provisions - inflation is a process of increasing Atom supply. Atoms are being created in the process of -(Cosmos Hub) blocks creation. Owners of bonded atoms are rewarded for securing network with inflationary provisions -proportional to it's bonded Atom share. -- Transaction fees - transaction fee is a fee that is included in the Cosmsos Hub transactions. The fees are collected -by the current validator set and distributed among validators and delegators in proportion to it's bonded Atom share. -- Commission fee - a fee taken from the transaction fees by a validator for it's service +* Cosmsos Hub - a Tendermint-based Proof of Stake blockchain system +* Atom - native token of the Cosmsos Hub +* Atom holder - an entity that holds some amount of Atoms +* Candidate - an Atom holder that is actively involved in the Tendermint + blockchain protocol (running Tendermint Full Node (TODO: add link to Full + Node definition) and is competing with other candidates to be elected as a + validator (TODO: add link to Validator definition)) +* Validator - a candidate that is currently selected among a set of candidates + to be able to sign protocol messages in the Tendermint consensus protocol +* Delegator - an Atom holder that has bonded some of its Atoms by delegating + them to a validator (or a candidate) +* Bonding Atoms - a process of locking Atoms in a bond deposit (putting Atoms + under protocol control). Atoms are always bonded through a validator (or + candidate) process. Bonded atoms can be slashed (burned) in case a validator + process misbehaves (does not behave according to the protocol specification). + Atom holders can regain access to their bonded Atoms if they have not been + slashed by waiting an Unbonding period. +* Unbonding period - a period of time after which Atom holder gains access to + its bonded Atoms (they can be withdrawn to a user account) or they can be + re-delegated. +* Inflationary provisions - inflation is the process of increasing the Atom supply. + Atoms are periodically created on the Cosmos Hub and issued to bonded Atom holders. + The goal of inflation is to incentize most of the Atoms in existence to be bonded. +* Transaction fees - transaction fee is a fee that is included in a Cosmsos Hub + transaction. The fees are collected by the current validator set and + distributed among validators and delegators in proportion to their bonded + Atom share. +* Commission fee - a fee taken from the transaction fees by a validator for + their service ## The pool and the share -At the core of the Staking module is the concept of a pool which denotes collection of Atoms contributed by different -Atom holders. There are two global pools in the Staking module: bonded pool and unbonded pool. Bonded Atoms are part -of the global bonded pool. On the other side, if a candidate or delegator wants to unbond its Atoms, those Atoms are -kept in the unbonding pool for a duration of the unbonding period. In the Staking module, a pool is logical concept, -i.e., there is no pool data structure that would be responsible for managing pool resources. Instead, it is managed -in a distributed way. More precisely, at the global level, for each pool, we track only the total amount of -(bonded or unbonded) Atoms and a current amount of issued shares. A share is a unit of Atom distribution and the -value of the share (share-to-atom exchange rate) is changing during the system execution. The -share-to-atom exchange rate can be computed as: - -`share-to-atom-ex-rate = size of the pool / ammount of issued shares` - -Then for each candidate (in a per candidate data structure) we keep track of an amount of shares the candidate is owning -in a pool. At any point in time, the exact amount of Atoms a candidate has in the pool -can be computed as the number of shares it owns multiplied with the share-to-atom exchange rate: - -`candidate-coins = candidate.Shares * share-to-atom-ex-rate` - -The benefit of such accounting of the pool resources is the fact that a modification to the pool because of -bonding/unbonding/slashing/provisioning of atoms affects only global data (size of the pool and the number of shares) -and the related validator/candidate data structure, i.e., the data structure of other validators do not need to be -modified. Let's explain this further with several small examples: - -We consider initially 4 validators p1, p2, p3 and p4, and that each validator has bonded 10 Atoms -to a bonded pool. Furthermore, let's assume that we have issued initially 40 shares (note that the initial distribution -of the shares, i.e., share-to-atom exchange rate can be set to any meaningful value), i.e., -share-to-atom-ex-rate = 1 atom per share. Then at the global pool level we have, the size of the pool is 40 Atoms, and -the amount of issued shares is equal to 40. And for each validator we store in their corresponding data structure -that each has 10 shares of the bonded pool. Now lets assume that the validator p4 starts process of unbonding of 5 -shares. Then the total size of the pool is decreased and now it will be 35 shares and the amount of Atoms is 35. -Note that the only change in other data structures needed is reducing the number of shares for a validator p4 from 10 -to 5. - -Let's consider now the case where a validator p1 wants to bond 15 more atoms to the pool. Now the size of the pool -is 50, and as the exchange rate hasn't changed (1 share is still worth 1 Atom), we need to create more shares, -i.e. we now have 50 shares in the pool in total. -Validators p2, p3 and p4 still have (correspondingly) 10, 10 and 5 shares each worth of 1 atom per share, so we -don't need to modify anything in their corresponding data structures. But p1 now has 25 shares, so we update the amount -of shares owned by the p1 in its data structure. Note that apart from the size of the pool that is in Atoms, all other -data structures refer only to shares. - -Finally, let's consider what happens when new Atoms are created and added to the pool due to inflation. Let's assume that -the inflation rate is 10 percent and that it is applied to the current state of the pool. This means that 5 Atoms are -created and added to the pool and that each validator now proportionally increase it's Atom count. Let's analyse how this -change is reflected in the data structures. First, the size of the pool is increased and is now 55 atoms. As a share of -each validator in the pool hasn't changed, this means that the total number of shares stay the same (50) and that the -amount of shares of each validator stays the same (correspondingly 25, 10, 10, 5). But the exchange rate has changed and -each share is now worth 55/50 Atoms per share, so each validator has effectively increased amount of Atoms it has. -So validators now have (correspondingly) 55/2, 55/5, 55/5 and 55/10 Atoms. - -The concepts of the pool and its shares is at the core of the accounting in the Staking module. It is used for managing -the global pools (such as bonding and unbonding pool), but also for distribution of Atoms between validator and its -delegators (we will explain this in section X). +At the core of the Staking module is the concept of a pool which denotes a +collection of Atoms contributed by different Atom holders. There are two global +pools in the Staking module: the bonded pool and unbonding pool. Bonded Atoms +are part of the global bonded pool. If a candidate or delegator wants to unbond +its Atoms, those Atoms are moved to the the unbonding pool for the duration of +the unbonding period. In the Staking module, a pool is a logical concept, i.e., +there is no pool data structure that would be responsible for managing pool +resources. Instead, it is managed in a distributed way. More precisely, at the +global level, for each pool, we track only the total amount of bonded or unbonded +Atoms and the current amount of issued shares. A share is a unit of Atom distribution +and the value of the share (share-to-atom exchange rate) changes during +system execution. The share-to-atom exchange rate can be computed as: + +`share-to-atom-exchange-rate = size of the pool / ammount of issued shares` + +Then for each validator candidate (in a per candidate data structure) we keep track of +the amount of shares the candidate owns in a pool. At any point in time, +the exact amount of Atoms a candidate has in the pool can be computed as the +number of shares it owns multiplied with the current share-to-atom exchange rate: + +`candidate-coins = candidate.Shares * share-to-atom-exchange-rate` + +The benefit of such accounting of the pool resources is the fact that a +modification to the pool from bonding/unbonding/slashing/provisioning of +Atoms affects only global data (size of the pool and the number of shares) and +not the related validator/candidate data structure, i.e., the data structure of +other validators do not need to be modified. This has the advantage that +modifying global data is much cheaper computationally than modifying data of +every validator. Let's explain this further with several small examples: + +We consider initially 4 validators p1, p2, p3 and p4, and that each validator +has bonded 10 Atoms to the bonded pool. Furthermore, let's assume that we have +issued initially 40 shares (note that the initial distribution of the shares, +i.e., share-to-atom exchange rate can be set to any meaningful value), i.e., +share-to-atom-ex-rate = 1 atom per share. Then at the global pool level we +have, the size of the pool is 40 Atoms, and the amount of issued shares is +equal to 40. And for each validator we store in their corresponding data +structure that each has 10 shares of the bonded pool. Now lets assume that the +validator p4 starts process of unbonding of 5 shares. Then the total size of +the pool is decreased and now it will be 35 shares and the amount of Atoms is +35 . Note that the only change in other data structures needed is reducing the +number of shares for a validator p4 from 10 to 5. + +Let's consider now the case where a validator p1 wants to bond 15 more atoms to +the pool. Now the size of the pool is 50, and as the exchange rate hasn't +changed (1 share is still worth 1 Atom), we need to create more shares, i.e. we +now have 50 shares in the pool in total. Validators p2, p3 and p4 still have +(correspondingly) 10, 10 and 5 shares each worth of 1 atom per share, so we +don't need to modify anything in their corresponding data structures. But p1 +now has 25 shares, so we update the amount of shares owned by p1 in its +data structure. Note that apart from the size of the pool that is in Atoms, all +other data structures refer only to shares. + +Finally, let's consider what happens when new Atoms are created and added to +the pool due to inflation. Let's assume that the inflation rate is 10 percent +and that it is applied to the current state of the pool. This means that 5 +Atoms are created and added to the pool and that each validator now +proportionally increase it's Atom count. Let's analyse how this change is +reflected in the data structures. First, the size of the pool is increased and +is now 55 atoms. As a share of each validator in the pool hasn't changed, this +means that the total number of shares stay the same (50) and that the amount of +shares of each validator stays the same (correspondingly 25, 10, 10, 5). But +the exchange rate has changed and each share is now worth 55/50 Atoms per +share, so each validator has effectively increased amount of Atoms it has. So +validators now have (correspondingly) 55/2, 55/5, 55/5 and 55/10 Atoms. + +The concepts of the pool and its shares is at the core of the accounting in the +Staking module. It is used for managing the global pools (such as bonding and +unbonding pool), but also for distribution of Atoms between validator and its +delegators (we will explain this in section X). #### Delegator shares -A candidate is, depending on it's status, contributing Atoms to either the bonded or unbonded pool, and in return gets -some amount of (global) pool shares. Note that not all those Atoms (and respective shares) are owned by the candidate -as some Atoms could be delegated to a candidate. The mechanism for distribution of Atoms (and shares) between a -candidate and it's delegators is based on a notion of delegator shares. More precisely, every candidate is issuing -(local) delegator shares (`Candidate.IssuedDelegatorShares`) that represents some portion of global shares -managed by the candidate (`Candidate.GlobalStakeShares`). The principle behind managing delegator shares is the same -as described in [Section](#The pool and the share). We now illustrate it with an example. - -Lets consider 4 validators p1, p2, p3 and p4, and assume that each validator has bonded 10 Atoms to a bonded pool. -Furthermore, lets assume that we have issued initially 40 global shares, i.e., that `share-to-atom-ex-rate = 1 atom per -share`. So we will `GlobalState.BondedPool = 40` and `GlobalState.BondedShares = 40` and in the Candidate data structure -of each validator `Candidate.GlobalStakeShares = 10`. Furthermore, each validator issued 10 delegator -shares which are initially owned by itself, i.e., `Candidate.IssuedDelegatorShares = 10`, where +A candidate is, depending on it's status, contributing Atoms to either the +bonded or unbonding pool, and in return gets some amount of (global) pool +shares. Note that not all those Atoms (and respective shares) are owned by the +candidate as some Atoms could be delegated to a candidate. The mechanism for +distribution of Atoms (and shares) between a candidate and it's delegators is +based on a notion of delegator shares. More precisely, every candidate is +issuing (local) delegator shares (`Candidate.IssuedDelegatorShares`) that +represents some portion of global shares managed by the candidate +(`Candidate.GlobalStakeShares`). The principle behind managing delegator shares +is the same as described in [Section](#The pool and the share). We now +illustrate it with an example. + +Let's consider 4 validators p1, p2, p3 and p4, and assume that each validator +has bonded 10 Atoms to the bonded pool. Furthermore, let's assume that we have +issued initially 40 global shares, i.e., that +`share-to-atom-exchange-rate = 1 atom per share`. So we will set +`GlobalState.BondedPool = 40` and `GlobalState.BondedShares = 40` and in the +Candidate data structure of each validator `Candidate.GlobalStakeShares = 10`. +Furthermore, each validator issued 10 delegator shares which are initially +owned by itself, i.e., `Candidate.IssuedDelegatorShares = 10`, where `delegator-share-to-global-share-ex-rate = 1 global share per delegator share`. -Now lets assume that a delegator d1 delegates 5 atoms to a validator p1 and consider what are the updates we need -to make to the data structures. First, `GlobalState.BondedPool = 45` and `GlobalState.BondedShares = 45`. Then, -for validator p1 we have `Candidate.GlobalStakeShares = 15`, but we also need to issue also additional delegator shares, -i.e., `Candidate.IssuedDelegatorShares = 15` as the delegator d1 now owns 5 delegator shares of validator p1, where -each delegator share is worth 1 global shares, i.e, 1 Atom. Lets see now what happens after 5 new Atoms are created due -to inflation. In that case, we only need to update `GlobalState.BondedPool` which is now equal to 50 Atoms as created -Atoms are added to the bonded pool. Note that the amount of global and delegator shares stay the same but they are now -worth more as share-to-atom-ex-rate is now worth 50/45 Atoms per share. Therefore, a delegator d1 now owns - -`delegatorCoins = 10 (delegator shares) * 1 (delegator-share-to-global-share-ex-rate) * 50/45 (share-to-atom-ex-rate) = 100/9 Atoms` - - +Now lets assume that a delegator d1 delegates 5 atoms to a validator p1 and +consider what are the updates we need to make to the data structures. First, +`GlobalState.BondedPool = 45` and `GlobalState.BondedShares = 45`. Then, for +validator p1 we have `Candidate.GlobalStakeShares = 15`, but we also need to +issue also additional delegator shares, i.e., +`Candidate.IssuedDelegatorShares = 15` as the delegator d1 now owns 5 delegator +shares of validator p1, where each delegator share is worth 1 global shares, +i.e, 1 Atom. Lets see now what happens after 5 new Atoms are created due to +inflation. In that case, we only need to update `GlobalState.BondedPool` which +is now equal to 50 Atoms as created Atoms are added to the bonded pool. Note +that the amount of global and delegator shares stay the same but they are now +worth more as share-to-atom-exchange-rate is now worth 50/45 Atoms per share. +Therefore, a delegator d1 now owns: + +`delegatorCoins = 5 (delegator shares) * 1 (delegator-share-to-global-share-ex-rate) * 50/45 (share-to-atom-ex-rate) = 5.55 Atoms` + +### Inflation provisions + +Validator provisions are minted on an hourly basis (the first block of a new +hour). The annual target of between 7% and 20%. The long-term target ratio of +bonded tokens to unbonded tokens is 67%. + +The target annual inflation rate is recalculated for each provisions cycle. The +inflation is also subject to a rate change (positive or negative) depending on +the distance from the desired ratio (67%). The maximum rate change possible is +defined to be 13% per year, however the annual inflation is capped as between +7% and 20%. + +```go +inflationRateChange(0) = 0 +GlobalState.Inflation(0) = 0.07 + +bondedRatio = GlobalState.BondedPool / GlobalState.TotalSupply +AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13 + +annualInflation += AnnualInflationRateChange + +if annualInflation > 0.20 then GlobalState.Inflation = 0.20 +if annualInflation < 0.07 then GlobalState.Inflation = 0.07 + +provisionTokensHourly = GlobalState.TotalSupply * GlobalState.Inflation / (365.25*24) +``` + +Because the validators hold a relative bonded share (`GlobalStakeShares`), when +more bonded tokens are added proportionally to all validators, the only term +which needs to be updated is the `GlobalState.BondedPool`. So for each +provisions cycle: + +```go +GlobalState.BondedPool += provisionTokensHourly +``` diff --git a/docs/spec/staking/spec-technical.md b/docs/spec/staking/spec-technical.md index e3a528d948b5..8d9baa796799 100644 --- a/docs/spec/staking/spec-technical.md +++ b/docs/spec/staking/spec-technical.md @@ -2,54 +2,66 @@ ## Overview -The Cosmos Hub is a Tendermint-based Proof of Stake blockchain system that serves as a backbone of the Cosmos ecosystem. -It is operated and secured by an open and globally decentralized set of validators. Tendermint consensus is a -Byzantine fault-tolerant distributed protocol that involves all validators in the process of exchanging protocol -messages in the production of each block. To avoid Nothing-at-Stake problem, a validator in Tendermint needs to lock up -coins in a bond deposit. Tendermint protocol messages are signed by the validator's private key, and this is a basis for -Tendermint strict accountability that allows punishing misbehaving validators by slashing (burning) their bonded Atoms. -On the other hand, validators are for it's service of securing blockchain network rewarded by the inflationary -provisions and transactions fees. This incentivizes correct behavior of the validators and provide economic security -of the network. - -The native token of the Cosmos Hub is called Atom; becoming a validator of the Cosmos Hub requires holding Atoms. -However, not all Atom holders are validators of the Cosmos Hub. More precisely, there is a selection process that -determines the validator set as a subset of all validator candidates (Atom holder that wants to -become a validator). The other option for Atom holder is to delegate their atoms to validators, i.e., -being a delegator. A delegator is an Atom holder that has bonded its Atoms by delegating it to a validator -(or validator candidate). By bonding Atoms to securing network (and taking a risk of being slashed in case the -validator misbehaves), a user is rewarded with inflationary provisions and transaction fees proportional to the amount -of its bonded Atoms. The Cosmos Hub is designed to efficiently facilitate a small numbers of validators (hundreds), and -large numbers of delegators (tens of thousands). More precisely, it is the role of the Staking module of the Cosmos Hub -to support various staking functionality including validator set selection; delegating, bonding and withdrawing Atoms; -and the distribution of inflationary provisions and transaction fees. - +The Cosmos Hub is a Tendermint-based Proof of Stake blockchain system that +serves as a backbone of the Cosmos ecosystem. It is operated and secured by an +open and globally decentralized set of validators. Tendermint consensus is a +Byzantine fault-tolerant distributed protocol that involves all validators in +the process of exchanging protocol messages in the production of each block. To +avoid Nothing-at-Stake problem, a validator in Tendermint needs to lock up +coins in a bond deposit. Tendermint protocol messages are signed by the +validator's private key, and this is a basis for Tendermint strict +accountability that allows punishing misbehaving validators by slashing +(burning) their bonded Atoms. On the other hand, validators are rewarded for +their service of securing blockchain network by the inflationary provisions and +transactions fees. This incentives correct behavior of the validators and +provides the economic security of the network. + +The native token of the Cosmos Hub is called Atom; becoming a validator of the +Cosmos Hub requires holding Atoms. However, not all Atom holders are validators +of the Cosmos Hub. More precisely, there is a selection process that determines +the validator set as a subset of all validator candidates (Atom holders that +wants to become a validator). The other option for Atom holder is to delegate +their atoms to validators, i.e., being a delegator. A delegator is an Atom +holder that has bonded its Atoms by delegating it to a validator (or validator +candidate). By bonding Atoms to secure the network (and taking a risk of being +slashed in case of misbehaviour), a user is rewarded with inflationary +provisions and transaction fees proportional to the amount of its bonded Atoms. +The Cosmos Hub is designed to efficiently facilitate a small numbers of +validators (hundreds), and large numbers of delegators (tens of thousands). +More precisely, it is the role of the Staking module of the Cosmos Hub to +support various staking functionality including validator set selection, +delegating, bonding and withdrawing Atoms, and the distribution of inflationary +provisions and transaction fees. + ## State The staking module persists the following information to the store: -- `GlobalState`, describing the global pools and the inflation related fields -- `map[PubKey]Candidate`, a map of validator candidates (including current validators), indexed by public key -- `map[rational.Rat]Candidate`, an ordered map of validator candidates (including current validators), indexed by -shares in the global pool (bonded or unbonded depending on candidate status) -- `map[[]byte]DelegatorBond`, a map of DelegatorBonds (for each delegation to a candidate by a delegator), indexed by -the delegator address and the candidate public key -- `queue[QueueElemUnbondDelegation]`, a queue of unbonding delegations -- `queue[QueueElemReDelegate]`, a queue of re-delegations +* `GlobalState`, describing the global pools and the inflation related fields +* validator candidates (including current validators), indexed by public key and shares in the global pool +(bonded or unbonded depending on candidate status) +* delegator bonds (for each delegation to a candidate by a delegator), indexed by the delegator address and the candidate + public key +* the queue of unbonding delegations +* the queue of re-delegations ### Global State -GlobalState data structure contains total Atoms supply, amount of Atoms in the bonded pool, sum of all shares -distributed for the bonded pool, amount of Atoms in the unbonded pool, sum of all shares distributed for the -unbonded pool, a timestamp of the last processing of inflation, the current annual inflation rate, a timestamp -for the last comission accounting reset, the global fee pool, a pool of reserve taxes collected for the governance use -and an adjustment factor for calculating global feel accum (?). - -``` golang +The GlobalState data structure contains total Atom supply, amount of Atoms in +the bonded pool, sum of all shares distributed for the bonded pool, amount of +Atoms in the unbonded pool, sum of all shares distributed for the unbonded +pool, a timestamp of the last processing of inflation, the current annual +inflation rate, a timestamp for the last comission accounting reset, the global +fee pool, a pool of reserve taxes collected for the governance use and an +adjustment factor for calculating global fee accum. `Params` is global data +structure that stores system parameters and defines overall functioning of the +module. + +``` go type GlobalState struct { TotalSupply int64 // total supply of Atoms BondedPool int64 // reserve of bonded tokens BondedShares rational.Rat // sum of all shares distributed for the BondedPool - UnbondedPool int64 // reserve of unbonded tokens held with candidates + UnbondedPool int64 // reserve of unbonding tokens held with candidates UnbondedShares rational.Rat // sum of all shares distributed for the UnbondedPool InflationLastTime int64 // timestamp of last processing of inflation Inflation rational.Rat // current annual inflation rate @@ -58,19 +70,40 @@ type GlobalState struct { ReservePool coin.Coins // pool of reserve taxes collected on all fees for governance use Adjustment rational.Rat // Adjustment factor for calculating global fee accum } + +type Params struct { + HoldBonded Address // account where all bonded coins are held + HoldUnbonding Address // account where all delegated but unbonding coins are held + + InflationRateChange rational.Rational // maximum annual change in inflation rate + InflationMax rational.Rational // maximum inflation rate + InflationMin rational.Rational // minimum inflation rate + GoalBonded rational.Rational // Goal of percent bonded atoms + ReserveTax rational.Rational // Tax collected on all fees + + MaxVals uint16 // maximum number of validators + AllowedBondDenom string // bondable coin denomination + + // gas costs for txs + GasDeclareCandidacy int64 + GasEditCandidacy int64 + GasDelegate int64 + GasRedelegate int64 + GasUnbond int64 +} ``` ### Candidate -The `Candidate` data structure holds the current state and some historical actions of -validators or candidate-validators. +The `Candidate` data structure holds the current state and some historical +actions of validators or candidate-validators. -``` golang +``` go type Candidate struct { Status CandidateStatus - PubKey crypto.PubKey + ConsensusPubKey crypto.PubKey GovernancePubKey crypto.PubKey - Owner Address + Owner crypto.Address GlobalStakeShares rational.Rat IssuedDelegatorShares rational.Rat RedelegatingShares rational.Rat @@ -83,118 +116,115 @@ type Candidate struct { Adjustment rational.Rat Description Description } -``` - -CandidateStatus can be VyingUnbonded, VyingUnbonding, Bonded, KickUnbonding and KickUnbonded. - -``` golang type Description struct { - Name string - DateBonded string - Identity string - Website string - Details string + Name string + DateBonded string + Identity string + Website string + Details string } ``` Candidate parameters are described: - - Status: signal that the candidate is either vying for validator status, - either unbonded or unbonding, an active validator, or a kicked validator - either unbonding or unbonded. - - PubKey: separated key from the owner of the candidate as is used strictly - for participating in consensus. - - Owner: Address where coins are bonded from and unbonded to - - GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if - `Candidate.Status` is `Bonded`; or shares of `GlobalState.UnbondedPool` otherwise - - IssuedDelegatorShares: Sum of all shares a candidate issued to delegators (which - includes the candidate's self-bond); a delegator share represents their stake in - the Candidate's `GlobalStakeShares` - - RedelegatingShares: The portion of `IssuedDelegatorShares` which are - currently re-delegating to a new validator - - VotingPower: Proportional to the amount of bonded tokens which the validator - has if the candidate is a validator. - - Commission: The commission rate of fees charged to any delegators - - CommissionMax: The maximum commission rate this candidate can charge - each day from the date `GlobalState.DateLastCommissionReset` - - CommissionChangeRate: The maximum daily increase of the candidate commission - - CommissionChangeToday: Counter for the amount of change to commission rate - which has occurred today, reset on the first block of each day (UTC time) - - ProposerRewardPool: reward pool for extra fees collected when this candidate - is the proposer of a block - - Adjustment factor used to passively calculate each validators entitled fees - from `GlobalState.FeePool` - - Description - - Name: moniker - - DateBonded: date determined which the validator was bonded - - Identity: optional field to provide a signature which verifies the - validators identity (ex. UPort or Keybase) - - Website: optional website link - - Details: optional details +* Status: it can be Bonded (active validator), Unbonding (validator candidate) + or Revoked +* ConsensusPubKey: candidate public key that is used strictly for participating in + consensus +* GovernancePubKey: public key used by the validator for governance voting +* Owner: Address that is allowed to unbond coins. +* GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if + `Candidate.Status` is `Bonded`; or shares of `GlobalState.Unbondingt Pool` + otherwise +* IssuedDelegatorShares: Sum of all shares a candidate issued to delegators + (which includes the candidate's self-bond); a delegator share represents + their stake in the Candidate's `GlobalStakeShares` +* RedelegatingShares: The portion of `IssuedDelegatorShares` which are + currently re-delegating to a new validator +* VotingPower: Proportional to the amount of bonded tokens which the validator + has if `Candidate.Status` is `Bonded`; otherwise it is equal to `0` +* Commission: The commission rate of fees charged to any delegators +* CommissionMax: The maximum commission rate this candidate can charge each + day from the date `GlobalState.DateLastCommissionReset` +* CommissionChangeRate: The maximum daily increase of the candidate commission +* CommissionChangeToday: Counter for the amount of change to commission rate + which has occurred today, reset on the first block of each day (UTC time) +* ProposerRewardPool: reward pool for extra fees collected when this candidate + is the proposer of a block +* Adjustment factor used to passively calculate each validators entitled fees + from `GlobalState.FeePool` +* Description + * Name: moniker + * DateBonded: date determined which the validator was bonded + * Identity: optional field to provide a signature which verifies the + validators identity (ex. UPort or Keybase) + * Website: optional website link + * Details: optional details ### DelegatorBond -Atom holders may delegate coins to validators; under this circumstance their -funds are held in a `DelegatorBond` data structure. It is owned by one delegator, and is -associated with the shares for one validator. The sender of the transaction is -considered the owner of the bond. +Atom holders may delegate coins to candidates; under this circumstance their +funds are held in a `DelegatorBond` data structure. It is owned by one +delegator, and is associated with the shares for one candidate. The sender of +the transaction is the owner of the bond. -``` golang +``` go type DelegatorBond struct { - Candidate crypto.PubKey - Shares rational.Rat + Candidate crypto.PubKey + Shares rational.Rat AdjustmentFeePool coin.Coins AdjustmentRewardPool coin.Coins } ``` Description: - - Candidate: the public key of the validator candidate: bonding too - - Shares: the number of delegator shares received from the validator candidate - - AdjustmentFeePool: Adjustment factor used to passively calculate each bonds - entitled fees from `GlobalState.FeePool` - - AdjustmentRewardPool: Adjustment factor used to passively calculate each - bonds entitled fees from `Candidate.ProposerRewardPool`` +* Candidate: the public key of the validator candidate: bonding too +* Shares: the number of delegator shares received from the validator candidate +* AdjustmentFeePool: Adjustment factor used to passively calculate each bonds + entitled fees from `GlobalState.FeePool` +* AdjustmentRewardPool: Adjustment factor used to passively calculate each + bonds entitled fees from `Candidate.ProposerRewardPool` + ### QueueElem -Unbonding and re-delegation process is implemented using the ordered queue data structure. -All queue elements used share a common structure: +The Unbonding and re-delegation process is implemented using the ordered queue +data structure. All queue elements share a common structure: -``` golang +```golang type QueueElem struct { - Candidate crypto.PubKey - InitHeight int64 // when the queue was initiated + Candidate crypto.PubKey + InitTime int64 // when the element was added to the queue } ``` -The queue is ordered so the next to unbond/re-delegate is at the head. Every -tick the head of the queue is checked and if the unbonding period has passed -since `InitHeight`, the final settlement of the unbonding is started or re-delegation is executed, and the element is -pop from the queue. Each `QueueElem` is persisted in the store until it is popped from the queue. +The queue is ordered so the next element to unbond/re-delegate is at the head. +Every tick the head of the queue is checked and if the unbonding period has +passed since `InitTime`, the final settlement of the unbonding is started or +re-delegation is executed, and the element is popped from the queue. Each +`QueueElem` is persisted in the store until it is popped from the queue. ### QueueElemUnbondDelegation -``` golang +QueueElemUnbondDelegation structure is used in the unbonding queue. + +```golang type QueueElemUnbondDelegation struct { - QueueElem - Payout Address // account to pay out to - Shares rational.Rat // amount of delegator shares which are unbonding - StartSlashRatio rational.Rat // candidate slash ratio at start of re-delegation + QueueElem + Payout Address // account to pay out to + Tokens coin.Coins // the value in Atoms of the amount of delegator shares which are unbonding + StartSlashRatio rational.Rat // candidate slash ratio } ``` -In the unbonding queue - the fraction of all historical slashings on -that validator are recorded (`StartSlashRatio`). When this queue reaches maturity -if that total slashing applied is greater on the validator then the -difference (amount that should have been slashed from the first validator) is -assigned to the amount being paid out. ### QueueElemReDelegate -``` golang +QueueElemReDelegate structure is used in the re-delegation queue. + +```golang type QueueElemReDelegate struct { - QueueElem - Payout Address // account to pay out to + QueueElem + Payout Address // account to pay out to Shares rational.Rat // amount of shares which are unbonding NewCandidate crypto.PubKey // validator to bond to after unbond } @@ -203,31 +233,38 @@ type QueueElemReDelegate struct { ### Transaction Overview Available Transactions: - - TxDeclareCandidacy - - TxEditCandidacy - - TxLivelinessCheck - - TxProveLive - - TxDelegate - - TxUnbond - - TxRedelegate +* TxDeclareCandidacy +* TxEditCandidacy +* TxDelegate +* TxUnbond +* TxRedelegate +* TxLivelinessCheck +* TxProveLive ## Transaction processing -In this section we describe the processing of the transactions and the corresponding updates to the global state. -For the following text we will use gs to refer to the GlobalState data structure, candidateMap is a reference to the -map[PubKey]Candidate, delegatorBonds is a reference to map[[]byte]DelegatorBond, unbondDelegationQueue is a -reference to the queue[QueueElemUnbondDelegation] and redelegationQueue is the reference for the -queue[QueueElemReDelegate]. We use tx to denote reference to a transaction that is being processed. +In this section we describe the processing of the transactions and the +corresponding updates to the global state. In the following text we will use +`gs` to refer to the `GlobalState` data structure, `unbondDelegationQueue` is a +reference to the queue of unbond delegations, `reDelegationQueue` is the +reference for the queue of redelegations. We use `tx` to denote a +reference to a transaction that is being processed, and `sender` to denote the +address of the sender of the transaction. We use function +`loadCandidate(store, PubKey)` to obtain a Candidate structure from the store, +and `saveCandidate(store, candidate)` to save it. Similarly, we use +`loadDelegatorBond(store, sender, PubKey)` to load a delegator bond with the +key (sender and PubKey) from the store, and +`saveDelegatorBond(store, sender, bond)` to save it. +`removeDelegatorBond(store, sender, bond)` is used to remove the bond from the +store. ### TxDeclareCandidacy -A validator candidacy can be declared using the `TxDeclareCandidacy` transaction. -During this transaction a self-delegation transaction is executed to bond -tokens which are sent in with the transaction (TODO: What does this mean?). +A validator candidacy is declared using the `TxDeclareCandidacy` transaction. -``` golang +```golang type TxDeclareCandidacy struct { - PubKey crypto.PubKey + ConsensusPubKey crypto.PubKey Amount coin.Coin GovernancePubKey crypto.PubKey Commission rational.Rat @@ -235,28 +272,25 @@ type TxDeclareCandidacy struct { CommissionMaxChange int64 Description Description } -``` -``` declareCandidacy(tx TxDeclareCandidacy): - // create and save the empty candidate candidate = loadCandidate(store, tx.PubKey) - if candidate != nil then return + if candidate != nil return // candidate with that public key already exists candidate = NewCandidate(tx.PubKey) candidate.Status = Unbonded candidate.Owner = sender - init candidate VotingPower, GlobalStakeShares, IssuedDelegatorShares,RedelegatingShares and Adjustment to rational.Zero + init candidate VotingPower, GlobalStakeShares, IssuedDelegatorShares, RedelegatingShares and Adjustment to rational.Zero init commision related fields based on the values from tx candidate.ProposerRewardPool = Coin(0) candidate.Description = tx.Description saveCandidate(store, candidate) - // move coins from the sender account to a (self-bond) delegator account - // the candidate account and global shares are updated within here - txDelegate = TxDelegate{tx.BondUpdate} - return delegateWithCandidate(txDelegate, candidate) + txDelegate = TxDelegate(tx.PubKey, tx.Amount) + return delegateWithCandidate(txDelegate, candidate) + +// see delegateWithCandidate function in [TxDelegate](TxDelegate) ``` ### TxEditCandidacy @@ -265,221 +299,326 @@ If either the `Description` (excluding `DateBonded` which is constant), `Commission`, or the `GovernancePubKey` need to be updated, the `TxEditCandidacy` transaction should be sent from the owner account: -``` golang +```golang type TxEditCandidacy struct { GovernancePubKey crypto.PubKey Commission int64 Description Description } -``` -``` editCandidacy(tx TxEditCandidacy): candidate = loadCandidate(store, tx.PubKey) - if candidate == nil or candidate.Status == Unbonded return - if tx.GovernancePubKey != nil then candidate.GovernancePubKey = tx.GovernancePubKey - if tx.Commission >= 0 then candidate.Commission = tx.Commission - if tx.Description != nil then candidate.Description = tx.Description + if candidate == nil or candidate.Status == Revoked return + + if tx.GovernancePubKey != nil candidate.GovernancePubKey = tx.GovernancePubKey + if tx.Commission >= 0 candidate.Commission = tx.Commission + if tx.Description != nil candidate.Description = tx.Description + saveCandidate(store, candidate) return - ``` +``` ### TxDelegate -All bonding, whether self-bonding or delegation, is done via `TxDelegate`. - -Delegator bonds are created using the `TxDelegate` transaction. Within this transaction the delegator provides -an amount of coins, and in return receives some amount of candidate's delegator shares that are assigned to -`DelegatorBond.Shares`. The amount of created delegator shares depends on the candidate's -delegator-shares-to-atoms exchange rate and is computed as -`delegator-shares = delegator-coins / delegator-shares-to-atom-ex-rate`. +Delegator bonds are created using the `TxDelegate` transaction. Within this +transaction the delegator provides an amount of coins, and in return receives +some amount of candidate's delegator shares that are assigned to +`DelegatorBond.Shares`. -``` golang -type TxDelegate struct { - PubKey crypto.PubKey - Amount coin.Coin +```golang +type TxDelegate struct { + PubKey crypto.PubKey + Amount coin.Coin } -``` -``` delegate(tx TxDelegate): candidate = loadCandidate(store, tx.PubKey) - if candidate == nil then return + if candidate == nil return return delegateWithCandidate(tx, candidate) delegateWithCandidate(tx TxDelegate, candidate Candidate): - if candidate.Status == Revoked then return + if candidate.Status == Revoked return - if candidate.Status == Bonded then - poolAccount = address of the bonded pool - else - poolAccount = address of the unbonded pool - - // Move coins from the delegator account to the bonded pool account - err = transfer(sender, poolAccount, tx.Amount) - if err != nil then return - - // Get or create the delegator bond - bond = loadDelegatorBond(store, sender, tx.PubKey) - if bond == nil then - bond = DelegatorBond{tx.PubKey,rational.Zero, Coin(0), Coin(0)} + if candidate.Status == Bonded + poolAccount = params.HoldBonded + else + poolAccount = params.HoldUnbonded - issuedDelegatorShares = candidate.addTokens(tx.Amount, gs) - bond.Shares = bond.Shares.Add(issuedDelegatorShares) + err = transfer(sender, poolAccount, tx.Amount) + if err != nil return + + bond = loadDelegatorBond(store, sender, tx.PubKey) + if bond == nil then bond = DelegatorBond(tx.PubKey, rational.Zero, Coin(0), Coin(0)) - saveCandidate(store, candidate) + issuedDelegatorShares = addTokens(tx.Amount, candidate) + bond.Shares += issuedDelegatorShares - store.Set(GetDelegatorBondKey(sender, bond.PubKey), bond) + saveCandidate(store, candidate) + saveDelegatorBond(store, sender, bond) + saveGlobalState(store, gs) + return + +addTokens(amount coin.Coin, candidate Candidate): + if candidate.Status == Bonded + gs.BondedPool += amount + issuedShares = amount / exchangeRate(gs.BondedShares, gs.BondedPool) + gs.BondedShares += issuedShares + else + gs.UnbondedPool += amount + issuedShares = amount / exchangeRate(gs.UnbondedShares, gs.UnbondedPool) + gs.UnbondedShares += issuedShares - saveGlobalState(store, gs) - return - -addTokens(amount int64, gs GlobalState, candidate Candidate): - - // get the exchange rate of global pool shares over delegator shares - if candidate.IssuedDelegatorShares.IsZero() then + candidate.GlobalStakeShares += issuedShares + + if candidate.IssuedDelegatorShares.IsZero() exRate = rational.One else - exRate = candiate.GlobalStakeShares.Quo(candidate.IssuedDelegatorShares) - - if candidate.Status == Bonded then - gs.BondedPool += amount - issuedShares = exchangeRate(gs.BondedShares, gs.BondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens - gs.BondedShares = gs.BondedShares.Add(issuedShares) - else - gs.UnbondedPool += amount - issuedShares = exchangeRate(gs.UnbondedShares, gs.UnbondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens - gs.UnbondedShares = gs.UnbondedShares.Add(issuedShares) + exRate = candidate.GlobalStakeShares / candidate.IssuedDelegatorShares - candidate.GlobalStakeShares = candidate.GlobalStakeShares.Add(issuedShares) - - issuedDelegatorShares = exRate.Mul(receivedGlobalShares) - candidate.IssuedDelegatorShares = candidate.IssuedDelegatorShares.Add(issuedDelegatorShares) - return + issuedDelegatorShares = issuedShares / exRate + candidate.IssuedDelegatorShares += issuedDelegatorShares + return issuedDelegatorShares exchangeRate(shares rational.Rat, tokenAmount int64): if shares.IsZero() then return rational.One - return shares.Inv().Mul(tokenAmount) + return tokenAmount / shares ``` ### TxUnbond + Delegator unbonding is defined with the following transaction: -``` golang -type TxUnbond struct { - PubKey crypto.PubKey - Shares rational.Rat +```golang +type TxUnbond struct { + PubKey crypto.PubKey + Shares rational.Rat } -``` -``` -unbond(tx TxUnbond): +unbond(tx TxUnbond): + bond = loadDelegatorBond(store, sender, tx.PubKey) + if bond == nil return + if bond.Shares < tx.Shares return + + bond.Shares -= tx.Shares + + candidate = loadCandidate(store, tx.PubKey) + + revokeCandidacy = false + if bond.Shares.IsZero() + if sender == candidate.Owner and candidate.Status != Revoked then revokeCandidacy = true then removeDelegatorBond(store, sender, bond) + else + saveDelegatorBond(store, sender, bond) - // get delegator bond - bond = loadDelegatorBond(store, sender, tx.PubKey) - if bond == nil then return + if candidate.Status == Bonded + poolAccount = params.HoldBonded + else + poolAccount = params.HoldUnbonded - // subtract bond tokens from delegator bond - if bond.Shares.LT(tx.Shares) return // bond shares < tx shares + returnedCoins = removeShares(candidate, shares) - bond.Shares = bond.Shares.Sub(ts.Shares) + unbondDelegationElem = QueueElemUnbondDelegation(tx.PubKey, currentHeight(), sender, returnedCoins, startSlashRatio) + unbondDelegationQueue.add(unbondDelegationElem) + + transfer(poolAccount, unbondingPoolAddress, returnCoins) + + if revokeCandidacy + if candidate.Status == Bonded then bondedToUnbondedPool(candidate) + candidate.Status = Revoked - candidate = loadCandidate(store, tx.PubKey) - if candidate == nil return + if candidate.IssuedDelegatorShares.IsZero() + removeCandidate(store, tx.PubKey) + else + saveCandidate(store, candidate) - revokeCandidacy = false - if bond.Shares.IsZero() { - // if the bond is the owner of the candidate then trigger a revoke candidacy - if sender.Equals(candidate.Owner) and candidate.Status != Revoked then - revokeCandidacy = true + saveGlobalState(store, gs) + return - // remove the bond - removeDelegatorBond(store, sender, tx.PubKey) - else - saveDelegatorBond(store, sender, bond) +removeShares(candidate Candidate, shares rational.Rat): + globalPoolSharesToRemove = delegatorShareExRate(candidate) * shares - // transfer coins back to account - if candidate.Status == Bonded then - poolAccount = address of the bonded pool + if candidate.Status == Bonded + gs.BondedShares -= globalPoolSharesToRemove + removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * globalPoolSharesToRemove + gs.BondedPool -= removedTokens else - poolAccount = address of the unbonded pool - - returnCoins = candidate.removeShares(shares, gs) - // TODO: Shouldn't it be created a queue element in this case? - transfer(poolAccount, sender, returnCoins) - - if revokeCandidacy then - // change the share types to unbonded if they were not already - if candidate.Status == Bonded then - // replace bonded shares with unbonded shares - tokens = gs.removeSharesBonded(candidate.GlobalStakeShares) - candidate.GlobalStakeShares = gs.addTokensUnbonded(tokens) - candidate.Status = Unbonded - - transfer(address of the bonded pool, address of the unbonded pool, tokens) - // lastly update the status - candidate.Status = Revoked - - // deduct shares from the candidate and save - if candidate.GlobalStakeShares.IsZero() then - removeCandidate(store, tx.PubKey) - else - saveCandidate(store, candidate) - - saveGlobalState(store, gs) - return + gs.UnbondedShares -= globalPoolSharesToRemove + removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * globalPoolSharesToRemove + gs.UnbondedPool -= removedTokens + + candidate.GlobalStakeShares -= removedTokens + candidate.IssuedDelegatorShares -= shares + return returnedCoins + +delegatorShareExRate(candidate Candidate): + if candidate.IssuedDelegatorShares.IsZero() then return rational.One + return candidate.GlobalStakeShares / candidate.IssuedDelegatorShares + +bondedToUnbondedPool(candidate Candidate): + removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * candidate.GlobalStakeShares + gs.BondedShares -= candidate.GlobalStakeShares + gs.BondedPool -= removedTokens -removeDelegatorBond(candidate Candidate): - - // first remove from the list of bonds - pks = loadDelegatorCandidates(store, sender) - for i, pk := range pks { - if candidate.Equals(pk) { - pks = append(pks[:i], pks[i+1:]...) - } - } - b := wire.BinaryBytes(pks) - store.Set(GetDelegatorBondsKey(delegator), b) - - // now remove the actual bond - store.Remove(GetDelegatorBondKey(delegator, candidate)) - //updateDelegatorBonds(store, delegator) -} + gs.UnbondedPool += removedTokens + issuedShares = removedTokens / exchangeRate(gs.UnbondedShares, gs.UnbondedPool) + gs.UnbondedShares += issuedShares + + candidate.GlobalStakeShares = issuedShares + candidate.Status = Unbonded + + return transfer(address of the bonded pool, address of the unbonded pool, removedTokens) ``` -### Inflation provisions +### TxRedelegate -Validator provisions are minted on an hourly basis (the first block of a new -hour). The annual target of between 7% and 20%. The long-term target ratio of -bonded tokens to unbonded tokens is 67%. +The re-delegation command allows delegators to switch validators while still +receiving equal reward to as if they had never unbonded. + +```golang +type TxRedelegate struct { + PubKeyFrom crypto.PubKey + PubKeyTo crypto.PubKey + Shares rational.Rat +} + +redelegate(tx TxRedelegate): + bond = loadDelegatorBond(store, sender, tx.PubKey) + if bond == nil then return -The target annual inflation rate is recalculated for each previsions cycle. The -inflation is also subject to a rate change (positive of negative) depending or -the distance from the desired ratio (67%). The maximum rate change possible is -defined to be 13% per year, however the annual inflation is capped as between -7% and 20%. + if bond.Shares < tx.Shares return + candidate = loadCandidate(store, tx.PubKeyFrom) + if candidate == nil return + candidate.RedelegatingShares += tx.Shares + reDelegationElem = QueueElemReDelegate(tx.PubKeyFrom, currentHeight(), sender, tx.Shares, tx.PubKeyTo) + redelegationQueue.add(reDelegationElem) + return ``` -inflationRateChange(0) = 0 -GlobalState.Inflation(0) = 0.07 - -bondedRatio = GlobalState.BondedPool / GlobalState.TotalSupply -AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13 -annualInflation += AnnualInflationRateChange +### TxLivelinessCheck + +Liveliness issues are calculated by keeping track of the block precommits in +the block header. A queue is persisted which contains the block headers from +all recent blocks for the duration of the unbonding period. A validator is +defined as having livliness issues if they have not been included in more than +33% of the blocks over: +* The most recent 24 Hours if they have >= 20% of global stake +* The most recent week if they have = 0% of global stake +* Linear interpolation of the above two scenarios -if annualInflation > 0.20 then GlobalState.Inflation = 0.20 -if annualInflation < 0.07 then GlobalState.Inflation = 0.07 +Liveliness kicks are only checked when a `TxLivelinessCheck` transaction is +submitted. -provisionTokensHourly = GlobalState.TotalSupply * GlobalState.Inflation / (365.25*24) +```golang +type TxLivelinessCheck struct { + PubKey crypto.PubKey + RewardAccount Addresss +} ``` -Because the validators hold a relative bonded share (`GlobalStakeShares`), when -more bonded tokens are added proportionally to all validators, the only term -which needs to be updated is the `GlobalState.BondedPool`. So for each previsions -cycle: +If the `TxLivelinessCheck` is successful in kicking a validator, 5% of the +liveliness punishment is provided as a reward to `RewardAccount`. + +### TxProveLive +If the validator was kicked for liveliness issues and is able to regain +liveliness then all delegators in the temporary unbonding pool which have not +transacted to move will be bonded back to the now-live validator and begin to +once again collect provisions and rewards. Regaining liveliness is demonstrated +by sending in a `TxProveLive` transaction: + +```golang +type TxProveLive struct { + PubKey crypto.PubKey +} ``` -GlobalState.BondedPool += provisionTokensHourly + +### End of block handling + +```golang +tick(ctx Context): + hrsPerYr = 8766 // as defined by a julian year of 365.25 days + + time = ctx.Time() + if time > gs.InflationLastTime + ProvisionTimeout + gs.InflationLastTime = time + gs.Inflation = nextInflation(hrsPerYr).Round(1000000000) + + provisions = gs.Inflation * (gs.TotalSupply / hrsPerYr) + + gs.BondedPool += provisions + gs.TotalSupply += provisions + + saveGlobalState(store, gs) + + if time > unbondDelegationQueue.head().InitTime + UnbondingPeriod + for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do + transfer(unbondingQueueAddress, elem.Payout, elem.Tokens) + unbondDelegationQueue.remove(elem) + + if time > reDelegationQueue.head().InitTime + UnbondingPeriod + for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do + candidate = getCandidate(store, elem.PubKey) + returnedCoins = removeShares(candidate, elem.Shares) + candidate.RedelegatingShares -= elem.Shares + delegateWithCandidate(TxDelegate(elem.NewCandidate, returnedCoins), candidate) + reDelegationQueue.remove(elem) + + return UpdateValidatorSet() + +nextInflation(hrsPerYr rational.Rat): + if gs.TotalSupply > 0 + bondedRatio = gs.BondedPool / gs.TotalSupply + else + bondedRation = 0 + + inflationRateChangePerYear = (1 - bondedRatio / params.GoalBonded) * params.InflationRateChange + inflationRateChange = inflationRateChangePerYear / hrsPerYr + + inflation = gs.Inflation + inflationRateChange + if inflation > params.InflationMax then inflation = params.InflationMax + + if inflation < params.InflationMin then inflation = params.InflationMin + + return inflation + +UpdateValidatorSet(): + candidates = loadCandidates(store) + + v1 = candidates.Validators() + v2 = updateVotingPower(candidates).Validators() + + change = v1.validatorsUpdated(v2) // determine all updated validators between two validator sets + return change + +updateVotingPower(candidates Candidates): + foreach candidate in candidates do + candidate.VotingPower = (candidate.IssuedDelegatorShares - candidate.RedelegatingShares) * delegatorShareExRate(candidate) + + candidates.Sort() + + foreach candidate in candidates do + if candidate is not in the first params.MaxVals + candidate.VotingPower = rational.Zero + if candidate.Status == Bonded then bondedToUnbondedPool(candidate Candidate) + + else if candidate.Status == UnBonded then unbondedToBondedPool(candidate) + + saveCandidate(store, c) + + return candidates + +unbondedToBondedPool(candidate Candidate): + removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * candidate.GlobalStakeShares + gs.UnbondedShares -= candidate.GlobalStakeShares + gs.UnbondedPool -= removedTokens + + gs.BondedPool += removedTokens + issuedShares = removedTokens / exchangeRate(gs.BondedShares, gs.BondedPool) + gs.BondedShares += issuedShares + + candidate.GlobalStakeShares = issuedShares + candidate.Status = Bonded + + return transfer(address of the unbonded pool, address of the bonded pool, removedTokens) ``` diff --git a/examples/basecoin/.gitignore b/examples/basecoin/.gitignore deleted file mode 100644 index 25e54fd6bbad..000000000000 --- a/examples/basecoin/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -*.swp -*.swo -vendor -build -app/data - - -### Vagrant ### -.vagrant/ -*.box -*.log -vagrant diff --git a/examples/basecoin/Makefile b/examples/basecoin/Makefile index dac7dd213d79..e0cf14caa684 100644 --- a/examples/basecoin/Makefile +++ b/examples/basecoin/Makefile @@ -1,11 +1,18 @@ PACKAGES=$(shell go list ./... | grep -v '/vendor/') BUILD_FLAGS = -ldflags "-X github.com/cosmos/cosmos-sdk/examples/basecoin/version.GitCommit=`git rev-parse --short HEAD`" -all: build test +all: get_tools get_vendor_deps build test + +get_tools: + go get github.com/golang/dep/cmd/dep build: go build $(BUILD_FLAGS) -o build/basecoin ./cmd/... +get_vendor_deps: + @rm -rf vendor/ + @dep ensure + test: @go test $(PACKAGES) diff --git a/examples/basecoin/README.md b/examples/basecoin/README.md index ed667535deb5..e6de9480db8f 100644 --- a/examples/basecoin/README.md +++ b/examples/basecoin/README.md @@ -1,8 +1,70 @@ -This is the "Basecoin" example application built on the Cosmos-SDK. This +# Basecoin + +This is the "Basecoin" example application built on the Cosmos-Sdk. This "Basecoin" is not affiliated with [Coinbase](http://www.getbasecoin.com/), nor the [stable coin](http://www.getbasecoin.com/). -Assuming you've run `make get_tools && make get_vendor_deps` from the root of this repository, -run `make build` here to build the `basecoind` and `basecli` binaries. +Assuming you've run `make get_tools && make get_vendor_deps` from the root of +this repository, run `make build` here to build the `basecoind` and `basecli` +binaries. If you want to create a new application, start by copying the Basecoin app. + + +# Building your own Blockchain + +Basecoin is the equivalent of an ERC20 token contract for blockchains. In order +to deploy your own application all you need to do is clone `examples/basecoin` +and run it. Now you are already running your own blockchain. In the following +I will explain how to add functionality to your blockchain. This is akin to +defining your own vesting schedule within a contract or setting a specific +multisig. You are just extending the base layer with extra functionality here +and there. + +## Structure of Basecoin + +Basecoin is build with the cosmos-sdk. It is a sample application that works +with any engine that implements the ABCI protocol. Basecoin defines multiple +unique modules as well as uses modules directly from the sdk. If you want +to modify Basecoin, you either remove or add modules according to your wishes. + + +## Modules + +A module is a fundamental unit in the cosmos-sdk. A module defines its own +transaction, handles its own state as well as its own state transition logic. +Globally, in the `app/app.go` file you just have to define a key for that +module to access some parts of the state, as well as initialise the module +object and finally add it to the transaction router. The router ensures that +every module only gets its own messages. + + +## Transactions + +A user can send a transaction to the running blockchain application. This +transaction can be of any of the ones that are supported by any of the +registered modules. + +### CheckTx + +Once a user has submitted their transaction to the engine, +the engine will first run `checkTx` to confirm that it is a valid transaction. +The module has to define a handler that knows how to handle every transaction +type. The corresponding handler gets invoked with the checkTx flag set to true. +This means that the handler shouldn't do any expensive operations, but it can +and should write to the checkTx state. + +### DeliverTx + +The engine calls `deliverTx` when a new block has been agreed upon in +consensus. Again, the corresponding module will have its handler invoked +and the state and context is passed in. During deliverTx execution the +transaction needs to be processed fully and the results are written to the +application state. + + +## CLI + +The cosmos-sdk contains a number of helper libraries in `clients/` to build cli +and RPC interfaces for your specific application. + diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 34778456b200..1d31b0edc4f4 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -4,18 +4,21 @@ import ( "encoding/json" abci "github.com/tendermint/abci/types" - crypto "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + oldwire "github.com/tendermint/go-wire" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" bam "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/ibc" + "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" "github.com/cosmos/cosmos-sdk/examples/basecoin/x/sketchy" ) @@ -29,8 +32,9 @@ type BasecoinApp struct { cdc *wire.Codec // keys to access the substores - capKeyMainStore *sdk.KVStoreKey - capKeyIBCStore *sdk.KVStoreKey + capKeyMainStore *sdk.KVStoreKey + capKeyIBCStore *sdk.KVStoreKey + capKeyStakingStore *sdk.KVStoreKey // Manage getting and setting accounts accountMapper sdk.AccountMapper @@ -39,10 +43,11 @@ type BasecoinApp struct { func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // create your application object var app = &BasecoinApp{ - BaseApp: bam.NewBaseApp(appName, logger, db), - cdc: MakeCodec(), - capKeyMainStore: sdk.NewKVStoreKey("main"), - capKeyIBCStore: sdk.NewKVStoreKey("ibc"), + BaseApp: bam.NewBaseApp(appName, logger, db), + cdc: MakeCodec(), + capKeyMainStore: sdk.NewKVStoreKey("main"), + capKeyIBCStore: sdk.NewKVStoreKey("ibc"), + capKeyStakingStore: sdk.NewKVStoreKey("staking"), } // define the accountMapper @@ -53,16 +58,20 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // add handlers coinKeeper := bank.NewCoinKeeper(app.accountMapper) + coolMapper := cool.NewMapper(app.capKeyMainStore) + ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) + stakingMapper := staking.NewMapper(app.capKeyStakingStore) app.Router(). AddRoute("bank", bank.NewHandler(coinKeeper)). - AddRoute("sketchy", sketchy.NewHandler()) + AddRoute("cool", cool.NewHandler(coinKeeper, coolMapper)). + AddRoute("sketchy", sketchy.NewHandler()). + AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). + AddRoute("staking", staking.NewHandler(stakingMapper, coinKeeper)) // initialize BaseApp app.SetTxDecoder(app.txDecoder) app.SetInitChainer(app.initChainer) - // TODO: mounting multiple stores is broken - // https://github.com/cosmos/cosmos-sdk/issues/532 - app.MountStoresIAVL(app.capKeyMainStore) // , app.capKeyIBCStore) + app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore, app.capKeyStakingStore) app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper)) err := app.LoadLatestVersion(app.capKeyMainStore) if err != nil { @@ -73,22 +82,55 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { } // custom tx codec +// TODO: use new go-wire func MakeCodec() *wire.Codec { + const msgTypeSend = 0x1 + const msgTypeIssue = 0x2 + const msgTypeQuiz = 0x3 + const msgTypeSetTrend = 0x4 + const msgTypeIBCTransferMsg = 0x5 + const msgTypeIBCReceiveMsg = 0x6 + const msgTypeBondMsg = 0x7 + const msgTypeUnbondMsg = 0x8 + var _ = oldwire.RegisterInterface( + struct{ sdk.Msg }{}, + oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, + oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, + oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, + oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, + oldwire.ConcreteType{ibc.IBCTransferMsg{}, msgTypeIBCTransferMsg}, + oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, + oldwire.ConcreteType{staking.BondMsg{}, msgTypeBondMsg}, + oldwire.ConcreteType{staking.UnbondMsg{}, msgTypeUnbondMsg}, + ) + + const accTypeApp = 0x1 + var _ = oldwire.RegisterInterface( + struct{ sdk.Account }{}, + oldwire.ConcreteType{&types.AppAccount{}, accTypeApp}, + ) cdc := wire.NewCodec() - cdc.RegisterInterface((*sdk.Msg)(nil), nil) - bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. - crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + + // cdc.RegisterInterface((*sdk.Msg)(nil), nil) + // bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. + // crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + // ibc.RegisterWire(cdc) // Register ibc.[IBCTransferMsg, IBCReceiveMsg] types. return cdc } // custom logic for transaction decoding func (app *BasecoinApp) txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { var tx = sdk.StdTx{} + + if len(txBytes) == 0 { + return nil, sdk.ErrTxDecode("txBytes are empty") + } + // StdTx.Msg is an interface. The concrete types // are registered by MakeTxCodec in bank.RegisterWire. err := app.cdc.UnmarshalBinary(txBytes, &tx) if err != nil { - return nil, sdk.ErrTxParse("").TraceCause(err, "") + return nil, sdk.ErrTxDecode("").TraceCause(err, "") } return tx, nil } diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index b6ed35b16e21..f97ae30bf6de 100644 --- a/examples/basecoin/app/app_test.go +++ b/examples/basecoin/app/app_test.go @@ -10,9 +10,11 @@ import ( "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/ibc" abci "github.com/tendermint/abci/types" crypto "github.com/tendermint/go-crypto" @@ -20,54 +22,93 @@ import ( "github.com/tendermint/tmlibs/log" ) +// Construct some global addrs and txs for tests. +var ( + chainID = "" // TODO + + priv1 = crypto.GenPrivKeyEd25519() + addr1 = priv1.PubKey().Address() + addr2 = crypto.GenPrivKeyEd25519().PubKey().Address() + coins = sdk.Coins{{"foocoin", 10}} + fee = sdk.StdFee{ + sdk.Coins{{"foocoin", 0}}, + 0, + } + + sendMsg = bank.SendMsg{ + Inputs: []bank.Input{bank.NewInput(addr1, coins)}, + Outputs: []bank.Output{bank.NewOutput(addr2, coins)}, + } + + quizMsg1 = cool.QuizMsg{ + Sender: addr1, + CoolAnswer: "icecold", + } + + quizMsg2 = cool.QuizMsg{ + Sender: addr1, + CoolAnswer: "badvibesonly", + } + + setTrendMsg1 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "icecold", + } + + setTrendMsg2 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "badvibesonly", + } + + setTrendMsg3 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "warmandkind", + } +) + func newBasecoinApp() *BasecoinApp { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") db := dbm.NewMemDB() return NewBasecoinApp(logger, db) } -func TestSendMsg(t *testing.T) { +//_______________________________________________________________________ + +func TestMsgs(t *testing.T) { bapp := newBasecoinApp() - // Construct a SendMsg - var msg = bank.SendMsg{ - Inputs: []bank.Input{ - { - Address: crypto.Address([]byte("input")), - Coins: sdk.Coins{{"atom", 10}}, - Sequence: 1, - }, - }, - Outputs: []bank.Output{ - { - Address: crypto.Address([]byte("output")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + msgs := []struct { + msg sdk.Msg + }{ + {sendMsg}, + {quizMsg1}, + {setTrendMsg1}, } - priv := crypto.GenPrivKeyEd25519() - sig := priv.Sign(msg.GetSignBytes()) - tx := sdk.NewStdTx(msg, []sdk.StdSignature{{ - PubKey: priv.PubKey(), - Signature: sig, - }}) - - // just marshal/unmarshal! - cdc := MakeCodec() - txBytes, err := cdc.MarshalBinary(tx) - require.NoError(t, err) - - // Run a Check - cres := bapp.CheckTx(txBytes) - assert.Equal(t, sdk.CodeUnrecognizedAddress, - sdk.CodeType(cres.Code), cres.Log) - - // Simulate a Block - bapp.BeginBlock(abci.RequestBeginBlock{}) - dres := bapp.DeliverTx(txBytes) - assert.Equal(t, sdk.CodeUnrecognizedAddress, - sdk.CodeType(dres.Code), dres.Log) + sequences := []int64{0} + for i, m := range msgs { + sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, m.msg)) + tx := sdk.NewStdTx(m.msg, fee, []sdk.StdSignature{{ + PubKey: priv1.PubKey(), + Signature: sig, + }}) + + // just marshal/unmarshal! + cdc := MakeCodec() + txBytes, err := cdc.MarshalBinary(tx) + require.NoError(t, err, "i: %v", i) + + // Run a Check + cres := bapp.CheckTx(txBytes) + assert.Equal(t, sdk.CodeUnknownAddress, + sdk.CodeType(cres.Code), "i: %v, log: %v", i, cres.Log) + + // Simulate a Block + bapp.BeginBlock(abci.RequestBeginBlock{}) + dres := bapp.DeliverTx(txBytes) + assert.Equal(t, sdk.CodeUnknownAddress, + sdk.CodeType(dres.Code), "i: %v, log: %v", i, dres.Log) + } } func TestGenesis(t *testing.T) { @@ -101,28 +142,19 @@ func TestGenesis(t *testing.T) { ctx := bapp.BaseApp.NewContext(true, abci.Header{}) res1 := bapp.accountMapper.GetAccount(ctx, baseAcc.Address) assert.Equal(t, acc, res1) - - // reload app and ensure the account is still there - bapp = NewBasecoinApp(logger, db) - ctx = bapp.BaseApp.NewContext(true, abci.Header{}) - res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) - assert.Equal(t, acc, res1) - + /* + // reload app and ensure the account is still there + bapp = NewBasecoinApp(logger, db) + ctx = bapp.BaseApp.NewContext(true, abci.Header{}) + res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) + assert.Equal(t, acc, res1) + */ } func TestSendMsgWithAccounts(t *testing.T) { bapp := newBasecoinApp() // Construct some genesis bytes to reflect basecoin/types/AppAccount - // First key goes in genesis, used for sending - priv1 := crypto.GenPrivKeyEd25519() - pk1 := priv1.PubKey() - addr1 := pk1.Address() - - // Second key receies - pk2 := crypto.GenPrivKeyEd25519().PubKey() - addr2 := pk2.Address() - // Give 77 foocoin to the first key coins, err := sdk.ParseCoins("77foocoin") require.Nil(t, err) @@ -139,6 +171,7 @@ func TestSendMsgWithAccounts(t *testing.T) { }, } stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) // Initialize the chain vals := []abci.Validator{} @@ -147,30 +180,13 @@ func TestSendMsgWithAccounts(t *testing.T) { // A checkTx context (true) ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) - res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) assert.Equal(t, acc1, res1) - // Construct a SendMsg - var msg = bank.SendMsg{ - Inputs: []bank.Input{ - { - Address: crypto.Address(addr1), - Coins: sdk.Coins{{"foocoin", 10}}, - Sequence: 1, - }, - }, - Outputs: []bank.Output{ - { - Address: crypto.Address(addr2), - Coins: sdk.Coins{{"foocoin", 10}}, - }, - }, - } - // Sign the tx - sig := priv1.Sign(msg.GetSignBytes()) - tx := sdk.NewStdTx(msg, []sdk.StdSignature{{ + sequences := []int64{0} + sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, sendMsg)) + tx := sdk.NewStdTx(sendMsg, fee, []sdk.StdSignature{{ PubKey: priv1.PubKey(), Signature: sig, }}) @@ -184,13 +200,160 @@ func TestSendMsgWithAccounts(t *testing.T) { res = bapp.Deliver(tx) assert.Equal(t, sdk.CodeOK, res.Code, res.Log) - // A deliverTx context - ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) - // Check balances + ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) res3 := bapp.accountMapper.GetAccount(ctxDeliver, addr2) - assert.Equal(t, fmt.Sprintf("%v", res2.GetCoins()), "67foocoin") assert.Equal(t, fmt.Sprintf("%v", res3.GetCoins()), "10foocoin") + + // Delivering again should cause replay error + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeInvalidSequence, res.Code, res.Log) + + // bumping the txnonce number without resigning should be an auth error + tx.Signatures[0].Sequence = 1 + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeUnauthorized, res.Code, res.Log) + + // resigning the tx with the bumped sequence should work + sequences = []int64{1} + sig = priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, tx.Msg)) + tx.Signatures[0].Signature = sig + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeOK, res.Code, res.Log) +} + +func TestQuizMsg(t *testing.T) { + bapp := newBasecoinApp() + + // Construct genesis state + // Construct some genesis bytes to reflect basecoin/types/AppAccount + coins := sdk.Coins{} + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + + // Construct genesis state + genesisState := types.GenesisState{ + Accounts: []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + + // Initialize the chain (nil) + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + // Set the trend, submit a really cool quiz and check for reward + SignCheckDeliver(t, bapp, setTrendMsg1, 0, true) + SignCheckDeliver(t, bapp, quizMsg1, 1, true) + CheckBalance(t, bapp, "69icecold") + SignCheckDeliver(t, bapp, quizMsg2, 2, true) // result without reward + CheckBalance(t, bapp, "69icecold") + SignCheckDeliver(t, bapp, quizMsg1, 3, true) + CheckBalance(t, bapp, "138icecold") + SignCheckDeliver(t, bapp, setTrendMsg2, 4, true) // reset the trend + SignCheckDeliver(t, bapp, quizMsg1, 5, true) // the same answer will nolonger do! + CheckBalance(t, bapp, "138icecold") + SignCheckDeliver(t, bapp, quizMsg2, 6, true) // earlier answer now relavent again + CheckBalance(t, bapp, "69badvibesonly,138icecold") + SignCheckDeliver(t, bapp, setTrendMsg3, 7, false) // expect to fail to set the trend to something which is not cool + +} + +func TestHandler(t *testing.T) { + bapp := newBasecoinApp() + + sourceChain := "source-chain" + destChain := "dest-chain" + + vals := []abci.Validator{} + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + genesisState := types.GenesisState{ + Accounts: []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + packet := ibc.IBCPacket{ + SrcAddr: addr1, + DestAddr: addr1, + Coins: coins, + SrcChain: sourceChain, + DestChain: destChain, + } + + transferMsg := ibc.IBCTransferMsg{ + IBCPacket: packet, + } + + receiveMsg := ibc.IBCReceiveMsg{ + IBCPacket: packet, + Relayer: addr1, + Sequence: 0, + } + + SignCheckDeliver(t, bapp, transferMsg, 0, true) + CheckBalance(t, bapp, "") + SignCheckDeliver(t, bapp, transferMsg, 1, false) + SignCheckDeliver(t, bapp, receiveMsg, 2, true) + CheckBalance(t, bapp, "10foocoin") + SignCheckDeliver(t, bapp, receiveMsg, 3, false) +} + +func SignCheckDeliver(t *testing.T, bapp *BasecoinApp, msg sdk.Msg, seq int64, expPass bool) { + + // Sign the tx + tx := sdk.NewStdTx(msg, fee, []sdk.StdSignature{{ + PubKey: priv1.PubKey(), + Signature: priv1.Sign(sdk.StdSignBytes(chainID, []int64{seq}, fee, msg)), + Sequence: seq, + }}) + + // Run a Check + res := bapp.Check(tx) + if expPass { + require.Equal(t, sdk.CodeOK, res.Code, res.Log) + } else { + require.NotEqual(t, sdk.CodeOK, res.Code, res.Log) + } + + // Simulate a Block + bapp.BeginBlock(abci.RequestBeginBlock{}) + res = bapp.Deliver(tx) + if expPass { + require.Equal(t, sdk.CodeOK, res.Code, res.Log) + } else { + require.NotEqual(t, sdk.CodeOK, res.Code, res.Log) + } +} + +func CheckBalance(t *testing.T, bapp *BasecoinApp, balExpected string) { + ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) + res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) + assert.Equal(t, balExpected, fmt.Sprintf("%v", res2.GetCoins())) } diff --git a/examples/basecoin/cmd/basecli/main.go b/examples/basecoin/cmd/basecli/main.go index 638071d14f0e..6fabc612c77f 100644 --- a/examples/basecoin/cmd/basecli/main.go +++ b/examples/basecoin/cmd/basecli/main.go @@ -2,9 +2,8 @@ package main import ( "errors" - "os" - "github.com/spf13/cobra" + "os" "github.com/tendermint/tmlibs/cli" @@ -13,9 +12,13 @@ import ( "github.com/cosmos/cosmos-sdk/client/lcd" "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/client/tx" + + coolcmd "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool/commands" "github.com/cosmos/cosmos-sdk/version" authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands" bankcmd "github.com/cosmos/cosmos-sdk/x/bank/commands" + ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/commands" + stakingcmd "github.com/cosmos/cosmos-sdk/x/staking/commands" "github.com/cosmos/cosmos-sdk/examples/basecoin/app" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" @@ -40,6 +43,10 @@ func main() { // get the codec cdc := app.MakeCodec() + // TODO: setup keybase, viper object, etc. to be passed into + // the below functions and eliminate global vars, like we do + // with the cdc + // add standard rpc, and tx commands rpc.AddCommands(basecliCmd) basecliCmd.AddCommand(client.LineBreak) @@ -49,17 +56,38 @@ func main() { // add query/post commands (custom to binary) basecliCmd.AddCommand( client.GetCommands( - authcmd.GetAccountCmd("main", cdc, types.GetParseAccount(cdc)), + authcmd.GetAccountCmd("main", cdc, types.GetAccountDecoder(cdc)), )...) basecliCmd.AddCommand( client.PostCommands( bankcmd.SendTxCmd(cdc), )...) + basecliCmd.AddCommand( + client.PostCommands( + coolcmd.QuizTxCmd(cdc), + )...) + basecliCmd.AddCommand( + client.PostCommands( + coolcmd.SetTrendTxCmd(cdc), + )...) + basecliCmd.AddCommand( + client.PostCommands( + ibccmd.IBCTransferCmd(cdc), + )...) + basecliCmd.AddCommand( + client.PostCommands( + ibccmd.IBCRelayCmd(cdc), + stakingcmd.BondTxCmd(cdc), + )...) + basecliCmd.AddCommand( + client.PostCommands( + stakingcmd.UnbondTxCmd(cdc), + )...) // add proxy, version and key info basecliCmd.AddCommand( client.LineBreak, - lcd.ServeCommand(), + lcd.ServeCommand(cdc), keys.Commands(), client.LineBreak, version.VersionCmd, diff --git a/examples/basecoin/cmd/basecoind/main.go b/examples/basecoin/cmd/basecoind/main.go index 44ea00fbbab3..ea49c0b970e5 100644 --- a/examples/basecoin/cmd/basecoind/main.go +++ b/examples/basecoin/cmd/basecoind/main.go @@ -59,6 +59,7 @@ func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { } func main() { + // TODO: set logger through CLI logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). With("module", "main") @@ -66,6 +67,7 @@ func main() { server.InitCmd(defaultOptions, logger), server.StartCmd(generateApp, logger), server.UnsafeResetAllCmd(logger), + server.ShowNodeIdCmd(logger), version.VersionCmd, ) diff --git a/examples/basecoin/types/account.go b/examples/basecoin/types/account.go index f5b4f7aa012e..f34113fc65a8 100644 --- a/examples/basecoin/types/account.go +++ b/examples/basecoin/types/account.go @@ -2,9 +2,8 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" - crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" ) var _ sdk.Account = (*AppAccount)(nil) @@ -23,11 +22,17 @@ type AppAccount struct { func (acc AppAccount) GetName() string { return acc.Name } func (acc *AppAccount) SetName(name string) { acc.Name = name } -// Get the ParseAccount function for the custom AppAccount -func GetParseAccount(cdc *wire.Codec) sdk.ParseAccount { +// Get the AccountDecoder function for the custom AppAccount +func GetAccountDecoder(cdc *wire.Codec) sdk.AccountDecoder { return func(accBytes []byte) (res sdk.Account, err error) { + if len(accBytes) == 0 { + return nil, sdk.ErrTxDecode("accBytes are empty") + } acct := new(AppAccount) - err = cdc.UnmarshalBinary(accBytes, acct) + err = cdc.UnmarshalBinary(accBytes, &acct) + if err != nil { + panic(err) + } return acct, err } } @@ -41,9 +46,9 @@ type GenesisState struct { // GenesisAccount doesn't need pubkey or sequence type GenesisAccount struct { - Name string `json:"name"` - Address crypto.Address `json:"address"` - Coins sdk.Coins `json:"coins"` + Name string `json:"name"` + Address sdk.Address `json:"address"` + Coins sdk.Coins `json:"coins"` } func NewGenesisAccount(aa *AppAccount) *GenesisAccount { diff --git a/examples/basecoin/x/cool/commands/tx.go b/examples/basecoin/x/cool/commands/tx.go new file mode 100644 index 000000000000..f06eb8af423f --- /dev/null +++ b/examples/basecoin/x/cool/commands/tx.go @@ -0,0 +1,99 @@ +package commands + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + "github.com/cosmos/cosmos-sdk/wire" + + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" +) + +// take the coolness quiz transaction +func QuizTxCmd(cdc *wire.Codec) *cobra.Command { + return &cobra.Command{ + Use: "cool [answer]", + Short: "What's cooler than being cool?", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide an answer") + } + + // get the from address from the name flag + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + // create the message + msg := cool.NewQuizMsg(from, args[0]) + + // get account name + name := viper.GetString(client.FlagName) + + // get password + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } +} + +// set a new cool trend transaction +func SetTrendTxCmd(cdc *wire.Codec) *cobra.Command { + return &cobra.Command{ + Use: "setcool [answer]", + Short: "You're so cool, tell us what is cool!", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide an answer") + } + + // get the from address from the name flag + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + // get account name + name := viper.GetString(client.FlagName) + + // get password + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return err + } + + // create the message + msg := cool.NewSetTrendMsg(from, args[0]) + + // build and sign the transaction, then broadcast to Tendermint + res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } +} diff --git a/examples/basecoin/x/cool/handler.go b/examples/basecoin/x/cool/handler.go new file mode 100644 index 000000000000..c0aae11bba73 --- /dev/null +++ b/examples/basecoin/x/cool/handler.go @@ -0,0 +1,56 @@ +package cool + +import ( + "fmt" + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// This is just an example to demonstrate a functional custom module +// with full feature set functionality. +// +// /$$$$$$$ /$$$$$$ /$$$$$$ /$$ +// /$$_____/ /$$__ $$ /$$__ $$| $$ +//| $$ | $$ \ $$| $$ \ $$| $$ +//| $$ | $$ | $$| $$ | $$| $$ +//| $$$$$$$| $$$$$$/| $$$$$$/| $$$$$$$ +// \_______/ \______/ \______/ |______/ + +// Handle all "coolmodule" type objects +func NewHandler(ck bank.CoinKeeper, cm Mapper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case SetTrendMsg: + return handleSetTrendMsg(ctx, cm, msg) + case QuizMsg: + return handleQuizMsg(ctx, ck, cm, msg) + default: + errMsg := fmt.Sprintf("Unrecognized cool Msg type: %v", reflect.TypeOf(msg).Name()) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// Handle QuizMsg This is the engine of your module +func handleSetTrendMsg(ctx sdk.Context, cm Mapper, msg SetTrendMsg) sdk.Result { + cm.SetTrend(ctx, msg.Cool) + return sdk.Result{} +} + +// Handle QuizMsg This is the engine of your module +func handleQuizMsg(ctx sdk.Context, ck bank.CoinKeeper, cm Mapper, msg QuizMsg) sdk.Result { + + currentTrend := cm.GetTrend(ctx) + + if msg.CoolAnswer == currentTrend { + bonusCoins := sdk.Coins{{currentTrend, 69}} + _, err := ck.AddCoins(ctx, msg.Sender, bonusCoins) + if err != nil { + return err.Result() + } + } + + return sdk.Result{} +} diff --git a/examples/basecoin/x/cool/mapper.go b/examples/basecoin/x/cool/mapper.go new file mode 100644 index 000000000000..2e0a791fa694 --- /dev/null +++ b/examples/basecoin/x/cool/mapper.go @@ -0,0 +1,30 @@ +package cool + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// This Cool Mapper handlers sets/gets of custom variables for your module +type Mapper struct { + key sdk.StoreKey // The (unexposed) key used to access the store from the Context. +} + +func NewMapper(key sdk.StoreKey) Mapper { + return Mapper{key} +} + +// Key to knowing the trend on the streets! +var trendKey = []byte("TrendKey") + +// Implements sdk.AccountMapper. +func (am Mapper) GetTrend(ctx sdk.Context) string { + store := ctx.KVStore(am.key) + bz := store.Get(trendKey) + return string(bz) +} + +// Implements sdk.AccountMapper. +func (am Mapper) SetTrend(ctx sdk.Context, newTrend string) { + store := ctx.KVStore(am.key) + store.Set(trendKey, []byte(newTrend)) +} diff --git a/examples/basecoin/x/cool/types.go b/examples/basecoin/x/cool/types.go new file mode 100644 index 000000000000..a3fa6ca48e7c --- /dev/null +++ b/examples/basecoin/x/cool/types.go @@ -0,0 +1,103 @@ +package cool + +import ( + "encoding/json" + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// A really cool msg type, these fields are can be entirely arbitrary and +// custom to your message +type SetTrendMsg struct { + Sender sdk.Address + Cool string +} + +// New cool message +func NewSetTrendMsg(sender sdk.Address, cool string) SetTrendMsg { + return SetTrendMsg{ + Sender: sender, + Cool: cool, + } +} + +// enforce the msg type at compile time +var _ sdk.Msg = SetTrendMsg{} + +// nolint +func (msg SetTrendMsg) Type() string { return "cool" } +func (msg SetTrendMsg) Get(key interface{}) (value interface{}) { return nil } +func (msg SetTrendMsg) GetSigners() []sdk.Address { return []sdk.Address{msg.Sender} } +func (msg SetTrendMsg) String() string { + return fmt.Sprintf("SetTrendMsg{Sender: %v, Cool: %v}", msg.Sender, msg.Cool) +} + +// Validate Basic is used to quickly disqualify obviously invalid messages quickly +func (msg SetTrendMsg) ValidateBasic() sdk.Error { + if len(msg.Sender) == 0 { + return sdk.ErrUnknownAddress(msg.Sender.String()).Trace("") + } + if strings.Contains(msg.Cool, "hot") { + return sdk.ErrUnauthorized("").Trace("hot is not cool") + } + if strings.Contains(msg.Cool, "warm") { + return sdk.ErrUnauthorized("").Trace("warm is not very cool") + } + return nil +} + +// Get the bytes for the message signer to sign on +func (msg SetTrendMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +//_______________________________________________________________________ + +// A message type to quiz how cool you are. these fields are can be entirely +// arbitrary and custom to your message +type QuizMsg struct { + Sender sdk.Address + CoolAnswer string +} + +// New cool message +func NewQuizMsg(sender sdk.Address, coolerthancool string) QuizMsg { + return QuizMsg{ + Sender: sender, + CoolAnswer: coolerthancool, + } +} + +// enforce the msg type at compile time +var _ sdk.Msg = QuizMsg{} + +// nolint +func (msg QuizMsg) Type() string { return "cool" } +func (msg QuizMsg) Get(key interface{}) (value interface{}) { return nil } +func (msg QuizMsg) GetSigners() []sdk.Address { return []sdk.Address{msg.Sender} } +func (msg QuizMsg) String() string { + return fmt.Sprintf("QuizMsg{Sender: %v, CoolAnswer: %v}", msg.Sender, msg.CoolAnswer) +} + +// Validate Basic is used to quickly disqualify obviously invalid messages quickly +func (msg QuizMsg) ValidateBasic() sdk.Error { + if len(msg.Sender) == 0 { + return sdk.ErrUnknownAddress(msg.Sender.String()).Trace("") + } + return nil +} + +// Get the bytes for the message signer to sign on +func (msg QuizMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} diff --git a/examples/kvstore/tx.go b/examples/kvstore/tx.go index b324081e838f..c084925b76c9 100644 --- a/examples/kvstore/tx.go +++ b/examples/kvstore/tx.go @@ -4,7 +4,6 @@ import ( "bytes" sdk "github.com/cosmos/cosmos-sdk/types" - crypto "github.com/tendermint/go-crypto" ) // An sdk.Tx which is its own sdk.Msg. @@ -44,7 +43,7 @@ func (tx kvstoreTx) ValidateBasic() sdk.Error { return nil } -func (tx kvstoreTx) GetSigners() []crypto.Address { +func (tx kvstoreTx) GetSigners() []sdk.Address { return nil } @@ -52,10 +51,6 @@ func (tx kvstoreTx) GetSignatures() []sdk.StdSignature { return nil } -func (tx kvstoreTx) GetFeePayer() crypto.Address { - return nil -} - // takes raw transaction bytes and decodes them into an sdk.Tx. An sdk.Tx has // all the signatures and can be used to authenticate. func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) { @@ -69,7 +64,7 @@ func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) { k, v := split[0], split[1] tx = kvstoreTx{k, v, txBytes} } else { - return nil, sdk.ErrTxParse("too many =") + return nil, sdk.ErrTxDecode("too many =") } return tx, nil diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 9ff8dde6121e..000000000000 --- a/glide.lock +++ /dev/null @@ -1,253 +0,0 @@ -hash: fa45c8a4f5512ed730f793b93d4876bdc604a1333a5a1f938c98a0f7dd55f22e -updated: 2018-03-01T00:41:12.97082395-05:00 -imports: -- name: github.com/bgentry/speakeasy - version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd -- name: github.com/btcsuite/btcd - version: 50de9da05b50eb15658bb350f6ea24368a111ab7 - subpackages: - - btcec -- name: github.com/davecgh/go-spew - version: 346938d642f2ec3594ed81d874461961cd0faa76 - subpackages: - - spew -- name: github.com/ebuchman/fail-test - version: 95f809107225be108efcf10a3509e4ea6ceef3c4 -- name: github.com/fsnotify/fsnotify - version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 -- name: github.com/go-kit/kit - version: 4dc7be5d2d12881735283bcab7352178e190fc71 - subpackages: - - log - - log/level - - log/term -- name: github.com/go-logfmt/logfmt - version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 -- name: github.com/go-stack/stack - version: 259ab82a6cad3992b4e21ff5cac294ccb06474bc -- name: github.com/gogo/protobuf - version: 1adfc126b41513cc696b209667c8656ea7aac67c - subpackages: - - gogoproto - - jsonpb - - proto - - protoc-gen-gogo/descriptor - - sortkeys - - types -- name: github.com/golang/protobuf - version: 925541529c1fa6821df4e44ce2723319eb2be768 - subpackages: - - proto - - ptypes - - ptypes/any - - ptypes/duration - - ptypes/timestamp -- name: github.com/golang/snappy - version: 553a641470496b2327abcac10b36396bd98e45c9 -- name: github.com/gorilla/websocket - version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b -- name: github.com/hashicorp/hcl - version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 - subpackages: - - hcl/ast - - hcl/parser - - hcl/scanner - - hcl/strconv - - hcl/token - - json/parser - - json/scanner - - json/token -- name: github.com/howeyc/crc16 - version: 96a97a1abb579c7ff1a8ffa77f2e72d1c314b57f -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/jmhodges/levigo - version: c42d9e0ca023e2198120196f842701bb4c55d7b9 -- name: github.com/kr/logfmt - version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 -- name: github.com/magiconair/properties - version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934 -- name: github.com/mattn/go-isatty - version: 0360b2af4f38e8d38c7fce2a9f4e702702d73a39 -- name: github.com/mitchellh/mapstructure - version: b4575eea38cca1123ec2dc90c26529b5c5acfcff -- name: github.com/pelletier/go-toml - version: acdc4509485b587f5e675510c4f2c63e90ff68a8 -- name: github.com/pkg/errors - version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/rcrowley/go-metrics - version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c -- name: github.com/rigelrozanski/common - version: f691f115798593d783b9999b1263c2f4ffecc439 -- name: github.com/spf13/afero - version: bb8f1927f2a9d3ab41c9340aa034f6b803f4359c - subpackages: - - mem -- name: github.com/spf13/cast - version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 -- name: github.com/spf13/cobra - version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b -- name: github.com/spf13/jwalterweatherman - version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394 -- name: github.com/spf13/pflag - version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 -- name: github.com/spf13/viper - version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 -- name: github.com/syndtr/goleveldb - version: 34011bf325bce385408353a30b101fe5e923eb6e - subpackages: - - leveldb - - leveldb/cache - - leveldb/comparer - - leveldb/errors - - leveldb/filter - - leveldb/iterator - - leveldb/journal - - leveldb/memdb - - leveldb/opt - - leveldb/storage - - leveldb/table - - leveldb/util -- name: github.com/tendermint/abci - version: 68592f4d8ee34e97db94b7a7976b1309efdb7eb9 - subpackages: - - client - - example/code - - example/dummy - - server - - types -- name: github.com/tendermint/ed25519 - version: d8387025d2b9d158cf4efb07e7ebf814bcce2057 - subpackages: - - edwards25519 - - extra25519 -- name: github.com/tendermint/go-crypto - version: 4fc3055dbd17aa1203d0abc64b9293f378da22ec - subpackages: - - keys - - keys/bcrypt - - keys/words - - keys/words/wordlist -- name: github.com/tendermint/go-wire - version: 5d7845f24b843c914cf571dad2ca13c91cf70f0d -- name: github.com/tendermint/iavl - version: 1a59ec0c82dc940c25339dd7c834df5cb76a95cb -- name: github.com/tendermint/tendermint - version: c330b9e43c93351a5c3040333d7d0c7c27859a20 - subpackages: - - blockchain - - cmd/tendermint/commands - - config - - consensus - - consensus/types - - evidence - - lite - - lite/client - - lite/errors - - lite/files - - lite/proxy - - mempool - - node - - p2p - - p2p/conn - - p2p/pex - - p2p/trust - - p2p/upnp - - proxy - - rpc/client - - rpc/core - - rpc/core/types - - rpc/grpc - - rpc/lib - - rpc/lib/client - - rpc/lib/server - - rpc/lib/types - - state - - state/txindex - - state/txindex/kv - - state/txindex/null - - types - - version - - wire -- name: github.com/tendermint/tmlibs - version: 26f2ab65f82cfc6873c312e8030104c47c05f10e - subpackages: - - autofile - - cli - - cli/flags - - clist - - common - - db - - flowrate - - log - - merkle - - pubsub - - pubsub/query -- name: golang.org/x/crypto - version: 1875d0a70c90e57f11972aefd42276df65e895b9 - subpackages: - - blowfish - - curve25519 - - nacl/box - - nacl/secretbox - - openpgp/armor - - openpgp/errors - - poly1305 - - ripemd160 - - salsa20/salsa -- name: golang.org/x/net - version: 2fb46b16b8dda405028c50f7c7f0f9dd1fa6bfb1 - subpackages: - - context - - http2 - - http2/hpack - - idna - - internal/timeseries - - lex/httplex - - trace -- name: golang.org/x/sys - version: 37707fdb30a5b38865cfb95e5aab41707daec7fd - subpackages: - - unix -- name: golang.org/x/text - version: e19ae1496984b1c655b8044a65c0300a3c878dd3 - subpackages: - - secure/bidirule - - transform - - unicode/bidi - - unicode/norm -- name: google.golang.org/genproto - version: 4eb30f4778eed4c258ba66527a0d4f9ec8a36c45 - subpackages: - - googleapis/rpc/status -- name: google.golang.org/grpc - version: 401e0e00e4bb830a10496d64cd95e068c5bf50de - subpackages: - - balancer - - codes - - connectivity - - credentials - - grpclb/grpc_lb_v1/messages - - grpclog - - internal - - keepalive - - metadata - - naming - - peer - - resolver - - stats - - status - - tap - - transport -- name: gopkg.in/yaml.v2 - version: d670f9405373e636a5a2765eea47fac0c9bc91a4 -testImports: -- name: github.com/pmezard/go-difflib - version: 792786c7400a136282c1664665ae0a8db921c6c2 - subpackages: - - difflib -- name: github.com/stretchr/testify - version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 - subpackages: - - assert - - require diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 086cd0c25cfc..000000000000 --- a/glide.yaml +++ /dev/null @@ -1,54 +0,0 @@ -package: github.com/cosmos/cosmos-sdk -import: -- package: github.com/golang/protobuf - version: ^1.0.0 - subpackages: - - proto -- package: github.com/bgentry/speakeasy - version: ^0.1.0 -- package: github.com/mattn/go-isatty - version: ~0.0.3 -- package: github.com/pkg/errors - version: ^0.8.0 -- package: github.com/rigelrozanski/common -- package: github.com/tendermint/abci - version: develop - subpackages: - - server - - types -- package: github.com/tendermint/go-crypto - version: develop -- package: github.com/tendermint/go-wire - version: develop -- package: github.com/tendermint/iavl - version: develop -- package: github.com/tendermint/tmlibs - version: develop - subpackages: - - common - - db - - log - - merkle -- package: github.com/tendermint/tendermint - version: breaking/wire-sdk2 - subpackages: - - cmd/tendermint/commands - - config - - lite - - rpc/client - - types -- package: golang.org/x/crypto - subpackages: - - ripemd160 -- package: github.com/spf13/pflag - version: v1.0.0 -- package: github.com/spf13/cobra - version: v0.0.1 -- package: github.com/spf13/viper - version: ^1.0.0 -testImport: -- package: github.com/stretchr/testify - version: ^1.2.1 - subpackages: - - assert - - require diff --git a/mock/tx.go b/mock/tx.go index a9adff5deed8..cc79b1172aa0 100644 --- a/mock/tx.go +++ b/mock/tx.go @@ -6,7 +6,6 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - crypto "github.com/tendermint/go-crypto" ) // An sdk.Tx which is its own sdk.Msg. @@ -57,7 +56,7 @@ func (tx kvstoreTx) ValidateBasic() sdk.Error { return nil } -func (tx kvstoreTx) GetSigners() []crypto.Address { +func (tx kvstoreTx) GetSigners() []sdk.Address { return nil } @@ -65,10 +64,6 @@ func (tx kvstoreTx) GetSignatures() []sdk.StdSignature { return nil } -func (tx kvstoreTx) GetFeePayer() crypto.Address { - return nil -} - // takes raw transaction bytes and decodes them into an sdk.Tx. An sdk.Tx has // all the signatures and can be used to authenticate. func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) { @@ -82,7 +77,7 @@ func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) { k, v := split[0], split[1] tx = kvstoreTx{k, v, txBytes} } else { - return nil, sdk.ErrTxParse("too many =") + return nil, sdk.ErrTxDecode("too many =") } return tx, nil diff --git a/server/init.go b/server/init.go index 4dc724587336..12e330dbc83e 100644 --- a/server/init.go +++ b/server/init.go @@ -2,16 +2,11 @@ package server import ( "encoding/json" - "fmt" "io/ioutil" "github.com/spf13/cobra" - crypto "github.com/tendermint/go-crypto" - "github.com/tendermint/go-crypto/keys" - "github.com/tendermint/go-crypto/keys/words" cmn "github.com/tendermint/tmlibs/common" - dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" @@ -20,14 +15,14 @@ import ( ) // InitCmd will initialize all files for tendermint, -// along with proper app_options. +// along with proper app_state. // The application can pass in a function to generate -// proper options. And may want to use GenerateCoinKey +// proper state. And may want to use GenerateCoinKey // to create default account(s). -func InitCmd(gen GenOptions, logger log.Logger) *cobra.Command { +func InitCmd(gen GenAppState, logger log.Logger) *cobra.Command { cmd := initCmd{ - gen: gen, - logger: logger, + genAppState: gen, + logger: logger, } return &cobra.Command{ Use: "init", @@ -36,39 +31,14 @@ func InitCmd(gen GenOptions, logger log.Logger) *cobra.Command { } } -// GenOptions can parse command-line and flag to -// generate default app_options for the genesis file. +// GenAppState can parse command-line and flag to +// generate default app_state for the genesis file. // This is application-specific -type GenOptions func(args []string) (json.RawMessage, error) - -// GenerateCoinKey returns the address of a public key, -// along with the secret phrase to recover the private key. -// You can give coins to this address and return the recovery -// phrase to the user to access them. -func GenerateCoinKey() (crypto.Address, string, error) { - // construct an in-memory key store - codec, err := words.LoadCodec("english") - if err != nil { - return nil, "", err - } - keybase := keys.New( - dbm.NewMemDB(), - codec, - ) - - // generate a private key, with recovery phrase - info, secret, err := keybase.Create("name", "pass", keys.AlgoEd25519) - if err != nil { - return nil, "", err - } - - addr := info.PubKey.Address() - return addr, secret, nil -} +type GenAppState func(args []string) (json.RawMessage, error) type initCmd struct { - gen GenOptions - logger log.Logger + genAppState GenAppState + logger log.Logger } func (c initCmd) run(cmd *cobra.Command, args []string) error { @@ -84,19 +54,19 @@ func (c initCmd) run(cmd *cobra.Command, args []string) error { } // no app_options, leave like tendermint - if c.gen == nil { + if c.genAppState == nil { return nil } - // Now, we want to add the custom app_options - options, err := c.gen(args) + // Now, we want to add the custom app_state + appState, err := c.genAppState(args) if err != nil { return err } // And add them to the genesis file genFile := config.GenesisFile() - return addGenesisOptions(genFile, options) + return addGenesisState(genFile, appState) } // This was copied from tendermint/cmd/tendermint/commands/init.go @@ -140,7 +110,7 @@ func (c initCmd) initTendermintFiles(config *cfg.Config) error { // so we can add one line. type GenesisDoc map[string]json.RawMessage -func addGenesisOptions(filename string, options json.RawMessage) error { +func addGenesisState(filename string, appState json.RawMessage) error { bz, err := ioutil.ReadFile(filename) if err != nil { return err @@ -152,7 +122,7 @@ func addGenesisOptions(filename string, options json.RawMessage) error { return err } - doc["app_state"] = options + doc["app_state"] = appState out, err := json.MarshalIndent(doc, "", " ") if err != nil { return err @@ -160,23 +130,3 @@ func addGenesisOptions(filename string, options json.RawMessage) error { return ioutil.WriteFile(filename, out, 0600) } - -// GetGenesisJSON returns a new tendermint genesis with Basecoin app_options -// that grant a large amount of "mycoin" to a single address -// TODO: A better UX for generating genesis files -func GetGenesisJSON(pubkey, chainID, denom, addr string, options string) string { - return fmt.Sprintf(`{ - "accounts": [{ - "address": "%s", - "coins": [ - { - "denom": "%s", - "amount": 9007199254740992 - } - ] - }], - "plugin_options": [ - "coin/issuer", {"app": "sigs", "addr": "%s"}%s - ] -}`, addr, denom, addr, options) -} diff --git a/server/init_test.go b/server/init_test.go index 0af1ecc11823..0abb18040082 100644 --- a/server/init_test.go +++ b/server/init_test.go @@ -1,11 +1,8 @@ package server import ( - "io/ioutil" - "os" "testing" - "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/tendermint/tmlibs/log" @@ -13,21 +10,8 @@ import ( "github.com/cosmos/cosmos-sdk/mock" ) -// setupViper creates a homedir to run inside, -// and returns a cleanup function to defer -func setupViper() func() { - rootDir, err := ioutil.TempDir("", "mock-sdk-cmd") - if err != nil { - panic(err) // fuck it! - } - viper.Set("home", rootDir) - return func() { - os.RemoveAll(rootDir) - } -} - func TestInit(t *testing.T) { - defer setupViper()() + defer setupViper(t)() logger := log.NewNopLogger() cmd := InitCmd(mock.GenInitOptions, logger) diff --git a/server/key.go b/server/key.go new file mode 100644 index 000000000000..aed1f9d1ff32 --- /dev/null +++ b/server/key.go @@ -0,0 +1,34 @@ +package server + +import ( + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/words" + dbm "github.com/tendermint/tmlibs/db" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GenerateCoinKey returns the address of a public key, +// along with the secret phrase to recover the private key. +// You can give coins to this address and return the recovery +// phrase to the user to access them. +func GenerateCoinKey() (sdk.Address, string, error) { + // construct an in-memory key store + codec, err := words.LoadCodec("english") + if err != nil { + return nil, "", err + } + keybase := keys.New( + dbm.NewMemDB(), + codec, + ) + + // generate a private key, with recovery phrase + info, secret, err := keybase.Create("name", "pass", keys.AlgoEd25519) + if err != nil { + return nil, "", err + } + + addr := info.PubKey.Address() + return addr, secret, nil +} diff --git a/server/show_node_id.go b/server/show_node_id.go new file mode 100644 index 000000000000..f8d9f3f81f6b --- /dev/null +++ b/server/show_node_id.go @@ -0,0 +1,38 @@ +package server + +import ( + "fmt" + + "github.com/spf13/cobra" + + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tmlibs/log" +) + +// ShowNodeIdCmd - ported from Tendermint, dump node ID to stdout +func ShowNodeIdCmd(logger log.Logger) *cobra.Command { + cmd := showNodeId{logger} + return &cobra.Command{ + Use: "show_node_id", + Short: "Show this node's ID", + RunE: cmd.run, + } +} + +type showNodeId struct { + logger log.Logger +} + +func (s showNodeId) run(cmd *cobra.Command, args []string) error { + cfg, err := tcmd.ParseConfig() + if err != nil { + return err + } + nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) + if err != nil { + return err + } + fmt.Println(nodeKey.ID()) + return nil +} diff --git a/server/start.go b/server/start.go index 1424c81532da..c8d9fc4d1d47 100644 --- a/server/start.go +++ b/server/start.go @@ -23,14 +23,14 @@ const ( // appGenerator lets us lazily initialize app, using home dir // and other flags (?) to start -type appGenerator func(string, log.Logger) (abci.Application, error) +type appCreator func(string, log.Logger) (abci.Application, error) // StartCmd runs the service passed in, either // stand-alone, or in-process with tendermint -func StartCmd(app appGenerator, logger log.Logger) *cobra.Command { +func StartCmd(app appCreator, logger log.Logger) *cobra.Command { start := startCmd{ - app: app, - logger: logger, + appCreator: app, + logger: logger, } cmd := &cobra.Command{ Use: "start", @@ -48,8 +48,8 @@ func StartCmd(app appGenerator, logger log.Logger) *cobra.Command { } type startCmd struct { - app appGenerator - logger log.Logger + appCreator appCreator + logger log.Logger } func (s startCmd) run(cmd *cobra.Command, args []string) error { @@ -65,7 +65,7 @@ func (s startCmd) startStandAlone() error { // Generate the app in the proper dir addr := viper.GetString(flagAddress) home := viper.GetString("home") - app, err := s.app(home, s.logger) + app, err := s.appCreator(home, s.logger) if err != nil { return err } @@ -92,7 +92,7 @@ func (s startCmd) startInProcess() error { } home := cfg.RootDir - app, err := s.app(home, s.logger) + app, err := s.appCreator(home, s.logger) if err != nil { return err } diff --git a/server/start_test.go b/server/start_test.go index 5b7ab3e76c40..2657c5223483 100644 --- a/server/start_test.go +++ b/server/start_test.go @@ -1,12 +1,10 @@ package server import ( - "fmt" - "os" + // "os" "testing" "time" - "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -15,7 +13,7 @@ import ( ) func TestStartStandAlone(t *testing.T) { - defer setupViper()() + defer setupViper(t)() logger := log.NewNopLogger() initCmd := InitCmd(mock.GenInitOptions, logger) @@ -26,14 +24,15 @@ func TestStartStandAlone(t *testing.T) { viper.Set(flagWithTendermint, false) viper.Set(flagAddress, "localhost:11122") startCmd := StartCmd(mock.NewApp, logger) + startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address timeout := time.Duration(3) * time.Second - err = runOrTimeout(startCmd, timeout) - require.NoError(t, err) + RunOrTimeout(startCmd, timeout, t) } +/* func TestStartWithTendermint(t *testing.T) { - defer setupViper()() + defer setupViper(t)() logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). With("module", "mock-cmd") @@ -45,28 +44,12 @@ func TestStartWithTendermint(t *testing.T) { // set up app and start up viper.Set(flagWithTendermint, true) startCmd := StartCmd(mock.NewApp, logger) + startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address timeout := time.Duration(3) * time.Second - err = runOrTimeout(startCmd, timeout) - require.NoError(t, err) -} - -func runOrTimeout(cmd *cobra.Command, timeout time.Duration) error { - done := make(chan error) - go func(out chan<- error) { - // this should NOT exit - err := cmd.RunE(nil, nil) - if err != nil { - out <- err - } - out <- fmt.Errorf("start died for unknown reasons") - }(done) - timer := time.NewTimer(timeout) + //a, _ := startCmd.Flags().GetString(flagAddress) + //panic(a) - select { - case err := <-done: - return err - case <-timer.C: - return nil - } + RunOrTimeout(startCmd, timeout, t) } +*/ diff --git a/server/test_helpers.go b/server/test_helpers.go new file mode 100644 index 000000000000..103af7c33194 --- /dev/null +++ b/server/test_helpers.go @@ -0,0 +1,82 @@ +package server + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/mock" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/tendermint/tmlibs/cli" + "github.com/tendermint/tmlibs/log" +) + +// Get a free address for a test tendermint server +// protocol is either tcp, http, etc +func FreeTCPAddr(t *testing.T) string { + l, err := net.Listen("tcp", "0.0.0.0:0") + defer l.Close() + require.Nil(t, err) + + port := l.Addr().(*net.TCPAddr).Port + addr := fmt.Sprintf("tcp://0.0.0.0:%d", port) + return addr +} + +// setupViper creates a homedir to run inside, +// and returns a cleanup function to defer +func setupViper(t *testing.T) func() { + rootDir, err := ioutil.TempDir("", "mock-sdk-cmd") + require.Nil(t, err) + viper.Set(cli.HomeFlag, rootDir) + return func() { + os.RemoveAll(rootDir) + } +} + +// Begin the server pass up the channel to close +// NOTE pass up the channel so it can be closed at the end of the process +func StartServer(t *testing.T) chan error { + defer setupViper(t)() + + // init server + initCmd := InitCmd(mock.GenInitOptions, log.NewNopLogger()) + err := initCmd.RunE(nil, nil) + require.NoError(t, err) + + // start server + viper.Set(flagWithTendermint, true) + startCmd := StartCmd(mock.NewApp, log.NewNopLogger()) + startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address + startCmd.Flags().Set("rpc.laddr", FreeTCPAddr(t)) // set to a new free address + timeout := time.Duration(3) * time.Second + + return RunOrTimeout(startCmd, timeout, t) +} + +// Run or Timout RunE of command passed in +func RunOrTimeout(cmd *cobra.Command, timeout time.Duration, t *testing.T) chan error { + done := make(chan error) + go func(out chan<- error) { + // this should NOT exit + err := cmd.RunE(nil, nil) + if err != nil { + out <- err + } + out <- fmt.Errorf("start died for unknown reasons") + }(done) + timer := time.NewTimer(timeout) + + select { + case err := <-done: + require.NoError(t, err) + case <-timer.C: + return done + } + return done +} diff --git a/store/iavlstore.go b/store/iavlstore.go index e63585a16bbc..96110c59c967 100644 --- a/store/iavlstore.go +++ b/store/iavlstore.go @@ -19,7 +19,7 @@ const ( func LoadIAVLStore(db dbm.DB, id CommitID) (CommitStore, error) { tree := iavl.NewVersionedTree(db, defaultIAVLCacheSize) - err := tree.LoadVersion(id.Version) + _, err := tree.LoadVersion(id.Version) if err != nil { return nil, err } @@ -119,6 +119,13 @@ func (st *iavlStore) Iterator(start, end []byte) Iterator { return newIAVLIterator(st.tree.Tree(), start, end, true) } +func (st *iavlStore) Subspace(prefix []byte) Iterator { + end := make([]byte, len(prefix)) + copy(end, prefix) + end[len(end)-1]++ + return st.Iterator(prefix, end) +} + // Implements IterKVStore. func (st *iavlStore) ReverseIterator(start, end []byte) Iterator { return newIAVLIterator(st.tree.Tree(), start, end, false) @@ -134,7 +141,7 @@ func (st *iavlStore) ReverseIterator(start, end []byte) Iterator { func (st *iavlStore) Query(req abci.RequestQuery) (res abci.ResponseQuery) { if len(req.Data) == 0 { msg := "Query cannot be zero length" - return sdk.ErrTxParse(msg).Result().ToQuery() + return sdk.ErrTxDecode(msg).Result().ToQuery() } tree := st.tree diff --git a/store/iavlstore_test.go b/store/iavlstore_test.go index c426e4d8a8a5..7adae625eaf3 100644 --- a/store/iavlstore_test.go +++ b/store/iavlstore_test.go @@ -82,6 +82,26 @@ func TestIAVLIterator(t *testing.T) { } } +func TestIAVLSubspace(t *testing.T) { + db := dbm.NewMemDB() + tree, _ := newTree(t, db) + iavlStore := newIAVLStore(tree, numHistory) + + iavlStore.Set([]byte("test1"), []byte("test1")) + iavlStore.Set([]byte("test2"), []byte("test2")) + iavlStore.Set([]byte("test3"), []byte("test3")) + + iter := iavlStore.Subspace([]byte("test")) + expected := []string{"test1", "test2", "test3"} + for i := 0; iter.Valid(); iter.Next() { + expectedKey := expected[i] + key, value := iter.Key(), iter.Value() + assert.EqualValues(t, key, expectedKey) + assert.EqualValues(t, value, expectedKey) + i += 1 + } +} + func TestIAVLStoreQuery(t *testing.T) { db := dbm.NewMemDB() tree := iavl.NewVersionedTree(db, cacheSize) diff --git a/store/wire.go b/store/wire.go index 22536ff09e0a..7befbdfcd913 100644 --- a/store/wire.go +++ b/store/wire.go @@ -1,7 +1,7 @@ package store import ( - "github.com/tendermint/go-wire" + "github.com/cosmos/cosmos-sdk/wire" ) var cdc = wire.NewCodec() diff --git a/tests/tests.go b/tests/tests.go new file mode 100644 index 000000000000..845ac69259bc --- /dev/null +++ b/tests/tests.go @@ -0,0 +1,320 @@ +package tests + +import ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + //"strings" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/server" + "github.com/stretchr/testify/require" +) + +// Tests assume the `basecoind` and `basecli` binaries +// have been built and are located in `./build` + +// TODO remove test dirs if tests are successful + +//nolint +var ( + basecoind = "build/basecoind" + basecli = "build/basecli" + + basecoindDir = "./tmp-basecoind-tests" + basecliDir = "./tmp-basecli-tests" + + ACCOUNTS = []string{"alice", "bob", "charlie", "igor"} + alice = ACCOUNTS[0] + bob = ACCOUNTS[1] + charlie = ACCOUNTS[2] + igor = ACCOUNTS[3] +) + +func gopath() string { + return filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "cosmos", "cosmos-sdk") +} + +func whereIsBasecoind() string { + return filepath.Join(gopath(), basecoind) +} + +func whereIsBasecli() string { + return filepath.Join(gopath(), basecli) +} + +// Init Basecoin Test +func TestInitBasecoin(t *testing.T, home string) string { + var err error + + password := "some-random-password" + + initBasecoind := exec.Command(whereIsBasecoind(), "init", "--home", home) + cmdWriter, err := initBasecoind.StdinPipe() + require.Nil(t, err) + + buf := new(bytes.Buffer) + initBasecoind.Stdout = buf + + if err = initBasecoind.Start(); err != nil { + t.Error(err) + } + + _, err = cmdWriter.Write([]byte(password)) + require.Nil(t, err) + cmdWriter.Close() + + if err = initBasecoind.Wait(); err != nil { + t.Error(err) + } + + // get seed from initialization + theOutput := strings.Split(buf.String(), "\n") + var seedLine int + for _seedLine, o := range theOutput { + if strings.HasPrefix(string(o), "Secret phrase") { + seedLine = _seedLine + 1 + break + } + } + + seed := string(theOutput[seedLine]) + + // enable indexing + err = appendToFile(path.Join(home, "config", "config.toml"), "\n\n[tx_indexing]\nindex_all_tags = true\n") + require.Nil(t, err) + + return seed +} + +func appendToFile(path string, text string) error { + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return err + } + + defer f.Close() + + if _, err = f.WriteString(text); err != nil { + return err + } + + return nil +} + +func makeKeys() error { + for _, acc := range ACCOUNTS { + makeKeys := exec.Command(whereIsBasecli(), "keys", "add", acc, "--home", basecliDir) + cmdWriter, err := makeKeys.StdinPipe() + if err != nil { + return err + } + + makeKeys.Stdout = os.Stdout + if err := makeKeys.Start(); err != nil { + return err + } + cmdWriter.Write([]byte("1234567890")) + if err != nil { + return err + } + cmdWriter.Close() + + if err := makeKeys.Wait(); err != nil { + return err + } + } + + return nil +} + +func _TestSendCoins(t *testing.T) { + if err := StartServer(); err != nil { + t.Error(err) + } + + // send some coins + // [zr] where dafuq do I get a FROM (oh, use --name) + + sendTo := fmt.Sprintf("--to=%s", bob) + sendFrom := fmt.Sprintf("--from=%s", alice) + + cmdOut, err := exec.Command(whereIsBasecli(), "send", sendTo, "--amount=1000mycoin", sendFrom, "--seq=0").Output() + if err != nil { + t.Error(err) + } + + fmt.Printf("sent: %s", string(cmdOut)) + +} + +// expects TestInitBaseCoin to have been run +func StartServer() error { + // straight outta https://nathanleclaire.com/blog/2014/12/29/shelled-out-commands-in-golang/ + cmdName := whereIsBasecoind() + cmdArgs := []string{"start", "--home", basecoindDir} + + cmd := exec.Command(cmdName, cmdArgs...) + cmdReader, err := cmd.StdoutPipe() + if err != nil { + return err + } + + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + fmt.Printf("running [basecoind start] %s\n", scanner.Text()) + } + }() + + err = cmd.Start() + if err != nil { + return err + } + + err = cmd.Wait() + if err != nil { + return err + } + + time.Sleep(5 * time.Second) + + return nil + + // TODO return cmd.Process so that we can later do something like: + // cmd.Process.Kill() + // see: https://stackoverflow.com/questions/11886531/terminating-a-process-started-with-os-exec-in-golang +} + +// Init Basecoin Test +func InitServerForTest(t *testing.T) { + Clean() + + var err error + + password := "some-random-password" + usePassword := exec.Command("echo", password) + + initBasecoind := exec.Command(whereIsBasecoind(), "init", "--home", basecoindDir) + + initBasecoind.Stdin, err = usePassword.StdoutPipe() + require.Nil(t, err) + + initBasecoind.Stdout = os.Stdout + + err = initBasecoind.Start() + require.Nil(t, err) + err = usePassword.Run() + require.Nil(t, err) + err = initBasecoind.Wait() + require.Nil(t, err) + + err = makeKeys() + require.Nil(t, err) +} + +// expects TestInitBaseCoin to have been run +func StartNodeServerForTest(t *testing.T, home string) *exec.Cmd { + cmdName := whereIsBasecoind() + cmdArgs := []string{"start", "--home", home} + cmd := exec.Command(cmdName, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + require.Nil(t, err) + + // FIXME: if there is a nondeterministic node start failure, + // we should probably make this read the logs to wait for RPC + time.Sleep(time.Second * 2) + + return cmd +} + +// expects TestInitBaseCoin to have been run +func StartLCDServerForTest(t *testing.T, home, chainID string) (cmd *exec.Cmd, port string) { + cmdName := whereIsBasecli() + port = strings.Split(server.FreeTCPAddr(t), ":")[2] + cmdArgs := []string{ + "rest-server", + "--home", + home, + "--bind", + fmt.Sprintf("localhost:%s", port), + "--chain-id", + chainID, + } + cmd = exec.Command(cmdName, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + require.Nil(t, err) + time.Sleep(time.Second * 2) // TODO: LOL + return cmd, port +} + +// clean the directories +func Clean() { + // ignore errors b/c the dirs may not yet exist + err := os.Remove(basecoindDir) + panic(err) + err = os.Remove(basecliDir) + panic(err) +} + +/* + + chainID = "staking_test" + testDir = "./tmp_tests" +) + +func runTests() { + + if err := os.Mkdir(testDir, 0666); err != nil { + panic(err) + } + defer os.Remove(testDir) + + // make some keys + + //if err := makeKeys(); err != nil { + // panic(err) + //} + + if err := initServer(); err != nil { + fmt.Printf("Err: %v", err) + panic(err) + } + +} + +func initServer() error { + serveDir := filepath.Join(testDir, "server") + //serverLog := filepath.Join(testDir, "gaia-node.log") + + // get RICH + keyOut, err := exec.Command(GAIA, CLIENT_EXE, "keys", "get", "alice").Output() + if err != nil { + fmt.Println("one") + return err + } + key := strings.Split(string(keyOut), "\t") + fmt.Printf("wit:%s", key[2]) + + outByte, err := exec.Command(GAIA, SERVER_EXE, "init", "--static", fmt.Sprintf("--chain-id=%s", chainID), fmt.Sprintf("--home=%s", serveDir), key[2]).Output() + if err != nil { + fmt.Println("teo") + fmt.Printf("Error: %v", err) + + return err + } + fmt.Sprintf("OUT: %s", string(outByte)) + return nil +} + +*/ diff --git a/tools/Gopkg.lock b/tools/Gopkg.lock new file mode 100644 index 000000000000..92fe1e260caf --- /dev/null +++ b/tools/Gopkg.lock @@ -0,0 +1,56 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/alecthomas/gometalinter" + packages = ["."] + revision = "46cc1ea3778b247666c2949669a3333c532fa9c6" + version = "v2.0.5" + +[[projects]] + branch = "master" + name = "github.com/alecthomas/units" + packages = ["."] + revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" + +[[projects]] + branch = "master" + name = "github.com/google/shlex" + packages = ["."] + revision = "6f45313302b9c56850fc17f99e40caebce98c716" + +[[projects]] + name = "github.com/nicksnyder/go-i18n" + packages = [ + "i18n", + "i18n/bundle", + "i18n/language", + "i18n/translation" + ] + revision = "0dc1626d56435e9d605a29875701721c54bc9bbd" + version = "v1.10.0" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" + +[[projects]] + branch = "v3-unstable" + name = "gopkg.in/alecthomas/kingpin.v3-unstable" + packages = ["."] + revision = "b8d601de6db1f3b56a99ffe9051eb708574bc1cd" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" + version = "v2.1.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "bb8cda576a5c4dda202435f43a46ae50a254181a4bf22c6af6f4d3d03079d509" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/tools/Gopkg.toml b/tools/Gopkg.toml new file mode 100644 index 000000000000..97fb629752ce --- /dev/null +++ b/tools/Gopkg.toml @@ -0,0 +1,34 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + +[[constraint]] + name = "github.com/alecthomas/gometalinter" + version = "2.0.5" + +[prune] + go-tests = true + unused-packages = true + diff --git a/tools/Makefile b/tools/Makefile index 6786575cdc88..ee1c14a8437b 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,40 +1,40 @@ -all: install_glide check get_vendor_deps install +all: install ######################################## -### Glide +### DEP -GLIDE = github.com/tendermint/glide -GLIDE_CHECK := $(shell command -v glide 2> /dev/null) +DEP = github.com/golang/dep/cmd/dep +DEP_CHECK := $(shell command -v dep 2> /dev/null) -check: -ifndef GLIDE_CHECK - @echo "No glide in path. Install with 'make install_glide'." +check_tools: +ifndef DEP_CHECK + @echo "No dep in path. Install with 'make get_tools'." else - @echo "Found glide in path." + @echo "Found dep in path." endif -install_glide: -ifdef GLIDE_CHECK - @echo "Glide is already installed. Run 'make update_glide' to update." +get_tools: +ifdef DEP_CHECK + @echo "Dep is already installed. Run 'make update_tools' to update." else - @echo "$(ansi_grn)Installing glide$(ansi_end)" - go get -v $(GLIDE) + @echo "$(ansi_grn)Installing dep$(ansi_end)" + go get -v $(DEP) endif -update_glide: - @echo "$(ansi_grn)Updating glide$(ansi_end)" - go get -u -v $(GLIDE) +update_tools: + @echo "$(ansi_grn)Updating dep$(ansi_end)" + go get -u -v $(DEP) ######################################## ### Install tools -get_vendor_deps: check +get_vendor_deps: check_tools @rm -rf vendor/ - @echo "--> Running glide install" - @glide install + @echo "--> Running dep ensure" + @dep ensure -v install: get_vendor_deps @echo "$(ansi_grn)Installing tools$(ansi_end)" @@ -42,10 +42,7 @@ install: get_vendor_deps go build -o bin/go-vendorinstall go-vendorinstall/*.go @echo "$(ansi_yel)Install gometalinter.v2$(ansi_end)" - GOBIN=$(CURDIR)/bin ./bin/go-vendorinstall github.com/alecthomas/gometalinter - - @echo "$(ansi_yel)Install shelldown$(ansi_end)" - GOBIN=$(CURDIR)/bin ./bin/go-vendorinstall github.com/rigelrozanski/shelldown/cmd/shelldown + GOBIN="$(CURDIR)/bin" ./bin/go-vendorinstall github.com/alecthomas/gometalinter @echo "$(ansi_grn)Done installing tools$(ansi_end)" @@ -62,4 +59,4 @@ ansi_end=\033[0m # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check install_glide update_glide get_vendor_deps install +.PHONY: check_tools install_tools update_tools get_vendor_deps install diff --git a/tools/glide.lock b/tools/glide.lock deleted file mode 100644 index f4af28eeccab..000000000000 --- a/tools/glide.lock +++ /dev/null @@ -1,18 +0,0 @@ -hash: 934ad5be72c9c240e8555eb6e1b2319840266c04c0fa9e024008cf841c0cee65 -updated: 2018-02-23T19:33:08.596187+01:00 -imports: -- name: github.com/alecthomas/gometalinter - version: 46cc1ea3778b247666c2949669a3333c532fa9c6 -- name: github.com/rigelrozanski/common - version: f691f115798593d783b9999b1263c2f4ffecc439 -- name: github.com/rigelrozanski/shelldown - version: 2e18b6eb9bf428aa524e71433296e0b7c73ae0a3 - subpackages: - - cmd/shelldown -- name: github.com/spf13/cobra - version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b -- name: github.com/spf13/pflag - version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 -- name: github.com/spf13/viper - version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 -testImports: [] diff --git a/tools/glide.yaml b/tools/glide.yaml deleted file mode 100644 index 6ddd6443daef..000000000000 --- a/tools/glide.yaml +++ /dev/null @@ -1,13 +0,0 @@ -package: github.com/cosmos/cosmos-sdk/tools -import: -- package: github.com/alecthomas/gometalinter - version: ^2.0.5 -- package: github.com/rigelrozanski/shelldown - subpackages: - - cmd/shelldown -- package: github.com/spf13/pflag - version: v1.0.0 -- package: github.com/spf13/cobra - version: v0.0.1 -- package: github.com/spf13/viper - version: ^1.0.0 diff --git a/tools/main.go b/tools/main.go index 7fd61d589d6c..104268010a27 100644 --- a/tools/main.go +++ b/tools/main.go @@ -1,12 +1,11 @@ package main -import ( - // Include dependencies here so glide picks them up - // and installs sub-dependencies. +// Include dependencies here so dep picks them up +// and installs sub-dependencies. - // TODO: Ideally this gets auto-imported on glide update. - // Any way to make that happen? - _ "github.com/rigelrozanski/common" -) +// TODO: Ideally this gets auto-imported on dep update. +// Any way to make that happen? +// NOTE: problems with this import because its a main not a lib +// _ "github.com/alecthomas/gometalinter" func main() {} diff --git a/types/account.go b/types/account.go index 3a6e5931f000..c0fadcd3cf1c 100644 --- a/types/account.go +++ b/types/account.go @@ -2,13 +2,17 @@ package types import ( crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" ) +// Address in go-crypto style +type Address = cmn.HexBytes + // Account is a standard account using a sequence number for replay protection // and a pubkey for authentication. type Account interface { - GetAddress() crypto.Address - SetAddress(crypto.Address) error // errors if already set. + GetAddress() Address + SetAddress(Address) error // errors if already set. GetPubKey() crypto.PubKey // can return nil. SetPubKey(crypto.PubKey) error @@ -26,10 +30,10 @@ type Account interface { // AccountMapper stores and retrieves accounts from stores // retrieved from the context. type AccountMapper interface { - NewAccountWithAddress(ctx Context, addr crypto.Address) Account - GetAccount(ctx Context, addr crypto.Address) Account + NewAccountWithAddress(ctx Context, addr Address) Account + GetAccount(ctx Context, addr Address) Account SetAccount(ctx Context, acc Account) } -// Application function variable used to unmarshal account -type ParseAccount func([]byte) (Account, error) +// AccountDecoder unmarshals account bytes +type AccountDecoder func(accountBytes []byte) (Account, error) diff --git a/types/coin.go b/types/coin.go index ad541b99bd9d..92871cd17963 100644 --- a/types/coin.go +++ b/types/coin.go @@ -139,8 +139,14 @@ func (coins Coins) IsGTE(coinsB Coins) bool { } // IsZero returns true if there are no coins +// or all coins are zero. func (coins Coins) IsZero() bool { - return len(coins) == 0 + for _, coin := range coins { + if !coin.IsZero() { + return false + } + } + return true } // IsEqual returns true if the two sets of Coins have the same value diff --git a/types/errors.go b/types/errors.go index 77cea98ac742..fab28c4449d2 100644 --- a/types/errors.go +++ b/types/errors.go @@ -3,8 +3,6 @@ package types import ( "fmt" "runtime" - - "github.com/tendermint/go-crypto" ) // ABCI Response Code @@ -19,17 +17,20 @@ func (code CodeType) IsOK() bool { } // ABCI Response Codes -// Base SDK reserves 0 ~ 99. +// Base SDK reserves 0 - 99. const ( - CodeOK CodeType = 0 - CodeInternal CodeType = 1 - CodeTxParse CodeType = 2 - CodeBadNonce CodeType = 3 - CodeUnauthorized CodeType = 4 - CodeInsufficientFunds CodeType = 5 - CodeUnknownRequest CodeType = 6 - CodeUnrecognizedAddress CodeType = 7 - CodeInvalidSequence CodeType = 8 + CodeOK CodeType = 0 + CodeInternal CodeType = 1 + CodeTxDecode CodeType = 2 + CodeInvalidSequence CodeType = 3 + CodeUnauthorized CodeType = 4 + CodeInsufficientFunds CodeType = 5 + CodeUnknownRequest CodeType = 6 + CodeInvalidAddress CodeType = 7 + CodeInvalidPubKey CodeType = 8 + CodeUnknownAddress CodeType = 9 + CodeInsufficientCoins CodeType = 10 + CodeInvalidCoins CodeType = 11 CodeGenesisParse CodeType = 0xdead // TODO: remove ? ) @@ -39,22 +40,28 @@ func CodeToDefaultMsg(code CodeType) string { switch code { case CodeInternal: return "Internal error" - case CodeTxParse: + case CodeTxDecode: return "Tx parse error" case CodeGenesisParse: return "Genesis parse error" - case CodeBadNonce: - return "Bad nonce" + case CodeInvalidSequence: + return "Invalid sequence" case CodeUnauthorized: return "Unauthorized" case CodeInsufficientFunds: return "Insufficent funds" case CodeUnknownRequest: return "Unknown request" - case CodeUnrecognizedAddress: - return "Unrecognized address" - case CodeInvalidSequence: - return "Invalid sequence" + case CodeInvalidAddress: + return "Invalid address" + case CodeInvalidPubKey: + return "Invalid pubkey" + case CodeUnknownAddress: + return "Unknown address" + case CodeInsufficientCoins: + return "Insufficient coins" + case CodeInvalidCoins: + return "Invalid coins" default: return fmt.Sprintf("Unknown code %d", code) } @@ -68,14 +75,14 @@ func CodeToDefaultMsg(code CodeType) string { func ErrInternal(msg string) Error { return newError(CodeInternal, msg) } -func ErrTxParse(msg string) Error { - return newError(CodeTxParse, msg) +func ErrTxDecode(msg string) Error { + return newError(CodeTxDecode, msg) } func ErrGenesisParse(msg string) Error { return newError(CodeGenesisParse, msg) } -func ErrBadNonce(msg string) Error { - return newError(CodeBadNonce, msg) +func ErrInvalidSequence(msg string) Error { + return newError(CodeInvalidSequence, msg) } func ErrUnauthorized(msg string) Error { return newError(CodeUnauthorized, msg) @@ -86,11 +93,20 @@ func ErrInsufficientFunds(msg string) Error { func ErrUnknownRequest(msg string) Error { return newError(CodeUnknownRequest, msg) } -func ErrUnrecognizedAddress(addr crypto.Address) Error { - return newError(CodeUnrecognizedAddress, addr.String()) +func ErrInvalidAddress(msg string) Error { + return newError(CodeInvalidAddress, msg) } -func ErrInvalidSequence(msg string) Error { - return newError(CodeInvalidSequence, msg) +func ErrUnknownAddress(msg string) Error { + return newError(CodeUnknownAddress, msg) +} +func ErrInvalidPubKey(msg string) Error { + return newError(CodeInvalidPubKey, msg) +} +func ErrInsufficientCoins(msg string) Error { + return newError(CodeInsufficientCoins, msg) +} +func ErrInvalidCoins(msg string) Error { + return newError(CodeInvalidCoins, msg) } //---------------------------------------- diff --git a/types/errors_test.go b/types/errors_test.go new file mode 100644 index 000000000000..939cced7ccc7 --- /dev/null +++ b/types/errors_test.go @@ -0,0 +1,53 @@ +package types + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var codeTypes = []CodeType{ + CodeInternal, + CodeTxDecode, + CodeInvalidSequence, + CodeUnauthorized, + CodeInsufficientFunds, + CodeUnknownRequest, + CodeUnknownAddress, + CodeInvalidPubKey, + CodeGenesisParse, +} + +type errFn func(msg string) Error + +var errFns = []errFn{ + ErrInternal, + ErrTxDecode, + ErrInvalidSequence, + ErrUnauthorized, + ErrInsufficientFunds, + ErrUnknownRequest, + ErrUnknownAddress, + ErrInvalidPubKey, + ErrGenesisParse, +} + +func TestCodeType(t *testing.T) { + assert.True(t, CodeOK.IsOK()) + + for _, c := range codeTypes { + assert.False(t, c.IsOK()) + msg := CodeToDefaultMsg(c) + assert.False(t, strings.HasPrefix(msg, "Unknown code")) + } +} + +func TestErrFn(t *testing.T) { + for i, errFn := range errFns { + err := errFn("") + codeType := codeTypes[i] + assert.Equal(t, err.ABCICode(), codeType) + assert.Equal(t, err.Result().Code, codeType) + } +} diff --git a/types/stdlib/stdlib.go b/types/stdlib/stdlib.go new file mode 100644 index 000000000000..dd9f4efad182 --- /dev/null +++ b/types/stdlib/stdlib.go @@ -0,0 +1,232 @@ +package types + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" +) + +type ListMapper interface { // Solidity list like structure + Len(sdk.Context) int64 + Get(sdk.Context, int64, interface{}) + Set(sdk.Context, int64, interface{}) + Push(sdk.Context, interface{}) + Iterate(sdk.Context, interface{}, func(sdk.Context, int64)) +} + +type listMapper struct { + key sdk.StoreKey + cdc *wire.Codec + lk []byte +} + +func NewListMapper(cdc *wire.Codec, key sdk.StoreKey) ListMapper { + lk, err := cdc.MarshalBinary(int64(-1)) + if err != nil { + panic(err) + } + return listMapper{ + key: key, + cdc: cdc, + lk: lk, + } +} + +func (lm listMapper) Len(ctx sdk.Context) int64 { + store := ctx.KVStore(lm.key) + bz := store.Get(lm.lk) + if bz == nil { + zero, err := lm.cdc.MarshalBinary(0) + if err != nil { + panic(err) + } + store.Set(lm.lk, zero) + return 0 + } + var res int64 + if err := lm.cdc.UnmarshalBinary(bz, &res); err != nil { + panic(err) + } + return res +} + +func (lm listMapper) Get(ctx sdk.Context, index int64, ptr interface{}) { + if index < 0 { + panic(errors.New("")) + } + store := ctx.KVStore(lm.key) + bz := store.Get(marshalInt64(lm.cdc, index)) + if err := lm.cdc.UnmarshalBinary(bz, ptr); err != nil { + panic(err) + } +} + +func (lm listMapper) Set(ctx sdk.Context, index int64, value interface{}) { + if index < 0 { + panic(errors.New("")) + } + store := ctx.KVStore(lm.key) + bz, err := lm.cdc.MarshalBinary(value) + if err != nil { + panic(err) + } + store.Set(marshalInt64(lm.cdc, index), bz) +} + +func (lm listMapper) Push(ctx sdk.Context, value interface{}) { + length := lm.Len(ctx) + lm.Set(ctx, length, value) + + store := ctx.KVStore(lm.key) + store.Set(lm.lk, marshalInt64(lm.cdc, length+1)) +} + +func (lm listMapper) Iterate(ctx sdk.Context, ptr interface{}, fn func(sdk.Context, int64)) { + length := lm.Len(ctx) + for i := int64(0); i < length; i++ { + lm.Get(ctx, i, ptr) + fn(ctx, i) + } +} + +type QueueMapper interface { + Push(sdk.Context, interface{}) + Peek(sdk.Context, interface{}) + Pop(sdk.Context) + IsEmpty(sdk.Context) bool + Iterate(sdk.Context, interface{}, func(sdk.Context) bool) +} + +type queueMapper struct { + key sdk.StoreKey + cdc *wire.Codec + ik []byte +} + +func NewQueueMapper(cdc *wire.Codec, key sdk.StoreKey) QueueMapper { + ik, err := cdc.MarshalBinary(int64(-1)) + if err != nil { + panic(err) + } + return queueMapper{ + key: key, + cdc: cdc, + ik: ik, + } +} + +type queueInfo struct { + // begin <= elems < end + Begin int64 + End int64 +} + +func (info queueInfo) validateBasic() error { + if info.End < info.Begin || info.Begin < 0 || info.End < 0 { + return errors.New("") + } + return nil +} + +func (info queueInfo) isEmpty() bool { + return info.Begin == info.End +} + +func (qm queueMapper) getQueueInfo(store sdk.KVStore) queueInfo { + bz := store.Get(qm.ik) + if bz == nil { + store.Set(qm.ik, marshalQueueInfo(qm.cdc, queueInfo{0, 0})) + return queueInfo{0, 0} + } + var info queueInfo + if err := qm.cdc.UnmarshalBinary(bz, &info); err != nil { + panic(err) + } + if err := info.validateBasic(); err != nil { + panic(err) + } + return info +} + +func (qm queueMapper) setQueueInfo(store sdk.KVStore, info queueInfo) { + bz, err := qm.cdc.MarshalBinary(info) + if err != nil { + panic(err) + } + store.Set(qm.ik, bz) +} + +func (qm queueMapper) Push(ctx sdk.Context, value interface{}) { + store := ctx.KVStore(qm.key) + info := qm.getQueueInfo(store) + + bz, err := qm.cdc.MarshalBinary(value) + if err != nil { + panic(err) + } + store.Set(marshalInt64(qm.cdc, info.End), bz) + + info.End++ + qm.setQueueInfo(store, info) +} + +func (qm queueMapper) Peek(ctx sdk.Context, ptr interface{}) { + store := ctx.KVStore(qm.key) + info := qm.getQueueInfo(store) + bz := store.Get(marshalInt64(qm.cdc, info.Begin)) + if err := qm.cdc.UnmarshalBinary(bz, ptr); err != nil { + panic(err) + } +} + +func (qm queueMapper) Pop(ctx sdk.Context) { + store := ctx.KVStore(qm.key) + info := qm.getQueueInfo(store) + store.Delete(marshalInt64(qm.cdc, info.Begin)) + info.Begin++ + qm.setQueueInfo(store, info) +} + +func (qm queueMapper) IsEmpty(ctx sdk.Context) bool { + store := ctx.KVStore(qm.key) + info := qm.getQueueInfo(store) + return info.isEmpty() +} + +func (qm queueMapper) Iterate(ctx sdk.Context, ptr interface{}, fn func(sdk.Context) bool) { + store := ctx.KVStore(qm.key) + info := qm.getQueueInfo(store) + + var i int64 + for i = info.Begin; i < info.End; i++ { + key := marshalInt64(qm.cdc, i) + bz := store.Get(key) + if err := qm.cdc.UnmarshalBinary(bz, ptr); err != nil { + panic(err) + } + store.Delete(key) + if fn(ctx) { + break + } + } + + info.Begin = i + qm.setQueueInfo(store, info) +} + +func marshalQueueInfo(cdc *wire.Codec, info queueInfo) []byte { + bz, err := cdc.MarshalBinary(info) + if err != nil { + panic(err) + } + return bz +} + +func marshalInt64(cdc *wire.Codec, i int64) []byte { + bz, err := cdc.MarshalBinary(i) + if err != nil { + panic(err) + } + return bz +} diff --git a/types/stdlib/stdlib_test.go b/types/stdlib/stdlib_test.go new file mode 100644 index 000000000000..7c871ccf3c0f --- /dev/null +++ b/types/stdlib/stdlib_test.go @@ -0,0 +1,69 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbm "github.com/tendermint/tmlibs/db" + + abci "github.com/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" +) + +type S struct { + I int64 + B bool +} + +func defaultComponents(key sdk.StoreKey) (sdk.Context, *wire.Codec) { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db) + cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + cms.LoadLatestVersion() + ctx := sdk.NewContext(cms, abci.Header{}, false, nil) + cdc := wire.NewCodec() + return ctx, cdc +} + +func TestListMapper(t *testing.T) { + key := sdk.NewKVStoreKey("list") + ctx, cdc := defaultComponents(key) + lm := NewListMapper(cdc, key) + + val := S{1, true} + var res S + + lm.Push(ctx, val) + assert.Equal(t, int64(1), lm.Len(ctx)) + lm.Get(ctx, int64(0), &res) + assert.Equal(t, val, res) + + val = S{2, false} + lm.Set(ctx, int64(0), val) + lm.Get(ctx, int64(0), &res) + assert.Equal(t, val, res) +} + +func TestQueueMapper(t *testing.T) { + key := sdk.NewKVStoreKey("queue") + ctx, cdc := defaultComponents(key) + qm := NewQueueMapper(cdc, key) + + val := S{1, true} + var res S + + qm.Push(ctx, val) + qm.Peek(ctx, &res) + assert.Equal(t, val, res) + + qm.Pop(ctx) + empty := qm.IsEmpty(ctx) + + assert.Equal(t, true, empty) + + assert.Panics(t, func() { qm.Peek(ctx, &res) }) +} diff --git a/types/tx_msg.go b/types/tx_msg.go index 1a7f63f938a0..79272f386241 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -1,7 +1,7 @@ package types import ( - crypto "github.com/tendermint/go-crypto" + "encoding/json" ) // Transactions messages must fulfill the Msg @@ -24,19 +24,17 @@ type Msg interface { // Signers returns the addrs of signers that must sign. // CONTRACT: All signatures must be present to be valid. // CONTRACT: Returns addrs in some deterministic order. - GetSigners() []crypto.Address + GetSigners() []Address } +//__________________________________________________________ + // Transactions objects must fulfill the Tx type Tx interface { // Gets the Msg. GetMsg() Msg - // The address that pays the base fee for this message. The fee is - // deducted before the Msg is processed. - GetFeePayer() crypto.Address - // Signatures returns the signature of signers who signed the Msg. // CONTRACT: Length returned is same as length of // pubkeys returned from MsgKeySigners, and the order @@ -49,26 +47,140 @@ type Tx interface { var _ Tx = (*StdTx)(nil) -// StdTx is a standard way to wrap a Msg with Signatures. +// StdTx is a standard way to wrap a Msg with Fee and Signatures. // NOTE: the first signature is the FeePayer (Signatures must not be nil). type StdTx struct { Msg `json:"msg"` + Fee StdFee `json:"fee"` Signatures []StdSignature `json:"signatures"` } -func NewStdTx(msg Msg, sigs []StdSignature) StdTx { +func NewStdTx(msg Msg, fee StdFee, sigs []StdSignature) StdTx { return StdTx{ Msg: msg, + Fee: fee, Signatures: sigs, } } //nolint func (tx StdTx) GetMsg() Msg { return tx.Msg } -func (tx StdTx) GetFeePayer() crypto.Address { return tx.Signatures[0].PubKey.Address() } // XXX but PubKey is optional! func (tx StdTx) GetSignatures() []StdSignature { return tx.Signatures } -//------------------------------------- +// FeePayer returns the address responsible for paying the fees +// for the transactions. It's the first address returned by msg.GetSigners(). +// If GetSigners() is empty, this panics. +func FeePayer(tx Tx) Address { + return tx.GetMsg().GetSigners()[0] +} + +//__________________________________________________________ + +// StdFee includes the amount of coins paid in fees and the maximum +// 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"` +} + +func NewStdFee(gas int64, amount ...Coin) StdFee { + return StdFee{ + Amount: amount, + Gas: gas, + } +} + +func (fee StdFee) Bytes() []byte { + // normalize. XXX + // this is a sign of something ugly + // (in the lcd_test, client side its null, + // server side its []) + if len(fee.Amount) == 0 { + fee.Amount = Coins{} + } + bz, err := json.Marshal(fee) // TODO + if err != nil { + panic(err) + } + return bz +} + +//__________________________________________________________ + +// StdSignDoc is replay-prevention structure. +// It includes the result of msg.GetSignBytes(), +// as well as the ChainID (prevent cross chain replay) +// and the Sequence numbers for each signature (prevent +// inchain replay and enforce tx ordering per account). +type StdSignDoc struct { + ChainID string `json:"chain_id"` + Sequences []int64 `json:"sequences"` + FeeBytes []byte `json:"fee_bytes"` + MsgBytes []byte `json:"msg_bytes"` + AltBytes []byte `json:"alt_bytes"` +} + +// StdSignBytes returns the bytes to sign for a transaction. +// TODO: change the API to just take a chainID and StdTx ? +func StdSignBytes(chainID string, sequences []int64, fee StdFee, msg Msg) []byte { + bz, err := json.Marshal(StdSignDoc{ + ChainID: chainID, + Sequences: sequences, + FeeBytes: fee.Bytes(), + MsgBytes: msg.GetSignBytes(), + }) + if err != nil { + panic(err) + } + return bz +} + +// StdSignMsg is a convenience structure for passing along +// a Msg with the other requirements for a StdSignDoc before +// it is signed. For use in the CLI. +type StdSignMsg struct { + ChainID string + Sequences []int64 + Fee StdFee + Msg Msg + // XXX: Alt +} + +func (msg StdSignMsg) Bytes() []byte { + return StdSignBytes(msg.ChainID, msg.Sequences, msg.Fee, msg.Msg) +} -// Application function variable used to unmarshal transaction bytes +//__________________________________________________________ + +// TxDeocder unmarshals transaction bytes type TxDecoder func(txBytes []byte) (Tx, Error) + +//__________________________________________________________ + +var _ Msg = (*TestMsg)(nil) + +// msg type for testing +type TestMsg struct { + signers []Address +} + +func NewTestMsg(addrs ...Address) *TestMsg { + return &TestMsg{ + signers: addrs, + } +} + +func (msg *TestMsg) Type() string { return "TestMsg" } +func (msg *TestMsg) Get(key interface{}) (value interface{}) { return nil } +func (msg *TestMsg) GetSignBytes() []byte { + bz, err := json.Marshal(msg.signers) + if err != nil { + panic(err) + } + return bz +} +func (msg *TestMsg) ValidateBasic() Error { return nil } +func (msg *TestMsg) GetSigners() []Address { + return msg.signers +} diff --git a/types/tx_msg_test.go b/types/tx_msg_test.go new file mode 100644 index 000000000000..f72cdea26e91 --- /dev/null +++ b/types/tx_msg_test.go @@ -0,0 +1,30 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + crypto "github.com/tendermint/go-crypto" +) + +func newStdFee() StdFee { + return NewStdFee(100, + Coin{"atom", 150}, + ) +} + +func TestStdTx(t *testing.T) { + priv := crypto.GenPrivKeyEd25519() + addr := priv.PubKey().Address() + msg := NewTestMsg(addr) + fee := newStdFee() + sigs := []StdSignature{} + + tx := NewStdTx(msg, fee, sigs) + assert.Equal(t, msg, tx.GetMsg()) + assert.Equal(t, sigs, tx.GetSignatures()) + + feePayer := FeePayer(tx) + assert.Equal(t, addr, feePayer) +} diff --git a/version/command.go b/version/command.go index 9986f605d736..c21ebb937933 100644 --- a/version/command.go +++ b/version/command.go @@ -2,6 +2,7 @@ package version import ( "fmt" + "net/http" "github.com/spf13/cobra" ) @@ -11,14 +12,28 @@ var ( VersionCmd = &cobra.Command{ Use: "version", Short: "Print the app version", - Run: doVersionCmd, + Run: printVersion, } ) -func doVersionCmd(cmd *cobra.Command, args []string) { +func getVersion() string { v := Version if GitCommit != "" { v = v + " " + GitCommit } + return v +} + +// CMD + +func printVersion(cmd *cobra.Command, args []string) { + v := getVersion() fmt.Println(v) } + +// REST + +func VersionRequestHandler(w http.ResponseWriter, r *http.Request) { + v := getVersion() + w.Write([]byte(v)) +} diff --git a/version/version.go b/version/version.go index 6380ce453e0a..59f530bfb4c7 100644 --- a/version/version.go +++ b/version/version.go @@ -6,10 +6,10 @@ package version // TODO improve const Maj = "0" -const Min = "11" +const Min = "12" const Fix = "0" -const Version = "0.11.0" +const Version = "0.12.0" // GitCommit set by build flags var GitCommit = "" diff --git a/wire/wire.go b/wire/wire.go new file mode 100644 index 000000000000..e53d5e195b2e --- /dev/null +++ b/wire/wire.go @@ -0,0 +1,55 @@ +package wire + +import ( + "bytes" + "reflect" + + "github.com/tendermint/go-wire" +) + +type Codec struct{} + +func NewCodec() *Codec { + return &Codec{} +} + +func (cdc *Codec) MarshalBinary(o interface{}) ([]byte, error) { + w, n, err := new(bytes.Buffer), new(int), new(error) + wire.WriteBinary(o, w, n, err) + return w.Bytes(), *err +} + +func (cdc *Codec) UnmarshalBinary(bz []byte, o interface{}) error { + r, n, err := bytes.NewBuffer(bz), new(int), new(error) + + rv := reflect.ValueOf(o) + if rv.Kind() == reflect.Ptr { + wire.ReadBinaryPtr(o, r, len(bz), n, err) + } else { + wire.ReadBinary(o, r, len(bz), n, err) + } + return *err +} + +func (cdc *Codec) MarshalJSON(o interface{}) ([]byte, error) { + w, n, err := new(bytes.Buffer), new(int), new(error) + wire.WriteJSON(o, w, n, err) + return w.Bytes(), *err +} + +func (cdc *Codec) UnmarshalJSON(bz []byte, o interface{}) (err error) { + + rv := reflect.ValueOf(o) + if rv.Kind() == reflect.Ptr { + wire.ReadJSONPtr(o, bz, &err) + } else { + wire.ReadJSON(o, bz, &err) + } + return err +} + +//---------------------------------------------- + +func RegisterCrypto(cdc *Codec) { + // TODO +} diff --git a/x/auth/ante.go b/x/auth/ante.go index 28785c8096b3..a5ab83bcbefa 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -1,90 +1,158 @@ package auth import ( + "bytes" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/viper" ) +// NewAnteHandler returns an AnteHandler that checks +// and increments sequence numbers, checks signatures, +// and deducts fees from the first signer. func NewAnteHandler(accountMapper sdk.AccountMapper) sdk.AnteHandler { return func( ctx sdk.Context, tx sdk.Tx, ) (_ sdk.Context, _ sdk.Result, abort bool) { - // Deduct the fee from the fee payer. - // This is done first because it only - // requires fetching 1 account. - payerAddr := tx.GetFeePayer() - if payerAddr != nil { - payerAcc := accountMapper.GetAccount(ctx, payerAddr) - if payerAcc == nil { - return ctx, - sdk.ErrUnrecognizedAddress(payerAddr).Result(), - true - } - // TODO: Charge fee from payerAcc. - // TODO: accountMapper.SetAccount(ctx, payerAddr) - } else { - // TODO: Ensure that some other spam prevention is used. - } - - var sigs = tx.GetSignatures() - // Assert that there are signatures. + var sigs = tx.GetSignatures() if len(sigs) == 0 { return ctx, sdk.ErrUnauthorized("no signers").Result(), true } - // Ensure that sigs are correct. - var msg = tx.GetMsg() - var signerAddrs = msg.GetSigners() - var signerAccs = make([]sdk.Account, len(signerAddrs)) + // TODO: can tx just implement message? + msg := tx.GetMsg() + + // TODO: will this always be a stdtx? should that be used in the function signature? + stdTx, ok := tx.(sdk.StdTx) + if !ok { + return ctx, sdk.ErrInternal("tx must be sdk.StdTx").Result(), true + } // Assert that number of signatures is correct. + var signerAddrs = msg.GetSigners() if len(sigs) != len(signerAddrs) { return ctx, sdk.ErrUnauthorized("wrong number of signers").Result(), true } - // Check each nonce and sig. - // TODO Refactor out. - for i, sig := range sigs { - - var signerAcc = accountMapper.GetAccount(ctx, signerAddrs[i]) - signerAccs[i] = signerAcc + // Get the sign bytes (requires all sequence numbers and the fee) + sequences := make([]int64, len(signerAddrs)) + for i := 0; i < len(signerAddrs); i++ { + sequences[i] = sigs[i].Sequence + } + fee := stdTx.Fee + chainID := ctx.ChainID() + // XXX: major hack; need to get ChainID + // into the app right away (#565) + if chainID == "" { + chainID = viper.GetString("chain-id") + } + signBytes := sdk.StdSignBytes(ctx.ChainID(), sequences, fee, msg) - // If no pubkey, set pubkey. - if signerAcc.GetPubKey() == nil { - err := signerAcc.SetPubKey(sig.PubKey) - if err != nil { - return ctx, - sdk.ErrInternal("setting PubKey on signer").Result(), - true - } - } + // Check sig and nonce and collect signer accounts. + var signerAccs = make([]sdk.Account, len(signerAddrs)) + for i := 0; i < len(sigs); i++ { + signerAddr, sig := signerAddrs[i], sigs[i] - // Check and increment sequence number. - seq := signerAcc.GetSequence() - if seq != sig.Sequence { - return ctx, - sdk.ErrInvalidSequence("").Result(), - true + // check signature, return account with incremented nonce + signerAcc, res := processSig( + ctx, accountMapper, + signerAddr, sig, signBytes, + ) + if !res.IsOK() { + return ctx, res, true } - signerAcc.SetSequence(seq + 1) - // Check sig. - if !sig.PubKey.VerifyBytes(msg.GetSignBytes(), sig.Signature) { - return ctx, - sdk.ErrUnauthorized("").Result(), - true + // first sig pays the fees + if i == 0 { + // TODO: min fee + if !fee.Amount.IsZero() { + signerAcc, res = deductFees(signerAcc, fee) + if !res.IsOK() { + return ctx, res, true + } + } } // Save the account. accountMapper.SetAccount(ctx, signerAcc) + signerAccs[i] = signerAcc } + // cache the signer accounts in the context ctx = WithSigners(ctx, signerAccs) + + // TODO: tx tags (?) + return ctx, sdk.Result{}, false // continue... } } + +// verify the signature and increment the sequence. +// if the account doesn't have a pubkey, set it. +func processSig( + ctx sdk.Context, am sdk.AccountMapper, + addr sdk.Address, sig sdk.StdSignature, signBytes []byte) ( + acc sdk.Account, res sdk.Result) { + + // Get the account. + acc = am.GetAccount(ctx, addr) + if acc == nil { + return nil, sdk.ErrUnknownAddress(addr.String()).Result() + } + + // Check and increment sequence number. + seq := acc.GetSequence() + if seq != sig.Sequence { + return nil, sdk.ErrInvalidSequence( + fmt.Sprintf("Invalid sequence. Got %d, expected %d", sig.Sequence, seq)).Result() + } + acc.SetSequence(seq + 1) + + // If pubkey is not known for account, + // set it from the StdSignature. + pubKey := acc.GetPubKey() + if pubKey.Empty() { + pubKey = sig.PubKey + if pubKey.Empty() { + return nil, sdk.ErrInvalidPubKey("PubKey not found").Result() + } + if !bytes.Equal(pubKey.Address(), addr) { + return nil, sdk.ErrInvalidPubKey( + fmt.Sprintf("PubKey does not match Signer address %v", addr)).Result() + } + err := acc.SetPubKey(pubKey) + if err != nil { + return nil, sdk.ErrInternal("setting PubKey on signer's account").Result() + } + } + + // Check sig. + if !pubKey.VerifyBytes(signBytes, sig.Signature) { + return nil, sdk.ErrUnauthorized("signature verification failed").Result() + } + + return +} + +// Deduct the fee from the account. +// We could use the CoinKeeper (in addition to the AccountMapper, +// because the CoinKeeper doesn't give us accounts), but it seems easier to do this. +func deductFees(acc sdk.Account, fee sdk.StdFee) (sdk.Account, sdk.Result) { + coins := acc.GetCoins() + feeAmount := fee.Amount + + newCoins := coins.Minus(feeAmount) + if !newCoins.IsNotNegative() { + errMsg := fmt.Sprintf("%s < %s", coins, feeAmount) + return nil, sdk.ErrInsufficientFunds(errMsg).Result() + } + acc.SetCoins(newCoins) + return acc, sdk.Result{} +} diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go new file mode 100644 index 000000000000..fd94145dde0b --- /dev/null +++ b/x/auth/ante_test.go @@ -0,0 +1,325 @@ +package auth + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" +) + +func newTestMsg(addrs ...sdk.Address) *sdk.TestMsg { + return sdk.NewTestMsg(addrs...) +} + +func newStdFee() sdk.StdFee { + return sdk.NewStdFee(100, + sdk.Coin{"atom", 150}, + ) +} + +// coins to more than cover the fee +func newCoins() sdk.Coins { + return sdk.Coins{ + {"atom", 10000000}, + } +} + +// generate a priv key and return it with its address +func privAndAddr() (crypto.PrivKey, sdk.Address) { + priv := crypto.GenPrivKeyEd25519() + addr := priv.PubKey().Address() + return priv.Wrap(), addr +} + +// run the tx through the anteHandler and ensure its valid +func checkValidTx(t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context, tx sdk.Tx) { + _, result, abort := anteHandler(ctx, tx) + assert.False(t, abort) + assert.Equal(t, sdk.CodeOK, result.Code) + assert.True(t, result.IsOK()) +} + +// run the tx through the anteHandler and ensure it fails with the given code +func checkInvalidTx(t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context, tx sdk.Tx, code sdk.CodeType) { + _, result, abort := anteHandler(ctx, tx) + assert.True(t, abort) + assert.Equal(t, code, result.Code) +} + +func newTestTx(ctx sdk.Context, msg sdk.Msg, privs []crypto.PrivKey, seqs []int64, fee sdk.StdFee) sdk.Tx { + signBytes := sdk.StdSignBytes(ctx.ChainID(), seqs, fee, msg) + return newTestTxWithSignBytes(msg, privs, seqs, fee, signBytes) +} + +func newTestTxWithSignBytes(msg sdk.Msg, privs []crypto.PrivKey, seqs []int64, fee sdk.StdFee, signBytes []byte) sdk.Tx { + sigs := make([]sdk.StdSignature, len(privs)) + for i, priv := range privs { + sigs[i] = sdk.StdSignature{PubKey: priv.PubKey(), Signature: priv.Sign(signBytes), Sequence: seqs[i]} + } + tx := sdk.NewStdTx(msg, fee, sigs) + return tx +} + +// Test various error cases in the AnteHandler control flow. +func TestAnteHandlerSigErrors(t *testing.T) { + // setup + ms, capKey := setupMultiStore() + mapper := NewAccountMapper(capKey, &BaseAccount{}) + anteHandler := NewAnteHandler(mapper) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + // keys and addresses + priv1, addr1 := privAndAddr() + priv2, addr2 := privAndAddr() + + // msg and signatures + var tx sdk.Tx + msg := newTestMsg(addr1, addr2) + fee := newStdFee() + + // test no signatures + privs, seqs := []crypto.PrivKey{}, []int64{} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized) + + // test num sigs dont match GetSigners + privs, seqs = []crypto.PrivKey{priv1}, []int64{0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized) + + // test an unrecognized account + privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{0, 0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnknownAddress) + + // save the first account, but second is still unrecognized + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(fee.Amount) + mapper.SetAccount(ctx, acc1) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnknownAddress) +} + +// Test logic around sequence checking with one signer and many signers. +func TestAnteHandlerSequences(t *testing.T) { + // setup + ms, capKey := setupMultiStore() + mapper := NewAccountMapper(capKey, &BaseAccount{}) + anteHandler := NewAnteHandler(mapper) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + // keys and addresses + priv1, addr1 := privAndAddr() + priv2, addr2 := privAndAddr() + + // set the accounts + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc1) + acc2 := mapper.NewAccountWithAddress(ctx, addr2) + acc2.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc2) + + // msg and signatures + var tx sdk.Tx + msg := newTestMsg(addr1) + fee := newStdFee() + + // test good tx from one signer + privs, seqs := []crypto.PrivKey{priv1}, []int64{0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) + + // test sending it again fails (replay protection) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidSequence) + + // fix sequence, should pass + seqs = []int64{1} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) + + // new tx with another signer and correct sequences + msg = newTestMsg(addr1, addr2) + privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{2, 0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) + + // replay fails + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidSequence) + + // tx from just second signer with incorrect sequence fails + msg = newTestMsg(addr2) + privs, seqs = []crypto.PrivKey{priv2}, []int64{0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidSequence) + + // fix the sequence and it passes + tx = newTestTx(ctx, msg, []crypto.PrivKey{priv2}, []int64{1}, fee) + checkValidTx(t, anteHandler, ctx, tx) + + // another tx from both of them that passes + msg = newTestMsg(addr1, addr2) + privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{3, 2} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) +} + +// Test logic around fee deduction. +func TestAnteHandlerFees(t *testing.T) { + // setup + ms, capKey := setupMultiStore() + mapper := NewAccountMapper(capKey, &BaseAccount{}) + anteHandler := NewAnteHandler(mapper) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + // keys and addresses + priv1, addr1 := privAndAddr() + + // set the accounts + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + mapper.SetAccount(ctx, acc1) + + // msg and signatures + var tx sdk.Tx + msg := newTestMsg(addr1) + privs, seqs := []crypto.PrivKey{priv1}, []int64{0} + fee := sdk.NewStdFee(100, + sdk.Coin{"atom", 150}, + ) + + // signer does not have enough funds to pay the fee + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInsufficientFunds) + + acc1.SetCoins(sdk.Coins{{"atom", 149}}) + mapper.SetAccount(ctx, acc1) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInsufficientFunds) + + acc1.SetCoins(sdk.Coins{{"atom", 150}}) + mapper.SetAccount(ctx, acc1) + checkValidTx(t, anteHandler, ctx, tx) +} + +func TestAnteHandlerBadSignBytes(t *testing.T) { + // setup + ms, capKey := setupMultiStore() + mapper := NewAccountMapper(capKey, &BaseAccount{}) + anteHandler := NewAnteHandler(mapper) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + // keys and addresses + priv1, addr1 := privAndAddr() + priv2, addr2 := privAndAddr() + + // set the accounts + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc1) + acc2 := mapper.NewAccountWithAddress(ctx, addr2) + acc2.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc2) + + var tx sdk.Tx + msg := newTestMsg(addr1) + fee := newStdFee() + fee2 := newStdFee() + fee2.Gas += 100 + fee3 := newStdFee() + fee3.Amount[0].Amount += 100 + + // test good tx and signBytes + privs, seqs := []crypto.PrivKey{priv1}, []int64{0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) + + chainID := ctx.ChainID() + chainID2 := chainID + "somemorestuff" + codeUnauth := sdk.CodeUnauthorized + + cases := []struct { + chainID string + seqs []int64 + fee sdk.StdFee + msg sdk.Msg + code sdk.CodeType + }{ + {chainID2, []int64{1}, fee, msg, codeUnauth}, // test wrong chain_id + {chainID, []int64{2}, fee, msg, codeUnauth}, // test wrong seqs + {chainID, []int64{1, 2}, fee, msg, codeUnauth}, // test wrong seqs + {chainID, []int64{1}, fee, newTestMsg(addr2), codeUnauth}, // test wrong msg + {chainID, []int64{1}, fee2, newTestMsg(addr2), codeUnauth}, // test wrong fee + {chainID, []int64{1}, fee3, newTestMsg(addr2), codeUnauth}, // test wrong fee + } + + privs, seqs = []crypto.PrivKey{priv1}, []int64{1} + for _, cs := range cases { + tx := newTestTxWithSignBytes( + msg, privs, seqs, fee, + sdk.StdSignBytes(cs.chainID, cs.seqs, cs.fee, cs.msg), + ) + checkInvalidTx(t, anteHandler, ctx, tx, cs.code) + } + + // test wrong signer if public key exist + privs, seqs = []crypto.PrivKey{priv2}, []int64{1} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized) + + // test wrong signer if public doesn't exist + msg = newTestMsg(addr2) + privs, seqs = []crypto.PrivKey{priv1}, []int64{0} + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey) + +} + +func TestAnteHandlerSetPubKey(t *testing.T) { + // setup + ms, capKey := setupMultiStore() + mapper := NewAccountMapper(capKey, &BaseAccount{}) + anteHandler := NewAnteHandler(mapper) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + // keys and addresses + priv1, addr1 := privAndAddr() + _, addr2 := privAndAddr() + + // set the accounts + acc1 := mapper.NewAccountWithAddress(ctx, addr1) + acc1.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc1) + acc2 := mapper.NewAccountWithAddress(ctx, addr2) + acc2.SetCoins(newCoins()) + mapper.SetAccount(ctx, acc2) + + var tx sdk.Tx + + // test good tx and set public key + msg := newTestMsg(addr1) + privs, seqs := []crypto.PrivKey{priv1}, []int64{0} + fee := newStdFee() + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkValidTx(t, anteHandler, ctx, tx) + + acc1 = mapper.GetAccount(ctx, addr1) + require.Equal(t, acc1.GetPubKey(), priv1.PubKey()) + + // test public key not found + msg = newTestMsg(addr2) + tx = newTestTx(ctx, msg, privs, seqs, fee) + sigs := tx.GetSignatures() + sigs[0].PubKey = crypto.PubKey{} + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey) + + acc2 = mapper.GetAccount(ctx, addr2) + assert.True(t, acc2.GetPubKey().Empty()) + + // test invalid signature and public key + tx = newTestTx(ctx, msg, privs, seqs, fee) + checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey) + + acc2 = mapper.GetAccount(ctx, addr2) + assert.True(t, acc2.GetPubKey().Empty()) +} diff --git a/x/auth/baseaccount.go b/x/auth/baseaccount.go index a9c5475203f0..23123f9949c9 100644 --- a/x/auth/baseaccount.go +++ b/x/auth/baseaccount.go @@ -3,9 +3,10 @@ package auth import ( "errors" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" ) //----------------------------------------------------------- @@ -17,13 +18,13 @@ var _ sdk.Account = (*BaseAccount)(nil) // Extend this by embedding this in your AppAccount. // See the examples/basecoin/types/account.go for an example. type BaseAccount struct { - Address crypto.Address `json:"address"` - Coins sdk.Coins `json:"coins"` - PubKey crypto.PubKey `json:"public_key"` - Sequence int64 `json:"sequence"` + Address sdk.Address `json:"address"` + Coins sdk.Coins `json:"coins"` + PubKey crypto.PubKey `json:"public_key"` + Sequence int64 `json:"sequence"` } -func NewBaseAccountWithAddress(addr crypto.Address) BaseAccount { +func NewBaseAccountWithAddress(addr sdk.Address) BaseAccount { return BaseAccount{ Address: addr, } @@ -40,12 +41,12 @@ func (acc *BaseAccount) Set(key interface{}, value interface{}) error { } // Implements sdk.Account. -func (acc BaseAccount) GetAddress() crypto.Address { +func (acc BaseAccount) GetAddress() sdk.Address { return acc.Address } // Implements sdk.Account. -func (acc *BaseAccount) SetAddress(addr crypto.Address) error { +func (acc *BaseAccount) SetAddress(addr sdk.Address) error { if len(acc.Address) != 0 { return errors.New("cannot override BaseAccount address") } @@ -60,7 +61,7 @@ func (acc BaseAccount) GetPubKey() crypto.PubKey { // Implements sdk.Account. func (acc *BaseAccount) SetPubKey(pubKey crypto.PubKey) error { - if acc.PubKey != nil { + if !acc.PubKey.Empty() { return errors.New("cannot override BaseAccount pubkey") } acc.PubKey = pubKey @@ -94,5 +95,5 @@ func (acc *BaseAccount) SetSequence(seq int64) error { func RegisterWireBaseAccount(cdc *wire.Codec) { // Register crypto.[PubKey,PrivKey,Signature] types. - crypto.RegisterWire(cdc) + wire.RegisterCrypto(cdc) } diff --git a/x/auth/baseaccount_test.go b/x/auth/baseaccount_test.go index a7397dc453d5..b2f5b54ae28a 100644 --- a/x/auth/baseaccount_test.go +++ b/x/auth/baseaccount_test.go @@ -3,48 +3,115 @@ package auth import ( "testing" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" ) -func TestBaseAccount(t *testing.T) { +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.Address) { key := crypto.GenPrivKeyEd25519() pub := key.PubKey() addr := pub.Address() - someCoins := sdk.Coins{{"atom", 123}, {"eth", 246}} - seq := int64(7) + return key.Wrap(), pub, addr +} - acc := NewBaseAccountWithAddress(addr) +func TestBaseAccountAddressPubKey(t *testing.T) { + _, pub1, addr1 := keyPubAddr() + _, pub2, addr2 := keyPubAddr() + acc := NewBaseAccountWithAddress(addr1) - // need a codec for marshaling - codec := wire.NewCodec() - crypto.RegisterWire(codec) + // check the address (set) and pubkey (not set) + assert.EqualValues(t, addr1, acc.GetAddress()) + assert.EqualValues(t, crypto.PubKey{}, acc.GetPubKey()) - err := acc.SetPubKey(pub) + // can't override address + err := acc.SetAddress(addr2) + assert.NotNil(t, err) + assert.EqualValues(t, addr1, acc.GetAddress()) + + // set the pubkey + err = acc.SetPubKey(pub1) assert.Nil(t, err) - assert.Equal(t, pub, acc.GetPubKey()) + assert.Equal(t, pub1, acc.GetPubKey()) - assert.Equal(t, addr, acc.GetAddress()) + // can't override pubkey + err = acc.SetPubKey(pub2) + assert.NotNil(t, err) + assert.Equal(t, pub1, acc.GetPubKey()) - err = acc.SetCoins(someCoins) + //------------------------------------ + + // can set address on empty account + acc2 := BaseAccount{} + err = acc2.SetAddress(addr2) + assert.Nil(t, err) + assert.EqualValues(t, addr2, acc2.GetAddress()) +} + +func TestBaseAccountCoins(t *testing.T) { + _, _, addr := keyPubAddr() + acc := NewBaseAccountWithAddress(addr) + + someCoins := sdk.Coins{{"atom", 123}, {"eth", 246}} + + err := acc.SetCoins(someCoins) assert.Nil(t, err) assert.Equal(t, someCoins, acc.GetCoins()) +} - err = acc.SetSequence(seq) +func TestBaseAccountSequence(t *testing.T) { + _, _, addr := keyPubAddr() + acc := NewBaseAccountWithAddress(addr) + + seq := int64(7) + + err := acc.SetSequence(seq) assert.Nil(t, err) assert.Equal(t, seq, acc.GetSequence()) +} + +func TestBaseAccountMarshal(t *testing.T) { + _, pub, addr := keyPubAddr() + acc := NewBaseAccountWithAddress(addr) + + someCoins := sdk.Coins{{"atom", 123}, {"eth", 246}} + seq := int64(7) + + // set everything on the account + err := acc.SetPubKey(pub) + assert.Nil(t, err) + err = acc.SetSequence(seq) + assert.Nil(t, err) + err = acc.SetCoins(someCoins) + assert.Nil(t, err) + + // need a codec for marshaling + codec := wire.NewCodec() + wire.RegisterCrypto(codec) b, err := codec.MarshalBinary(acc) assert.Nil(t, err) - var acc2 BaseAccount + acc2 := BaseAccount{} err = codec.UnmarshalBinary(b, &acc2) assert.Nil(t, err) assert.Equal(t, acc, acc2) + // error on bad bytes acc2 = BaseAccount{} err = codec.UnmarshalBinary(b[:len(b)/2], &acc2) assert.NotNil(t, err) + +} + +func TestBaseAccountGetSet(t *testing.T) { + _, _, addr := keyPubAddr() + acc := NewBaseAccountWithAddress(addr) + + // Get/Set are not yet defined - all values cause a panic. + assert.Panics(t, func() { acc.Get("key") }) + assert.Panics(t, func() { acc.Set("key", "value") }) } diff --git a/x/auth/commands/account.go b/x/auth/commands/account.go index 613442d720d9..02af73f167ca 100644 --- a/x/auth/commands/account.go +++ b/x/auth/commands/account.go @@ -8,34 +8,35 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" - - "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" ) // GetAccountCmd for the auth.BaseAccount type func GetAccountCmdDefault(storeName string, cdc *wire.Codec) *cobra.Command { - return GetAccountCmd(storeName, cdc, getParseAccount(cdc)) + return GetAccountCmd(storeName, cdc, GetAccountDecoder(cdc)) } -func getParseAccount(cdc *wire.Codec) sdk.ParseAccount { +func GetAccountDecoder(cdc *wire.Codec) sdk.AccountDecoder { return func(accBytes []byte) (sdk.Account, error) { acct := new(auth.BaseAccount) - err := cdc.UnmarshalBinary(accBytes, acct) + err := cdc.UnmarshalBinary(accBytes, &acct) + if err != nil { + panic(err) + } return acct, err } } // GetAccountCmd returns a query account that will display the // state of the account at a given address -func GetAccountCmd(storeName string, cdc *wire.Codec, parser sdk.ParseAccount) *cobra.Command { +func GetAccountCmd(storeName string, cdc *wire.Codec, decoder sdk.AccountDecoder) *cobra.Command { cmdr := commander{ storeName, cdc, - parser, + decoder, } return &cobra.Command{ Use: "account
", @@ -47,7 +48,7 @@ func GetAccountCmd(storeName string, cdc *wire.Codec, parser sdk.ParseAccount) * type commander struct { storeName string cdc *wire.Codec - parser sdk.ParseAccount + decoder sdk.AccountDecoder } func (c commander) getAccountCmd(cmd *cobra.Command, args []string) error { @@ -61,12 +62,15 @@ func (c commander) getAccountCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - key := crypto.Address(bz) + key := sdk.Address(bz) - res, err := client.Query(key, c.storeName) + res, err := builder.Query(key, c.storeName) + if err != nil { + return err + } - // parse out the value - account, err := c.parser(res) + // decode the value + account, err := c.decoder(res) if err != nil { return err } diff --git a/x/auth/context.go b/x/auth/context.go index 90de99f783fa..91259d9e6d89 100644 --- a/x/auth/context.go +++ b/x/auth/context.go @@ -38,5 +38,9 @@ func WithSigners(ctx types.Context, accounts []types.Account) types.Context { } func GetSigners(ctx types.Context) []types.Account { - return ctx.Value(contextKeySigners).([]types.Account) + v := ctx.Value(contextKeySigners) + if v == nil { + return []types.Account{} + } + return v.([]types.Account) } diff --git a/x/auth/context_test.go b/x/auth/context_test.go new file mode 100644 index 000000000000..0e4db8b08077 --- /dev/null +++ b/x/auth/context_test.go @@ -0,0 +1,39 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestContextWithSigners(t *testing.T) { + ms, _ := setupMultiStore() + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil) + + _, _, addr1 := keyPubAddr() + _, _, addr2 := keyPubAddr() + acc1 := NewBaseAccountWithAddress(addr1) + acc1.SetSequence(7132) + acc2 := NewBaseAccountWithAddress(addr2) + acc2.SetSequence(8821) + + // new ctx has no signers + signers := GetSigners(ctx) + assert.Equal(t, 0, len(signers)) + + ctx2 := WithSigners(ctx, []sdk.Account{&acc1, &acc2}) + + // original context is unchanged + signers = GetSigners(ctx) + assert.Equal(t, 0, len(signers)) + + // new context has signers + signers = GetSigners(ctx2) + assert.Equal(t, 2, len(signers)) + assert.Equal(t, acc1, *(signers[0].(*BaseAccount))) + assert.Equal(t, acc2, *(signers[1].(*BaseAccount))) +} diff --git a/x/auth/mapper.go b/x/auth/mapper.go index 6cd51552c7df..1176db3cbec2 100644 --- a/x/auth/mapper.go +++ b/x/auth/mapper.go @@ -1,15 +1,19 @@ package auth import ( + "bytes" "fmt" "reflect" - crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" + oldwire "github.com/tendermint/go-wire" sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" ) +var _ sdk.AccountMapper = (*accountMapper)(nil) +var _ sdk.AccountMapper = (*sealedAccountMapper)(nil) + // Implements sdk.AccountMapper. // This AccountMapper encodes/decodes accounts using the // go-wire (binary) encoding/decoding library. @@ -66,14 +70,14 @@ func (am accountMapper) Seal() sealedAccountMapper { } // Implements sdk.AccountMapper. -func (am accountMapper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) sdk.Account { +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 crypto.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 { @@ -107,6 +111,7 @@ func (sam sealedAccountMapper) WireCodec() *wire.Codec { //---------------------------------------- // misc. +// NOTE: currently unused func (am accountMapper) clonePrototypePtr() interface{} { protoRt := reflect.TypeOf(am.proto) if protoRt.Kind() == reflect.Ptr { @@ -155,14 +160,27 @@ func (am accountMapper) encodeAccount(acc sdk.Account) []byte { } func (am accountMapper) decodeAccount(bz []byte) sdk.Account { - accPtr := am.clonePrototypePtr() - err := am.cdc.UnmarshalBinary(bz, accPtr) - if err != nil { - panic(err) - } - if reflect.ValueOf(am.proto).Kind() == reflect.Ptr { - return reflect.ValueOf(accPtr).Interface().(sdk.Account) - } else { - return reflect.ValueOf(accPtr).Elem().Interface().(sdk.Account) + // ... old go-wire ... + r, n, err := bytes.NewBuffer(bz), new(int), new(error) + accI := oldwire.ReadBinary(struct{ sdk.Account }{}, r, len(bz), n, err) + if *err != nil { + panic(*err) + } + + acc := accI.(struct{ sdk.Account }).Account + return acc + + /* + accPtr := am.clonePrototypePtr() + err := am.cdc.UnmarshalBinary(bz, accPtr) + if err != nil { + panic(err) + } + if reflect.ValueOf(am.proto).Kind() == reflect.Ptr { + return reflect.ValueOf(accPtr).Interface().(sdk.Account) + } else { + return reflect.ValueOf(accPtr).Elem().Interface().(sdk.Account) + } + */ } diff --git a/x/auth/mapper_test.go b/x/auth/mapper_test.go new file mode 100644 index 000000000000..4ac96c381055 --- /dev/null +++ b/x/auth/mapper_test.go @@ -0,0 +1,81 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + oldwire "github.com/tendermint/go-wire" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { + db := dbm.NewMemDB() + capKey := sdk.NewKVStoreKey("capkey") + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + // wire registration while we're at it ... TODO + var _ = oldwire.RegisterInterface( + struct{ sdk.Account }{}, + oldwire.ConcreteType{&BaseAccount{}, 0x1}, + ) + + return ms, capKey +} + +func TestAccountMapperGetSet(t *testing.T) { + ms, capKey := setupMultiStore() + + // make context and mapper + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + mapper := NewAccountMapper(capKey, &BaseAccount{}) + + addr := sdk.Address([]byte("some-address")) + + // no account before its created + acc := mapper.GetAccount(ctx, addr) + assert.Nil(t, acc) + + // create account and check default values + acc = mapper.NewAccountWithAddress(ctx, addr) + assert.NotNil(t, acc) + assert.Equal(t, addr, acc.GetAddress()) + assert.EqualValues(t, crypto.PubKey{}, acc.GetPubKey()) + assert.EqualValues(t, 0, acc.GetSequence()) + + // NewAccount doesn't call Set, so it's still nil + assert.Nil(t, mapper.GetAccount(ctx, addr)) + + // set some values on the account and save it + newSequence := int64(20) + acc.SetSequence(newSequence) + mapper.SetAccount(ctx, acc) + + // check the new values + acc = mapper.GetAccount(ctx, addr) + assert.NotNil(t, acc) + assert.Equal(t, newSequence, acc.GetSequence()) +} + +func TestAccountMapperSealed(t *testing.T) { + _, capKey := setupMultiStore() + + // normal mapper exposes the wire codec + mapper := NewAccountMapper(capKey, &BaseAccount{}) + assert.NotNil(t, mapper.WireCodec()) + + // seal mapper, should panic when we try to get the codec + mapperSealed := mapper.Seal() + assert.Panics(t, func() { mapperSealed.WireCodec() }) + + // another way to get a sealed mapper + mapperSealed = NewAccountMapperSealed(capKey, &BaseAccount{}) + assert.Panics(t, func() { mapperSealed.WireCodec() }) +} diff --git a/x/auth/rest/query.go b/x/auth/rest/query.go new file mode 100644 index 000000000000..1ef9abe449b3 --- /dev/null +++ b/x/auth/rest/query.go @@ -0,0 +1,67 @@ +package rest + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +type commander struct { + storeName string + cdc *wire.Codec + decoder sdk.AccountDecoder +} + +func QueryAccountRequestHandler(storeName string, cdc *wire.Codec, decoder sdk.AccountDecoder) func(http.ResponseWriter, *http.Request) { + c := commander{storeName, cdc, decoder} + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + addr := vars["address"] + + bz, err := hex.DecodeString(addr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + key := sdk.Address(bz) + + res, err := builder.Query(key, c.storeName) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Could't query account. Error: %s", err.Error()))) + return + } + + // the query will return empty if there is no data for this account + if len(res) == 0 { + w.WriteHeader(http.StatusNoContent) + return + } + + // decode the value + account, err := c.decoder(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Could't parse query result. Result: %s. Error: %s", res, err.Error()))) + return + } + + // print out whole account + output, err := json.MarshalIndent(account, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Could't marshall query result. Error: %s", err.Error()))) + return + } + + w.Write(output) + } +} diff --git a/x/auth/rest/root.go b/x/auth/rest/root.go new file mode 100644 index 000000000000..5417cf868366 --- /dev/null +++ b/x/auth/rest/root.go @@ -0,0 +1,11 @@ +package rest + +import ( + "github.com/cosmos/cosmos-sdk/wire" + auth "github.com/cosmos/cosmos-sdk/x/auth/commands" + "github.com/gorilla/mux" +) + +func RegisterRoutes(r *mux.Router, cdc *wire.Codec, storeName string) { + r.HandleFunc("/accounts/{address}", QueryAccountRequestHandler(storeName, cdc, auth.GetAccountDecoder(cdc))).Methods("GET") +} diff --git a/x/bank/commands/sendtx.go b/x/bank/commands/sendtx.go index 941b0b69f7f8..5d1a6e05cfa1 100644 --- a/x/bank/commands/sendtx.go +++ b/x/bank/commands/sendtx.go @@ -4,30 +4,25 @@ import ( "encoding/hex" "fmt" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" - crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/builder" sdk "github.com/cosmos/cosmos-sdk/types" - + "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/bank" + cryptokeys "github.com/tendermint/go-crypto/keys" ) const ( - flagTo = "to" - flagAmount = "amount" - flagFee = "fee" - flagSequence = "seq" + flagTo = "to" + flagAmount = "amount" ) // SendTxCommand will create a send tx and sign it with the given key -func SendTxCmd(cdc *wire.Codec) *cobra.Command { - cmdr := commander{cdc} +func SendTxCmd(Cdc *wire.Codec) *cobra.Command { + cmdr := Commander{Cdc} cmd := &cobra.Command{ Use: "send", Short: "Create and sign a send tx", @@ -35,95 +30,88 @@ func SendTxCmd(cdc *wire.Codec) *cobra.Command { } cmd.Flags().String(flagTo, "", "Address to send coins") cmd.Flags().String(flagAmount, "", "Amount of coins to send") - cmd.Flags().String(flagFee, "", "Fee to pay along with transaction") - cmd.Flags().Int64(flagSequence, 0, "Sequence number to sign the tx") return cmd } -type commander struct { - cdc *wire.Codec +type Commander struct { + Cdc *wire.Codec } -func (c commander) sendTxCmd(cmd *cobra.Command, args []string) error { - txBytes, err := c.buildTx() +func (c Commander) sendTxCmd(cmd *cobra.Command, args []string) error { + // get the from address + from, err := builder.GetFromAddress() if err != nil { return err } - res, err := client.BroadcastTx(txBytes) + // parse coins + amount := viper.GetString(flagAmount) + coins, err := sdk.ParseCoins(amount) if err != nil { return err } - fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) - return nil -} - -func (c commander) buildTx() ([]byte, error) { - keybase, err := keys.GetKeyBase() + // parse destination address + dest := viper.GetString(flagTo) + bz, err := hex.DecodeString(dest) if err != nil { - return nil, err + return err } + to := sdk.Address(bz) + // get account name name := viper.GetString(client.FlagName) - info, err := keybase.Get(name) + + // get password + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) if err != nil { - return nil, errors.Errorf("No key for: %s", name) + return err } - from := info.PubKey.Address() - msg, err := buildMsg(from) + // build message + msg := BuildMsg(from, to, coins) + + // build and sign the transaction, then broadcast to Tendermint + res, err := builder.SignBuildBroadcast(name, passphrase, msg, c.Cdc) if err != nil { - return nil, err + return err } + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil +} + +func BuildMsg(from sdk.Address, to sdk.Address, coins sdk.Coins) sdk.Msg { + input := bank.NewInput(from, coins) + output := bank.NewOutput(to, coins) + msg := bank.NewSendMsg([]bank.Input{input}, []bank.Output{output}) + return msg +} + +func (c Commander) SignMessage(msg sdk.Msg, kb cryptokeys.Keybase, accountName string, password string) ([]byte, error) { // sign and build bz := msg.GetSignBytes() - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return nil, err - } - sig, pubkey, err := keybase.Sign(name, passphrase, bz) + sig, pubkey, err := kb.Sign(accountName, password, bz) if err != nil { return nil, err } sigs := []sdk.StdSignature{{ PubKey: pubkey, Signature: sig, - Sequence: viper.GetInt64(flagSequence), + Sequence: viper.GetInt64(client.FlagName), }} + // TODO: fees + var fee sdk.StdFee + // marshal bytes - tx := sdk.NewStdTx(msg, sigs) + tx := sdk.NewStdTx(msg, fee, sigs) - txBytes, err := c.cdc.MarshalBinary(tx) + txBytes, err := c.Cdc.MarshalBinary(tx) if err != nil { return nil, err } return txBytes, nil } - -func buildMsg(from crypto.Address) (sdk.Msg, error) { - - // parse coins - amount := viper.GetString(flagAmount) - coins, err := sdk.ParseCoins(amount) - if err != nil { - return nil, err - } - - // parse destination address - dest := viper.GetString(flagTo) - bz, err := hex.DecodeString(dest) - if err != nil { - return nil, err - } - to := crypto.Address(bz) - - input := bank.NewInput(from, coins) - output := bank.NewOutput(to, coins) - msg := bank.NewSendMsg([]bank.Input{input}, []bank.Output{output}) - return msg, nil -} diff --git a/x/bank/errors.go b/x/bank/errors.go index b922e166d421..4223dd30b49c 100644 --- a/x/bank/errors.go +++ b/x/bank/errors.go @@ -5,36 +5,19 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -type CodeType = sdk.CodeType - +// Coin errors reserve 100 ~ 199. const ( - // Coin errors reserve 100 ~ 199. - CodeInvalidInput CodeType = 101 - CodeInvalidOutput CodeType = 102 - CodeInvalidAddress CodeType = 103 - CodeUnknownAddress CodeType = 104 - CodeInsufficientCoins CodeType = 105 - CodeInvalidCoins CodeType = 106 - CodeUnknownRequest CodeType = sdk.CodeUnknownRequest + CodeInvalidInput sdk.CodeType = 101 + CodeInvalidOutput sdk.CodeType = 102 ) // NOTE: Don't stringer this, we'll put better messages in later. -func codeToDefaultMsg(code CodeType) string { +func codeToDefaultMsg(code sdk.CodeType) string { switch code { case CodeInvalidInput: return "Invalid input coins" case CodeInvalidOutput: return "Invalid output coins" - case CodeInvalidAddress: - return "Invalid address" - case CodeUnknownAddress: - return "Unknown address" - case CodeInsufficientCoins: - return "Insufficient coins" - case CodeInvalidCoins: - return "Invalid coins" - case CodeUnknownRequest: - return "Unknown request" default: return sdk.CodeToDefaultMsg(code) } @@ -59,41 +42,16 @@ func ErrNoOutputs() sdk.Error { return newError(CodeInvalidOutput, "") } -func ErrInvalidSequence(msg string) sdk.Error { - return sdk.ErrInvalidSequence(msg) -} - -func ErrInvalidAddress(msg string) sdk.Error { - return newError(CodeInvalidAddress, msg) -} - -func ErrUnknownAddress(msg string) sdk.Error { - return newError(CodeUnknownAddress, msg) -} - -func ErrInsufficientCoins(msg string) sdk.Error { - return newError(CodeInsufficientCoins, msg) -} - -func ErrInvalidCoins(msg string) sdk.Error { - return newError(CodeInvalidCoins, msg) -} - -func ErrUnknownRequest(msg string) sdk.Error { - return newError(CodeUnknownRequest, msg) -} - //---------------------------------------- -func msgOrDefaultMsg(msg string, code CodeType) string { +func msgOrDefaultMsg(msg string, code sdk.CodeType) string { if msg != "" { return msg - } else { - return codeToDefaultMsg(code) } + return codeToDefaultMsg(code) } -func newError(code CodeType, msg string) sdk.Error { +func newError(code sdk.CodeType, msg string) sdk.Error { msg = msgOrDefaultMsg(msg, code) return sdk.NewError(code, msg) } diff --git a/x/bank/mapper.go b/x/bank/mapper.go index 7bff2a2c19b8..0530967f1250 100644 --- a/x/bank/mapper.go +++ b/x/bank/mapper.go @@ -3,8 +3,6 @@ package bank import ( "fmt" - crypto "github.com/tendermint/go-crypto" - sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -19,16 +17,16 @@ func NewCoinKeeper(am sdk.AccountMapper) CoinKeeper { } // SubtractCoins subtracts amt from the coins at the addr. -func (ck CoinKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) { +func (ck CoinKeeper) SubtractCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) { acc := ck.am.GetAccount(ctx, addr) if acc == nil { - return amt, sdk.ErrUnrecognizedAddress(addr) + return amt, sdk.ErrUnknownAddress(addr.String()) } coins := acc.GetCoins() newCoins := coins.Minus(amt) if !newCoins.IsNotNegative() { - return amt, ErrInsufficientCoins(fmt.Sprintf("%s < %s", coins, amt)) + return amt, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", coins, amt)) } acc.SetCoins(newCoins) @@ -37,7 +35,7 @@ func (ck CoinKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt sdk } // AddCoins adds amt to the coins at the addr. -func (ck CoinKeeper) AddCoins(ctx sdk.Context, addr crypto.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) { +func (ck CoinKeeper) AddCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) { acc := ck.am.GetAccount(ctx, addr) if acc == nil { acc = ck.am.NewAccountWithAddress(ctx, addr) diff --git a/x/bank/rest/root.go b/x/bank/rest/root.go new file mode 100644 index 000000000000..4534482a9347 --- /dev/null +++ b/x/bank/rest/root.go @@ -0,0 +1,14 @@ +package rest + +import ( + "github.com/gorilla/mux" + + keys "github.com/tendermint/go-crypto/keys" + + "github.com/cosmos/cosmos-sdk/wire" +) + +// RegisterRoutes - Central function to define routes that get registered by the main application +func RegisterRoutes(r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + r.HandleFunc("/accounts/{address}/send", SendRequestHandler(cdc, kb)).Methods("POST") +} diff --git a/x/bank/rest/sendtx.go b/x/bank/rest/sendtx.go new file mode 100644 index 000000000000..85b9dc4d50a4 --- /dev/null +++ b/x/bank/rest/sendtx.go @@ -0,0 +1,102 @@ +package rest + +import ( + "encoding/hex" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/viper" + "github.com/tendermint/go-crypto/keys" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank/commands" +) + +type sendBody struct { + // fees is not used currently + // Fees sdk.Coin `json="fees"` + Amount sdk.Coins `json:"amount"` + LocalAccountName string `json:"name"` + Password string `json:"password"` + ChainID string `json:"chain_id"` + Sequence int64 `json:"sequence"` +} + +// SendRequestHandler - http request handler to send coins to a address +func SendRequestHandler(cdc *wire.Codec, kb keys.Keybase) func(http.ResponseWriter, *http.Request) { + c := commands.Commander{cdc} + return func(w http.ResponseWriter, r *http.Request) { + // collect data + vars := mux.Vars(r) + address := vars["address"] + + var m sendBody + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = json.Unmarshal(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + info, err := kb.Get(m.LocalAccountName) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + bz, err := hex.DecodeString(address) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + to := sdk.Address(bz) + + // build message + msg := commands.BuildMsg(info.PubKey.Address(), to, m.Amount) + if err != nil { // XXX rechecking same error ? + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + // sign + // XXX: OMG + viper.Set(client.FlagSequence, m.Sequence) + txBytes, err := builder.SignAndBuild(m.LocalAccountName, m.Password, msg, c.Cdc) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + // send + res, err := builder.BroadcastTx(txBytes) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + output, err := json.MarshalIndent(res, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(output) + } +} diff --git a/x/bank/tx.go b/x/bank/tx.go index b28fdf9c9a4e..05f824eba572 100644 --- a/x/bank/tx.go +++ b/x/bank/tx.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" - crypto "github.com/tendermint/go-crypto" - sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -15,6 +13,8 @@ type SendMsg struct { Outputs []Output `json:"outputs"` } +var _ sdk.Msg = SendMsg{} + // NewSendMsg - construct arbitrary multi-in, multi-out send msg. func NewSendMsg(in []Input, out []Output) SendMsg { return SendMsg{Inputs: in, Outputs: out} @@ -49,7 +49,7 @@ func (msg SendMsg) ValidateBasic() sdk.Error { } // make sure inputs and outputs match if !totalIn.IsEqual(totalOut) { - return ErrInvalidCoins(totalIn.String()).Trace("inputs and outputs don't match") + return sdk.ErrInvalidCoins(totalIn.String()).Trace("inputs and outputs don't match") } return nil } @@ -73,8 +73,8 @@ func (msg SendMsg) GetSignBytes() []byte { } // Implements Msg. -func (msg SendMsg) GetSigners() []crypto.Address { - addrs := make([]crypto.Address, len(msg.Inputs)) +func (msg SendMsg) GetSigners() []sdk.Address { + addrs := make([]sdk.Address, len(msg.Inputs)) for i, in := range msg.Inputs { addrs[i] = in.Address } @@ -86,17 +86,17 @@ func (msg SendMsg) GetSigners() []crypto.Address { // IssueMsg - high level transaction of the coin module type IssueMsg struct { - Banker crypto.Address `json:"banker"` - Outputs []Output `json:"outputs"` + Banker sdk.Address `json:"banker"` + Outputs []Output `json:"outputs"` } // NewIssueMsg - construct arbitrary multi-in, multi-out send msg. -func NewIssueMsg(banker crypto.Address, out []Output) IssueMsg { +func NewIssueMsg(banker sdk.Address, out []Output) IssueMsg { return IssueMsg{Banker: banker, Outputs: out} } // Implements Msg. -func (msg IssueMsg) Type() string { return "bank" } // TODO: "bank/send" +func (msg IssueMsg) Type() string { return "bank" } // TODO: "bank/issue" // Implements Msg. func (msg IssueMsg) ValidateBasic() sdk.Error { @@ -131,34 +131,29 @@ func (msg IssueMsg) GetSignBytes() []byte { } // Implements Msg. -func (msg IssueMsg) GetSigners() []crypto.Address { - return []crypto.Address{msg.Banker} +func (msg IssueMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Banker} } //---------------------------------------- // Input +// Transaction Output type Input struct { - Address crypto.Address `json:"address"` - Coins sdk.Coins `json:"coins"` - Sequence int64 `json:"sequence"` - - signature crypto.Signature + Address sdk.Address `json:"address"` + Coins sdk.Coins `json:"coins"` } // ValidateBasic - validate transaction input func (in Input) ValidateBasic() sdk.Error { if len(in.Address) == 0 { - return ErrInvalidAddress(in.Address.String()) - } - if in.Sequence < 0 { - return ErrInvalidSequence("negative sequence") + return sdk.ErrInvalidAddress(in.Address.String()) } if !in.Coins.IsValid() { - return ErrInvalidCoins(in.Coins.String()) + return sdk.ErrInvalidCoins(in.Coins.String()) } if !in.Coins.IsPositive() { - return ErrInvalidCoins(in.Coins.String()) + return sdk.ErrInvalidCoins(in.Coins.String()) } return nil } @@ -168,7 +163,7 @@ func (in Input) String() string { } // NewInput - create a transaction input, used with SendMsg -func NewInput(addr crypto.Address, coins sdk.Coins) Input { +func NewInput(addr sdk.Address, coins sdk.Coins) Input { input := Input{ Address: addr, Coins: coins, @@ -176,31 +171,25 @@ func NewInput(addr crypto.Address, coins sdk.Coins) Input { return input } -// NewInputWithSequence - create a transaction input, used with SendMsg -func NewInputWithSequence(addr crypto.Address, coins sdk.Coins, seq int64) Input { - input := NewInput(addr, coins) - input.Sequence = seq - return input -} - //---------------------------------------- // Output +// Transaction Output type Output struct { - Address crypto.Address `json:"address"` - Coins sdk.Coins `json:"coins"` + Address sdk.Address `json:"address"` + Coins sdk.Coins `json:"coins"` } // ValidateBasic - validate transaction output func (out Output) ValidateBasic() sdk.Error { if len(out.Address) == 0 { - return ErrInvalidAddress(out.Address.String()) + return sdk.ErrInvalidAddress(out.Address.String()) } if !out.Coins.IsValid() { - return ErrInvalidCoins(out.Coins.String()) + return sdk.ErrInvalidCoins(out.Coins.String()) } if !out.Coins.IsPositive() { - return ErrInvalidCoins(out.Coins.String()) + return sdk.ErrInvalidCoins(out.Coins.String()) } return nil } @@ -210,7 +199,7 @@ func (out Output) String() string { } // NewOutput - create a transaction output, used with SendMsg -func NewOutput(addr crypto.Address, coins sdk.Coins) Output { +func NewOutput(addr sdk.Address, coins sdk.Coins) Output { output := Output{ Address: addr, Coins: coins, diff --git a/x/bank/tx_test.go b/x/bank/tx_test.go index 2bc0458b2594..bbfdc62ffd39 100644 --- a/x/bank/tx_test.go +++ b/x/bank/tx_test.go @@ -6,8 +6,6 @@ import ( "github.com/stretchr/testify/assert" - crypto "github.com/tendermint/go-crypto" - sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -15,20 +13,12 @@ func TestNewSendMsg(t *testing.T) {} func TestSendMsgType(t *testing.T) { // Construct a SendMsg + addr1 := sdk.Address([]byte("input")) + addr2 := sdk.Address([]byte("output")) + coins := sdk.Coins{{"atom", 10}} var msg = SendMsg{ - Inputs: []Input{ - { - Address: crypto.Address([]byte("input")), - Coins: sdk.Coins{{"atom", 10}}, - Sequence: 1, - }, - }, - Outputs: []Output{ - { - Address: crypto.Address([]byte("output")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Inputs: []Input{NewInput(addr1, coins)}, + Outputs: []Output{NewOutput(addr2, coins)}, } // TODO some failures for bad result @@ -36,12 +26,12 @@ func TestSendMsgType(t *testing.T) { } func TestInputValidation(t *testing.T) { - addr1 := crypto.Address([]byte{1, 2}) - addr2 := crypto.Address([]byte{7, 8}) + addr1 := sdk.Address([]byte{1, 2}) + addr2 := sdk.Address([]byte{7, 8}) someCoins := sdk.Coins{{"atom", 123}} multiCoins := sdk.Coins{{"atom", 123}, {"eth", 20}} - var emptyAddr crypto.Address + var emptyAddr sdk.Address emptyCoins := sdk.Coins{} emptyCoins2 := sdk.Coins{{"eth", 0}} someEmptyCoins := sdk.Coins{{"eth", 10}, {"atom", 0}} @@ -55,18 +45,16 @@ func TestInputValidation(t *testing.T) { }{ // auth works with different apps {true, NewInput(addr1, someCoins)}, - {true, NewInputWithSequence(addr1, someCoins, 100)}, - {true, NewInputWithSequence(addr2, someCoins, 100)}, - {true, NewInputWithSequence(addr2, multiCoins, 100)}, - - {false, NewInput(emptyAddr, someCoins)}, // empty address - {false, NewInputWithSequence(addr1, someCoins, -1)}, // negative sequence - {false, NewInput(addr1, emptyCoins)}, // invalid coins - {false, NewInput(addr1, emptyCoins2)}, // invalid coins - {false, NewInput(addr1, someEmptyCoins)}, // invalid coins - {false, NewInput(addr1, minusCoins)}, // negative coins - {false, NewInput(addr1, someMinusCoins)}, // negative coins - {false, NewInput(addr1, unsortedCoins)}, // unsorted coins + {true, NewInput(addr2, someCoins)}, + {true, NewInput(addr2, multiCoins)}, + + {false, NewInput(emptyAddr, someCoins)}, // empty address + {false, NewInput(addr1, emptyCoins)}, // invalid coins + {false, NewInput(addr1, emptyCoins2)}, // invalid coins + {false, NewInput(addr1, someEmptyCoins)}, // invalid coins + {false, NewInput(addr1, minusCoins)}, // negative coins + {false, NewInput(addr1, someMinusCoins)}, // negative coins + {false, NewInput(addr1, unsortedCoins)}, // unsorted coins } for i, tc := range cases { @@ -80,12 +68,12 @@ func TestInputValidation(t *testing.T) { } func TestOutputValidation(t *testing.T) { - addr1 := crypto.Address([]byte{1, 2}) - addr2 := crypto.Address([]byte{7, 8}) + addr1 := sdk.Address([]byte{1, 2}) + addr2 := sdk.Address([]byte{7, 8}) someCoins := sdk.Coins{{"atom", 123}} multiCoins := sdk.Coins{{"atom", 123}, {"eth", 20}} - var emptyAddr crypto.Address + var emptyAddr sdk.Address emptyCoins := sdk.Coins{} emptyCoins2 := sdk.Coins{{"eth", 0}} someEmptyCoins := sdk.Coins{{"eth", 10}, {"atom", 0}} @@ -122,8 +110,8 @@ func TestOutputValidation(t *testing.T) { } func TestSendMsgValidation(t *testing.T) { - addr1 := crypto.Address([]byte{1, 2}) - addr2 := crypto.Address([]byte{7, 8}) + addr1 := sdk.Address([]byte{1, 2}) + addr2 := sdk.Address([]byte{7, 8}) atom123 := sdk.Coins{{"atom", 123}} atom124 := sdk.Coins{{"atom", 124}} eth123 := sdk.Coins{{"eth", 123}} @@ -136,7 +124,7 @@ func TestSendMsgValidation(t *testing.T) { output3 := NewOutput(addr2, eth123) outputMulti := NewOutput(addr2, atom123eth123) - var emptyAddr crypto.Address + var emptyAddr sdk.Address cases := []struct { valid bool @@ -146,7 +134,7 @@ func TestSendMsgValidation(t *testing.T) { {false, SendMsg{Inputs: []Input{input1}}}, // just input {false, SendMsg{Outputs: []Output{output1}}}, // just ouput {false, SendMsg{ - Inputs: []Input{NewInputWithSequence(emptyAddr, atom123, 1)}, // invalid input + Inputs: []Input{NewInput(emptyAddr, atom123)}, // invalid input Outputs: []Output{output1}}}, {false, SendMsg{ Inputs: []Input{input1}, @@ -191,89 +179,61 @@ func TestSendMsgValidation(t *testing.T) { func TestSendMsgString(t *testing.T) { // Construct a SendMsg + addr1 := sdk.Address([]byte("input")) + addr2 := sdk.Address([]byte("output")) + coins := sdk.Coins{{"atom", 10}} var msg = SendMsg{ - Inputs: []Input{ - { - Address: crypto.Address([]byte("input")), - Coins: sdk.Coins{{"atom", 10}}, - Sequence: 1, - }, - }, - Outputs: []Output{ - { - Address: crypto.Address([]byte("output")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Inputs: []Input{NewInput(addr1, coins)}, + Outputs: []Output{NewOutput(addr2, coins)}, } + res := msg.String() // TODO some failures for bad results assert.Equal(t, res, "SendMsg{[Input{696E707574,10atom}]->[Output{364637353734373037353734,10atom}]}") } func TestSendMsgGet(t *testing.T) { + addr1 := sdk.Address([]byte("input")) + addr2 := sdk.Address([]byte("output")) + coins := sdk.Coins{{"atom", 10}} var msg = SendMsg{ - Inputs: []Input{ - { - Address: crypto.Address([]byte("input")), - Coins: sdk.Coins{{"atom", 10}}, - Sequence: 1, - }, - }, - Outputs: []Output{ - { - Address: crypto.Address([]byte("output")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Inputs: []Input{NewInput(addr1, coins)}, + Outputs: []Output{NewOutput(addr2, coins)}, } res := msg.Get(nil) assert.Nil(t, res) } func TestSendMsgGetSignBytes(t *testing.T) { + addr1 := sdk.Address([]byte("input")) + addr2 := sdk.Address([]byte("output")) + coins := sdk.Coins{{"atom", 10}} var msg = SendMsg{ - Inputs: []Input{ - { - Address: crypto.Address([]byte("input")), - Coins: sdk.Coins{{"atom", 10}}, - Sequence: 1, - }, - }, - Outputs: []Output{ - { - Address: crypto.Address([]byte("output")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Inputs: []Input{NewInput(addr1, coins)}, + Outputs: []Output{NewOutput(addr2, coins)}, } res := msg.GetSignBytes() // TODO bad results - assert.Equal(t, string(res), `{"inputs":[{"address":"696E707574","coins":[{"denom":"atom","amount":10}],"sequence":1}],"outputs":[{"address":"6F7574707574","coins":[{"denom":"atom","amount":10}]}]}`) + assert.Equal(t, string(res), `{"inputs":[{"address":"696E707574","coins":[{"denom":"atom","amount":10}]}],"outputs":[{"address":"6F7574707574","coins":[{"denom":"atom","amount":10}]}]}`) } func TestSendMsgGetSigners(t *testing.T) { var msg = SendMsg{ Inputs: []Input{ - { - Address: crypto.Address([]byte("input1")), - }, - { - Address: crypto.Address([]byte("input2")), - }, - { - Address: crypto.Address([]byte("input3")), - }, + NewInput(sdk.Address([]byte("input1")), nil), + NewInput(sdk.Address([]byte("input2")), nil), + NewInput(sdk.Address([]byte("input3")), nil), }, } res := msg.GetSigners() + // TODO: fix this ! assert.Equal(t, fmt.Sprintf("%v", res), "[696E70757431 696E70757432 696E70757433]") } /* // what to do w/ this test? func TestSendMsgSigners(t *testing.T) { - signers := []crypto.Address{ + signers := []sdk.Address{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, @@ -299,14 +259,11 @@ func TestNewIssueMsg(t *testing.T) { func TestIssueMsgType(t *testing.T) { // Construct an IssueMsg + addr := sdk.Address([]byte("loan-from-bank")) + coins := sdk.Coins{{"atom", 10}} var msg = IssueMsg{ - Banker: crypto.Address([]byte("input")), - Outputs: []Output{ - { - Address: crypto.Address([]byte("loan-from-bank")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Banker: sdk.Address([]byte("input")), + Outputs: []Output{NewOutput(addr, coins)}, } // TODO some failures for bad result @@ -319,42 +276,34 @@ func TestIssueMsgValidation(t *testing.T) { func TestIssueMsgString(t *testing.T) { // Construct a IssueMsg + addr := sdk.Address([]byte("loan-from-bank")) + coins := sdk.Coins{{"atom", 10}} var msg = IssueMsg{ - Banker: crypto.Address([]byte("input")), - Outputs: []Output{ - { - Address: crypto.Address([]byte("loan-from-bank")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Banker: sdk.Address([]byte("input")), + Outputs: []Output{NewOutput(addr, coins)}, } res := msg.String() + // TODO: FIX THIS OUTPUT! assert.Equal(t, res, "IssueMsg{696E707574#[Output{36433646363136453244363637323646364432443632363136453642,10atom}]}") } func TestIssueMsgGet(t *testing.T) { + addr := sdk.Address([]byte("loan-from-bank")) + coins := sdk.Coins{{"atom", 10}} var msg = IssueMsg{ - Banker: crypto.Address([]byte("input")), - Outputs: []Output{ - { - Address: crypto.Address([]byte("loan-from-bank")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Banker: sdk.Address([]byte("input")), + Outputs: []Output{NewOutput(addr, coins)}, } res := msg.Get(nil) assert.Nil(t, res) } func TestIssueMsgGetSignBytes(t *testing.T) { + addr := sdk.Address([]byte("loan-from-bank")) + coins := sdk.Coins{{"atom", 10}} var msg = IssueMsg{ - Banker: crypto.Address([]byte("input")), - Outputs: []Output{ - { - Address: crypto.Address([]byte("loan-from-bank")), - Coins: sdk.Coins{{"atom", 10}}, - }, - }, + Banker: sdk.Address([]byte("input")), + Outputs: []Output{NewOutput(addr, coins)}, } res := msg.GetSignBytes() // TODO bad results @@ -363,7 +312,7 @@ func TestIssueMsgGetSignBytes(t *testing.T) { func TestIssueMsgGetSigners(t *testing.T) { var msg = IssueMsg{ - Banker: crypto.Address([]byte("onlyone")), + Banker: sdk.Address([]byte("onlyone")), } res := msg.GetSigners() assert.Equal(t, fmt.Sprintf("%v", res), "[6F6E6C796F6E65]") diff --git a/x/bank/wire.go b/x/bank/wire.go index 7162a416aa9a..846103a52057 100644 --- a/x/bank/wire.go +++ b/x/bank/wire.go @@ -1,11 +1,12 @@ package bank import ( - "github.com/tendermint/go-wire" + "github.com/cosmos/cosmos-sdk/wire" ) +// Register concrete types on wire codec func RegisterWire(cdc *wire.Codec) { // TODO include option to always include prefix bytes. - cdc.RegisterConcrete(SendMsg{}, "cosmos-sdk/SendMsg", nil) - cdc.RegisterConcrete(IssueMsg{}, "cosmos-sdk/IssueMsg", nil) + //cdc.RegisterConcrete(SendMsg{}, "github.com/cosmos/cosmos-sdk/bank/SendMsg", nil) + //cdc.RegisterConcrete(IssueMsg{}, "github.com/cosmos/cosmos-sdk/bank/IssueMsg", nil) } diff --git a/x/ibc/commands/README.md b/x/ibc/commands/README.md new file mode 100644 index 000000000000..11c46c3261d0 --- /dev/null +++ b/x/ibc/commands/README.md @@ -0,0 +1,25 @@ +# IBC CLI Usage + +## initialize + +```bash +basecoind init # copy the recover key +basecli keys add keyname --recover +basecoind start +``` + +## transfer + +`transfer` sends coins from one chain to another(or itself). + +```bash +basecli transfer --name keyname --to address_of_destination --amount 10mycoin --chain test-chain-AAAAAA --chain-id AAAAAA +``` + +The id of the chain can be found in `$HOME/.basecoind/config/genesis.json` + +## relay + +```bash +basecli relay --name keyname --from-chain-id test-chain-AAAAAA --from-chain-node=tcp://0.0.0.0:46657 --to-chain-id test-chain-AAAAAA --to-chain-node=tcp://0.0.0.0:46657 +``` diff --git a/x/ibc/commands/ibctx.go b/x/ibc/commands/ibctx.go new file mode 100644 index 000000000000..17d1e0048349 --- /dev/null +++ b/x/ibc/commands/ibctx.go @@ -0,0 +1,94 @@ +package commands + +import ( + "encoding/hex" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" + + "github.com/cosmos/cosmos-sdk/x/ibc" +) + +const ( + flagTo = "to" + flagAmount = "amount" + flagChain = "chain" +) + +func IBCTransferCmd(cdc *wire.Codec) *cobra.Command { + cmdr := sendCommander{cdc} + cmd := &cobra.Command{ + Use: "transfer", + RunE: cmdr.sendIBCTransfer, + } + cmd.Flags().String(flagTo, "", "Address to send coins") + cmd.Flags().String(flagAmount, "", "Amount of coins to send") + cmd.Flags().String(flagChain, "", "Destination chain to send coins") + return cmd +} + +type sendCommander struct { + cdc *wire.Codec +} + +func (c sendCommander) sendIBCTransfer(cmd *cobra.Command, args []string) error { + // get the from address + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + // build the message + msg, err := buildMsg(from) + if err != nil { + return err + } + + // get password + name := viper.GetString(client.FlagName) + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return err + } + + res, err := builder.SignBuildBroadcast(name, passphrase, msg, c.cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil +} + +func buildMsg(from sdk.Address) (sdk.Msg, error) { + amount := viper.GetString(flagAmount) + coins, err := sdk.ParseCoins(amount) + if err != nil { + return nil, err + } + + dest := viper.GetString(flagTo) + bz, err := hex.DecodeString(dest) + if err != nil { + return nil, err + } + to := sdk.Address(bz) + + packet := ibc.NewIBCPacket(from, to, coins, client.FlagChainID, + viper.GetString(flagChain)) + + msg := ibc.IBCTransferMsg{ + IBCPacket: packet, + } + + return msg, nil +} diff --git a/x/ibc/commands/relay.go b/x/ibc/commands/relay.go new file mode 100644 index 000000000000..091b0e920604 --- /dev/null +++ b/x/ibc/commands/relay.go @@ -0,0 +1,191 @@ +package commands + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" + + authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands" + "github.com/cosmos/cosmos-sdk/x/ibc" +) + +const ( + FlagFromChainID = "from-chain-id" + FlagFromChainNode = "from-chain-node" + FlagToChainID = "to-chain-id" + FlagToChainNode = "to-chain-node" +) + +type relayCommander struct { + cdc *wire.Codec + address sdk.Address + decoder sdk.AccountDecoder + mainStore string + ibcStore string +} + +func IBCRelayCmd(cdc *wire.Codec) *cobra.Command { + cmdr := relayCommander{ + cdc: cdc, + decoder: authcmd.GetAccountDecoder(cdc), + ibcStore: "ibc", + mainStore: "main", + } + + cmd := &cobra.Command{ + Use: "relay", + Run: cmdr.runIBCRelay, + } + + cmd.Flags().String(FlagFromChainID, "", "Chain ID for ibc node to check outgoing packets") + cmd.Flags().String(FlagFromChainNode, "tcp://localhost:46657", ": to tendermint rpc interface for this chain") + cmd.Flags().String(FlagToChainID, "", "Chain ID for ibc node to broadcast incoming packets") + cmd.Flags().String(FlagToChainNode, "tcp://localhost:36657", ": to tendermint rpc interface for this chain") + + cmd.MarkFlagRequired(FlagFromChainID) + cmd.MarkFlagRequired(FlagFromChainNode) + cmd.MarkFlagRequired(FlagToChainID) + cmd.MarkFlagRequired(FlagToChainNode) + + viper.BindPFlag(FlagFromChainID, cmd.Flags().Lookup(FlagFromChainID)) + viper.BindPFlag(FlagFromChainNode, cmd.Flags().Lookup(FlagFromChainNode)) + viper.BindPFlag(FlagToChainID, cmd.Flags().Lookup(FlagToChainID)) + viper.BindPFlag(FlagToChainNode, cmd.Flags().Lookup(FlagToChainNode)) + + return cmd +} + +func (c relayCommander) runIBCRelay(cmd *cobra.Command, args []string) { + fromChainID := viper.GetString(FlagFromChainID) + fromChainNode := viper.GetString(FlagFromChainNode) + toChainID := viper.GetString(FlagToChainID) + toChainNode := viper.GetString(FlagToChainNode) + address, err := builder.GetFromAddress() + if err != nil { + panic(err) + } + c.address = address + + c.loop(fromChainID, fromChainNode, toChainID, toChainNode) +} + +func (c relayCommander) loop(fromChainID, fromChainNode, toChainID, toChainNode string) { + // get password + name := viper.GetString(client.FlagName) + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + panic(err) + } + + ingressKey := ibc.IngressSequenceKey(fromChainID) + + processedbz, err := query(toChainNode, ingressKey, c.ibcStore) + if err != nil { + panic(err) + } + + var processed int64 + if processedbz == nil { + processed = 0 + } else if err = c.cdc.UnmarshalBinary(processedbz, &processed); err != nil { + panic(err) + } + +OUTER: + for { + time.Sleep(time.Second) + + lengthKey := ibc.EgressLengthKey(toChainID) + egressLengthbz, err := query(fromChainNode, lengthKey, c.ibcStore) + if err != nil { + fmt.Printf("Error querying outgoing packet list length: '%s'\n", err) + continue OUTER + } + var egressLength int64 + if egressLengthbz == nil { + egressLength = 0 + } else if err = c.cdc.UnmarshalBinary(egressLengthbz, &egressLength); err != nil { + panic(err) + } + fmt.Printf("egressLength queried: %d\n", egressLength) + + for i := processed; i < egressLength; i++ { + egressbz, err := query(fromChainNode, ibc.EgressKey(toChainID, i), c.ibcStore) + if err != nil { + fmt.Printf("Error querying egress packet: '%s'\n", err) + continue OUTER + } + + err = c.broadcastTx(toChainNode, c.refine(egressbz, i, passphrase)) + if err != nil { + fmt.Printf("Error broadcasting ingress packet: '%s'\n", err) + continue OUTER + } + + fmt.Printf("Relayed packet: %d\n", i) + } + + processed = egressLength + } +} + +func query(node string, key []byte, storeName string) (res []byte, err error) { + orig := viper.GetString(client.FlagNode) + viper.Set(client.FlagNode, node) + res, err = builder.Query(key, storeName) + viper.Set(client.FlagNode, orig) + return res, err +} + +func (c relayCommander) broadcastTx(node string, tx []byte) error { + orig := viper.GetString(client.FlagNode) + viper.Set(client.FlagNode, node) + seq := c.getSequence(node) + 1 + viper.Set(client.FlagSequence, seq) + _, err := builder.BroadcastTx(tx) + viper.Set(client.FlagNode, orig) + return err +} + +func (c relayCommander) getSequence(node string) int64 { + res, err := query(node, c.address, c.mainStore) + if err != nil { + panic(err) + } + account, err := c.decoder(res) + if err != nil { + panic(err) + } + + return account.GetSequence() +} + +func (c relayCommander) refine(bz []byte, sequence int64, passphrase string) []byte { + var packet ibc.IBCPacket + if err := c.cdc.UnmarshalBinary(bz, &packet); err != nil { + panic(err) + } + + msg := ibc.IBCReceiveMsg{ + IBCPacket: packet, + Relayer: c.address, + Sequence: sequence, + } + + name := viper.GetString(client.FlagName) + res, err := builder.SignAndBuild(name, passphrase, msg, c.cdc) + if err != nil { + panic(err) + } + return res +} diff --git a/x/ibc/errors.go b/x/ibc/errors.go new file mode 100644 index 000000000000..f3c988b595ab --- /dev/null +++ b/x/ibc/errors.go @@ -0,0 +1,47 @@ +package ibc + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // IBC errors reserve 200 - 299. + CodeInvalidSequence sdk.CodeType = 200 + CodeIdenticalChains sdk.CodeType = 201 + CodeUnknownRequest sdk.CodeType = sdk.CodeUnknownRequest +) + +func codeToDefaultMsg(code sdk.CodeType) string { + switch code { + case CodeInvalidSequence: + return "Invalid IBC packet sequence" + case CodeIdenticalChains: + return "Source and destination chain cannot be identical" + default: + return sdk.CodeToDefaultMsg(code) + } +} + +func ErrInvalidSequence() sdk.Error { + return newError(CodeInvalidSequence, "") +} + +func ErrIdenticalChains() sdk.Error { + return newError(CodeIdenticalChains, "") +} + +// ------------------------- +// Helpers + +func newError(code sdk.CodeType, msg string) sdk.Error { + msg = msgOrDefaultMsg(msg, code) + return sdk.NewError(code, msg) +} + +func msgOrDefaultMsg(msg string, code sdk.CodeType) string { + if msg != "" { + return msg + } else { + return codeToDefaultMsg(code) + } +} diff --git a/x/ibc/handler.go b/x/ibc/handler.go new file mode 100644 index 000000000000..2922f6dabb9f --- /dev/null +++ b/x/ibc/handler.go @@ -0,0 +1,58 @@ +package ibc + +import ( + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +func NewHandler(ibcm IBCMapper, ck bank.CoinKeeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case IBCTransferMsg: + return handleIBCTransferMsg(ctx, ibcm, ck, msg) + case IBCReceiveMsg: + return handleIBCReceiveMsg(ctx, ibcm, ck, msg) + default: + errMsg := "Unrecognized IBC Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// IBCTransferMsg deducts coins from the account and creates an egress IBC packet. +func handleIBCTransferMsg(ctx sdk.Context, ibcm IBCMapper, ck bank.CoinKeeper, msg IBCTransferMsg) sdk.Result { + packet := msg.IBCPacket + + _, err := ck.SubtractCoins(ctx, packet.SrcAddr, packet.Coins) + if err != nil { + return err.Result() + } + + err = ibcm.PostIBCPacket(ctx, packet) + if err != nil { + return err.Result() + } + + return sdk.Result{} +} + +// IBCReceiveMsg adds coins to the destination address and creates an ingress IBC packet. +func handleIBCReceiveMsg(ctx sdk.Context, ibcm IBCMapper, ck bank.CoinKeeper, msg IBCReceiveMsg) sdk.Result { + packet := msg.IBCPacket + + seq := ibcm.GetIngressSequence(ctx, packet.SrcChain) + if msg.Sequence != seq { + return ErrInvalidSequence().Result() + } + + _, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins) + if err != nil { + return err.Result() + } + + ibcm.SetIngressSequence(ctx, packet.SrcChain, seq+1) + + return sdk.Result{} +} diff --git a/x/ibc/ibc_test.go b/x/ibc/ibc_test.go new file mode 100644 index 000000000000..bec08fb564a4 --- /dev/null +++ b/x/ibc/ibc_test.go @@ -0,0 +1,151 @@ +package ibc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/go-crypto" + oldwire "github.com/tendermint/go-wire" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + + "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" +) + +// AccountMapper(/CoinKeeper) and IBCMapper should use different StoreKey later + +func defaultContext(key sdk.StoreKey) sdk.Context { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db) + cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + cms.LoadLatestVersion() + ctx := sdk.NewContext(cms, abci.Header{}, false, nil) + return ctx +} + +func newAddress() crypto.Address { + return crypto.GenPrivKeyEd25519().PubKey().Address() +} + +func getCoins(ck bank.CoinKeeper, ctx sdk.Context, addr crypto.Address) (sdk.Coins, sdk.Error) { + zero := sdk.Coins{} + return ck.AddCoins(ctx, addr, zero) +} + +// custom tx codec +// TODO: use new go-wire +func makeCodec() *wire.Codec { + + const msgTypeSend = 0x1 + const msgTypeIssue = 0x2 + const msgTypeQuiz = 0x3 + const msgTypeSetTrend = 0x4 + const msgTypeIBCTransferMsg = 0x5 + const msgTypeIBCReceiveMsg = 0x6 + var _ = oldwire.RegisterInterface( + struct{ sdk.Msg }{}, + oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, + oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, + oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, + oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, + oldwire.ConcreteType{IBCTransferMsg{}, msgTypeIBCTransferMsg}, + oldwire.ConcreteType{IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, + ) + + const accTypeApp = 0x1 + var _ = oldwire.RegisterInterface( + struct{ sdk.Account }{}, + oldwire.ConcreteType{&auth.BaseAccount{}, accTypeApp}, + ) + cdc := wire.NewCodec() + + // cdc.RegisterInterface((*sdk.Msg)(nil), nil) + // bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. + // crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + // ibc.RegisterWire(cdc) // Register ibc.[IBCTransferMsg, IBCReceiveMsg] types. + return cdc +} + +func TestIBC(t *testing.T) { + cdc := makeCodec() + + key := sdk.NewKVStoreKey("ibc") + ctx := defaultContext(key) + + am := auth.NewAccountMapper(key, &auth.BaseAccount{}) + ck := bank.NewCoinKeeper(am) + + src := newAddress() + dest := newAddress() + chainid := "ibcchain" + zero := sdk.Coins{} + mycoins := sdk.Coins{sdk.Coin{"mycoin", 10}} + + coins, err := ck.AddCoins(ctx, src, mycoins) + assert.Nil(t, err) + assert.Equal(t, mycoins, coins) + + ibcm := NewIBCMapper(cdc, key) + h := NewHandler(ibcm, ck) + packet := IBCPacket{ + SrcAddr: src, + DestAddr: dest, + Coins: mycoins, + SrcChain: chainid, + DestChain: chainid, + } + + store := ctx.KVStore(key) + + var msg sdk.Msg + var res sdk.Result + var egl int64 + var igs int64 + + egl = ibcm.getEgressLength(store, chainid) + assert.Equal(t, egl, int64(0)) + + msg = IBCTransferMsg{ + IBCPacket: packet, + } + res = h(ctx, msg) + assert.True(t, res.IsOK()) + + coins, err = getCoins(ck, ctx, src) + assert.Nil(t, err) + assert.Equal(t, zero, coins) + + egl = ibcm.getEgressLength(store, chainid) + assert.Equal(t, egl, int64(1)) + + igs = ibcm.GetIngressSequence(ctx, chainid) + assert.Equal(t, igs, int64(0)) + + msg = IBCReceiveMsg{ + IBCPacket: packet, + Relayer: src, + Sequence: 0, + } + res = h(ctx, msg) + assert.True(t, res.IsOK()) + + coins, err = getCoins(ck, ctx, dest) + assert.Nil(t, err) + assert.Equal(t, mycoins, coins) + + igs = ibcm.GetIngressSequence(ctx, chainid) + assert.Equal(t, igs, int64(1)) + + res = h(ctx, msg) + assert.False(t, res.IsOK()) + + igs = ibcm.GetIngressSequence(ctx, chainid) + assert.Equal(t, igs, int64(1)) +} diff --git a/x/ibc/mapper.go b/x/ibc/mapper.go new file mode 100644 index 000000000000..1e8f9de25366 --- /dev/null +++ b/x/ibc/mapper.go @@ -0,0 +1,125 @@ +package ibc + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" +) + +type IBCMapper struct { + key sdk.StoreKey + cdc *wire.Codec +} + +// XXX: The IBCMapper should not take a CoinKeeper. Rather have the CoinKeeper +// take an IBCMapper. +func NewIBCMapper(cdc *wire.Codec, key sdk.StoreKey) IBCMapper { + // XXX: How are these codecs supposed to work? + return IBCMapper{ + key: key, + cdc: cdc, + } +} + +// XXX: This is not the public API. This will change in MVP2 and will henceforth +// only be invoked from another module directly and not through a user +// transaction. +// TODO: Handle invalid IBC packets and return errors. +func (ibcm IBCMapper) PostIBCPacket(ctx sdk.Context, packet IBCPacket) sdk.Error { + // write everything into the state + store := ctx.KVStore(ibcm.key) + index := ibcm.getEgressLength(store, packet.DestChain) + bz, err := ibcm.cdc.MarshalBinary(packet) + if err != nil { + panic(err) + } + + store.Set(EgressKey(packet.DestChain, index), bz) + bz, err = ibcm.cdc.MarshalBinary(int64(index + 1)) + if err != nil { + panic(err) + } + store.Set(EgressLengthKey(packet.DestChain), bz) + + return nil +} + +// XXX: In the future every module is able to register it's own handler for +// handling it's own IBC packets. The "ibc" handler will only route the packets +// to the appropriate callbacks. +// XXX: For now this handles all interactions with the CoinKeeper. +// XXX: This needs to do some authentication checking. +func (ibcm IBCMapper) ReceiveIBCPacket(ctx sdk.Context, packet IBCPacket) sdk.Error { + return nil +} + +// -------------------------- +// Functions for accessing the underlying KVStore. + +func marshalBinaryPanic(cdc *wire.Codec, value interface{}) []byte { + res, err := cdc.MarshalBinary(value) + if err != nil { + panic(err) + } + return res +} + +func unmarshalBinaryPanic(cdc *wire.Codec, bz []byte, ptr interface{}) { + err := cdc.UnmarshalBinary(bz, ptr) + if err != nil { + panic(err) + } +} + +func (ibcm IBCMapper) GetIngressSequence(ctx sdk.Context, srcChain string) int64 { + store := ctx.KVStore(ibcm.key) + key := IngressSequenceKey(srcChain) + + bz := store.Get(key) + if bz == nil { + zero := marshalBinaryPanic(ibcm.cdc, int64(0)) + store.Set(key, zero) + return 0 + } + + var res int64 + unmarshalBinaryPanic(ibcm.cdc, bz, &res) + return res +} + +func (ibcm IBCMapper) SetIngressSequence(ctx sdk.Context, srcChain string, sequence int64) { + store := ctx.KVStore(ibcm.key) + key := IngressSequenceKey(srcChain) + + bz := marshalBinaryPanic(ibcm.cdc, sequence) + store.Set(key, bz) +} + +// Retrieves the index of the currently stored outgoing IBC packets. +func (ibcm IBCMapper) getEgressLength(store sdk.KVStore, destChain string) int64 { + bz := store.Get(EgressLengthKey(destChain)) + if bz == nil { + zero := marshalBinaryPanic(ibcm.cdc, int64(0)) + store.Set(EgressLengthKey(destChain), zero) + return 0 + } + var res int64 + unmarshalBinaryPanic(ibcm.cdc, bz, &res) + return res +} + +// Stores an outgoing IBC packet under "egress/chain_id/index". +func EgressKey(destChain string, index int64) []byte { + return []byte(fmt.Sprintf("egress/%s/%d", destChain, index)) +} + +// Stores the number of outgoing IBC packets under "egress/index". +func EgressLengthKey(destChain string) []byte { + return []byte(fmt.Sprintf("egress/%s", destChain)) +} + +// Stores the sequence number of incoming IBC packet under "ingress/index". +func IngressSequenceKey(srcChain string) []byte { + return []byte(fmt.Sprintf("ingress/%s", srcChain)) +} diff --git a/x/ibc/types.go b/x/ibc/types.go new file mode 100644 index 000000000000..495d2a90096d --- /dev/null +++ b/x/ibc/types.go @@ -0,0 +1,113 @@ +package ibc + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + wire "github.com/cosmos/cosmos-sdk/wire" +) + +// ------------------------------ +// IBCPacket + +// IBCPacket defines a piece of data that can be send between two separate +// blockchains. +type IBCPacket struct { + SrcAddr sdk.Address + DestAddr sdk.Address + Coins sdk.Coins + SrcChain string + DestChain string +} + +func NewIBCPacket(srcAddr sdk.Address, destAddr sdk.Address, coins sdk.Coins, + srcChain string, destChain string) IBCPacket { + + return IBCPacket{ + SrcAddr: srcAddr, + DestAddr: destAddr, + Coins: coins, + SrcChain: srcChain, + DestChain: destChain, + } +} + +func (ibcp IBCPacket) ValidateBasic() sdk.Error { + if ibcp.SrcChain == ibcp.DestChain { + return ErrIdenticalChains().Trace("") + } + if !ibcp.Coins.IsValid() { + return sdk.ErrInvalidCoins("") + } + return nil +} + +// ---------------------------------- +// IBCTransferMsg + +// IBCTransferMsg defines how another module can send an IBCPacket. +type IBCTransferMsg struct { + IBCPacket +} + +func (msg IBCTransferMsg) Type() string { + return "ibc" +} + +func (msg IBCTransferMsg) Get(key interface{}) interface{} { + return nil +} + +func (msg IBCTransferMsg) GetSignBytes() []byte { + cdc := wire.NewCodec() + bz, err := cdc.MarshalBinary(msg) + if err != nil { + panic(err) + } + return bz +} + +func (msg IBCTransferMsg) ValidateBasic() sdk.Error { + return msg.IBCPacket.ValidateBasic() +} + +// x/bank/tx.go SendMsg.GetSigners() +func (msg IBCTransferMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.SrcAddr} +} + +// ---------------------------------- +// IBCReceiveMsg + +// IBCReceiveMsg defines the message that a relayer uses to post an IBCPacket +// to the destination chain. +type IBCReceiveMsg struct { + IBCPacket + Relayer sdk.Address + Sequence int64 +} + +func (msg IBCReceiveMsg) Type() string { + return "ibc" +} + +func (msg IBCReceiveMsg) Get(key interface{}) interface{} { + return nil +} + +func (msg IBCReceiveMsg) GetSignBytes() []byte { + cdc := wire.NewCodec() + bz, err := cdc.MarshalBinary(msg) + if err != nil { + panic(err) + } + return bz +} + +func (msg IBCReceiveMsg) ValidateBasic() sdk.Error { + return msg.IBCPacket.ValidateBasic() +} + +// x/bank/tx.go SendMsg.GetSigners() +func (msg IBCReceiveMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Relayer} +} diff --git a/x/ibc/types_test.go b/x/ibc/types_test.go new file mode 100644 index 000000000000..c16839ddca6f --- /dev/null +++ b/x/ibc/types_test.go @@ -0,0 +1,112 @@ +package ibc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// -------------------------------- +// IBCPacket Tests + +func TestIBCPacketValidation(t *testing.T) { + cases := []struct { + valid bool + packet IBCPacket + }{ + {true, constructIBCPacket(true)}, + {false, constructIBCPacket(false)}, + } + + for i, tc := range cases { + err := tc.packet.ValidateBasic() + if tc.valid { + assert.Nil(t, err, "%d: %+v", i, err) + } else { + assert.NotNil(t, err, "%d", i) + } + } +} + +// ------------------------------- +// IBCTransferMsg Tests + +func TestIBCTransferMsg(t *testing.T) { + packet := constructIBCPacket(true) + msg := IBCTransferMsg{packet} + + assert.Equal(t, msg.Type(), "ibc") +} + +func TestIBCTransferMsgValidation(t *testing.T) { + validPacket := constructIBCPacket(true) + invalidPacket := constructIBCPacket(false) + + cases := []struct { + valid bool + msg IBCTransferMsg + }{ + {true, IBCTransferMsg{validPacket}}, + {false, IBCTransferMsg{invalidPacket}}, + } + + for i, tc := range cases { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.Nil(t, err, "%d: %+v", i, err) + } else { + assert.NotNil(t, err, "%d", i) + } + } +} + +// ------------------------------- +// IBCReceiveMsg Tests + +func TestIBCReceiveMsg(t *testing.T) { + packet := constructIBCPacket(true) + msg := IBCReceiveMsg{packet, sdk.Address([]byte("relayer")), 0} + + assert.Equal(t, msg.Type(), "ibc") +} + +func TestIBCReceiveMsgValidation(t *testing.T) { + validPacket := constructIBCPacket(true) + invalidPacket := constructIBCPacket(false) + + cases := []struct { + valid bool + msg IBCReceiveMsg + }{ + {true, IBCReceiveMsg{validPacket, sdk.Address([]byte("relayer")), 0}}, + {false, IBCReceiveMsg{invalidPacket, sdk.Address([]byte("relayer")), 0}}, + } + + for i, tc := range cases { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.Nil(t, err, "%d: %+v", i, err) + } else { + assert.NotNil(t, err, "%d", i) + } + } +} + +// ------------------------------- +// Helpers + +func constructIBCPacket(valid bool) IBCPacket { + srcAddr := sdk.Address([]byte("source")) + destAddr := sdk.Address([]byte("destination")) + coins := sdk.Coins{{"atom", 10}} + srcChain := "source-chain" + destChain := "dest-chain" + + if valid { + return NewIBCPacket(srcAddr, destAddr, coins, srcChain, destChain) + } else { + return NewIBCPacket(srcAddr, destAddr, coins, srcChain, srcChain) + } +} diff --git a/x/ibc/wire.go b/x/ibc/wire.go new file mode 100644 index 000000000000..91e6d88bbcbb --- /dev/null +++ b/x/ibc/wire.go @@ -0,0 +1,11 @@ +package ibc + +import ( + "github.com/cosmos/cosmos-sdk/wire" +) + +// Register concrete types on wire codec +func RegisterWire(cdc *wire.Codec) { + //cdc.RegisterConcrete(IBCTransferMsg{}, "github.com/cosmos/cosmos-sdk/x/ibc/IBCTransferMsg", nil) + //cdc.RegisterConcrete(IBCReceiveMsg{}, "github.com/cosmos/cosmos-sdk/x/ibc/IBCReceiveMsg", nil) +} diff --git a/x/staking/commands/commands.go b/x/staking/commands/commands.go new file mode 100644 index 000000000000..c2830d3951aa --- /dev/null +++ b/x/staking/commands/commands.go @@ -0,0 +1,111 @@ +package commands + +import ( + "encoding/hex" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +const ( + flagStake = "stake" + flagValidator = "validator" +) + +func BondTxCmd(cdc *wire.Codec) *cobra.Command { + cmdr := commander{cdc} + cmd := &cobra.Command{ + Use: "bond", + Short: "Bond to a validator", + RunE: cmdr.bondTxCmd, + } + cmd.Flags().String(flagStake, "", "Amount of coins to stake") + cmd.Flags().String(flagValidator, "", "Validator address to stake") + return cmd +} + +func UnbondTxCmd(cdc *wire.Codec) *cobra.Command { + cmdr := commander{cdc} + cmd := &cobra.Command{ + Use: "unbond", + Short: "Unbond from a validator", + RunE: cmdr.unbondTxCmd, + } + return cmd +} + +type commander struct { + cdc *wire.Codec +} + +func (co commander) bondTxCmd(cmd *cobra.Command, args []string) error { + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + stakeString := viper.GetString(flagStake) + if len(stakeString) == 0 { + return fmt.Errorf("specify coins to bond with --stake") + } + + valString := viper.GetString(flagValidator) + if len(valString) == 0 { + return fmt.Errorf("specify pubkey to bond to with --validator") + } + + stake, err := sdk.ParseCoin(stakeString) + if err != nil { + return err + } + + // TODO: bech32 ... + rawPubKey, err := hex.DecodeString(valString) + if err != nil { + return err + } + var pubKeyEd crypto.PubKeyEd25519 + copy(pubKeyEd[:], rawPubKey) + + msg := staking.NewBondMsg(from, stake, pubKeyEd.Wrap()) + + return co.sendMsg(msg) +} + +func (co commander) unbondTxCmd(cmd *cobra.Command, args []string) error { + from, err := builder.GetFromAddress() + if err != nil { + return err + } + + msg := staking.NewUnbondMsg(from) + + return co.sendMsg(msg) +} + +func (co commander) sendMsg(msg sdk.Msg) error { + name := viper.GetString(client.FlagName) + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return err + } + + res, err := builder.SignBuildBroadcast(name, passphrase, msg, co.cdc) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil +} diff --git a/x/staking/errors.go b/x/staking/errors.go new file mode 100644 index 000000000000..77f009fe0fb1 --- /dev/null +++ b/x/staking/errors.go @@ -0,0 +1,31 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // Staking errors reserve 300 - 399. + CodeEmptyValidator sdk.CodeType = 300 + CodeInvalidUnbond sdk.CodeType = 301 + CodeEmptyStake sdk.CodeType = 302 +) + +func ErrEmptyValidator() sdk.Error { + return newError(CodeEmptyValidator, "") +} + +func ErrInvalidUnbond() sdk.Error { + return newError(CodeInvalidUnbond, "") +} + +func ErrEmptyStake() sdk.Error { + return newError(CodeEmptyStake, "") +} + +// ----------------------------- +// Helpers + +func newError(code sdk.CodeType, msg string) sdk.Error { + return sdk.NewError(code, msg) +} diff --git a/x/staking/handler.go b/x/staking/handler.go new file mode 100644 index 000000000000..c14756b7ab59 --- /dev/null +++ b/x/staking/handler.go @@ -0,0 +1,69 @@ +package staking + +import ( + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +func NewHandler(sm StakingMapper, ck bank.CoinKeeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case BondMsg: + return handleBondMsg(ctx, sm, ck, msg) + case UnbondMsg: + return handleUnbondMsg(ctx, sm, ck, msg) + default: + return sdk.ErrUnknownRequest("No match for message type.").Result() + } + } +} + +func handleBondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg BondMsg) sdk.Result { + _, err := ck.SubtractCoins(ctx, msg.Address, []sdk.Coin{msg.Stake}) + if err != nil { + return err.Result() + } + + power, err := sm.Bond(ctx, msg.Address, msg.PubKey, msg.Stake.Amount) + if err != nil { + return err.Result() + } + + valSet := abci.Validator{ + PubKey: msg.PubKey.Bytes(), + Power: power, + } + + return sdk.Result{ + Code: sdk.CodeOK, + ValidatorUpdates: abci.Validators{valSet}, + } +} + +func handleUnbondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg UnbondMsg) sdk.Result { + pubKey, power, err := sm.Unbond(ctx, msg.Address) + if err != nil { + return err.Result() + } + + stake := sdk.Coin{ + Denom: "mycoin", + Amount: power, + } + _, err = ck.AddCoins(ctx, msg.Address, sdk.Coins{stake}) + if err != nil { + return err.Result() + } + + valSet := abci.Validator{ + PubKey: pubKey.Bytes(), + Power: int64(0), + } + + return sdk.Result{ + Code: sdk.CodeOK, + ValidatorUpdates: abci.Validators{valSet}, + } +} diff --git a/x/staking/mapper.go b/x/staking/mapper.go new file mode 100644 index 000000000000..7f155d00a116 --- /dev/null +++ b/x/staking/mapper.go @@ -0,0 +1,92 @@ +package staking + +import ( + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" +) + +type StakingMapper struct { + key sdk.StoreKey + cdc *wire.Codec +} + +func NewMapper(key sdk.StoreKey) StakingMapper { + cdc := wire.NewCodec() + return StakingMapper{ + key: key, + cdc: cdc, + } +} + +func (sm StakingMapper) getBondInfo(ctx sdk.Context, addr sdk.Address) bondInfo { + store := ctx.KVStore(sm.key) + bz := store.Get(addr) + if bz == nil { + return bondInfo{} + } + var bi bondInfo + err := sm.cdc.UnmarshalBinary(bz, &bi) + if err != nil { + panic(err) + } + return bi +} + +func (sm StakingMapper) setBondInfo(ctx sdk.Context, addr sdk.Address, bi bondInfo) { + store := ctx.KVStore(sm.key) + bz, err := sm.cdc.MarshalBinary(bi) + if err != nil { + panic(err) + } + store.Set(addr, bz) +} + +func (sm StakingMapper) deleteBondInfo(ctx sdk.Context, addr sdk.Address) { + store := ctx.KVStore(sm.key) + store.Delete(addr) +} + +func (sm StakingMapper) Bond(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, power int64) (int64, sdk.Error) { + + bi := sm.getBondInfo(ctx, addr) + if bi.isEmpty() { + bi = bondInfo{ + PubKey: pubKey, + Power: power, + } + sm.setBondInfo(ctx, addr, bi) + return bi.Power, nil + } + + newPower := bi.Power + power + newBi := bondInfo{ + PubKey: bi.PubKey, + Power: newPower, + } + sm.setBondInfo(ctx, addr, newBi) + + return newBi.Power, nil +} + +func (sm StakingMapper) Unbond(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64, sdk.Error) { + bi := sm.getBondInfo(ctx, addr) + if bi.isEmpty() { + return crypto.PubKey{}, 0, ErrInvalidUnbond() + } + sm.deleteBondInfo(ctx, addr) + return bi.PubKey, bi.Power, nil +} + +type bondInfo struct { + PubKey crypto.PubKey + Power int64 +} + +func (bi bondInfo) isEmpty() bool { + if bi == (bondInfo{}) { + return true + } + return false +} diff --git a/x/staking/mapper_test.go b/x/staking/mapper_test.go new file mode 100644 index 000000000000..8f4e2d50e4b7 --- /dev/null +++ b/x/staking/mapper_test.go @@ -0,0 +1,76 @@ +package staking + +import ( + "fmt" + + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { + db := dbm.NewMemDB() + capKey := sdk.NewKVStoreKey("capkey") + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + return ms, capKey +} + +func TestStakingMapperGetSet(t *testing.T) { + ms, capKey := setupMultiStore() + + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + stakingMapper := NewMapper(capKey) + addr := sdk.Address([]byte("some-address")) + + bi := stakingMapper.getBondInfo(ctx, addr) + assert.Equal(t, bi, bondInfo{}) + + privKey := crypto.GenPrivKeyEd25519() + + bi = bondInfo{ + PubKey: privKey.PubKey(), + Power: int64(10), + } + fmt.Printf("Pubkey: %v\n", privKey.PubKey()) + stakingMapper.setBondInfo(ctx, addr, bi) + + savedBi := stakingMapper.getBondInfo(ctx, addr) + assert.NotNil(t, savedBi) + fmt.Printf("Bond Info: %v\n", savedBi) + assert.Equal(t, int64(10), savedBi.Power) +} + +func TestBonding(t *testing.T) { + ms, capKey := setupMultiStore() + + ctx := sdk.NewContext(ms, abci.Header{}, false, nil) + stakingMapper := NewMapper(capKey) + addr := sdk.Address([]byte("some-address")) + privKey := crypto.GenPrivKeyEd25519() + pubKey := privKey.PubKey() + + _, _, err := stakingMapper.Unbond(ctx, addr) + assert.Equal(t, err, ErrInvalidUnbond()) + + _, err = stakingMapper.Bond(ctx, addr, pubKey, 10) + assert.Nil(t, err) + + power, err := stakingMapper.Bond(ctx, addr, pubKey, 10) + assert.Equal(t, int64(20), power) + + pk, _, err := stakingMapper.Unbond(ctx, addr) + assert.Nil(t, err) + assert.Equal(t, pubKey, pk) + + _, _, err = stakingMapper.Unbond(ctx, addr) + assert.Equal(t, err, ErrInvalidUnbond()) +} diff --git a/x/staking/types.go b/x/staking/types.go new file mode 100644 index 000000000000..9bbf425c7c80 --- /dev/null +++ b/x/staking/types.go @@ -0,0 +1,95 @@ +package staking + +import ( + "encoding/json" + + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ------------------------- +// BondMsg + +type BondMsg struct { + Address sdk.Address `json:"address"` + Stake sdk.Coin `json:"coins"` + PubKey crypto.PubKey `json:"pub_key"` +} + +func NewBondMsg(addr sdk.Address, stake sdk.Coin, pubKey crypto.PubKey) BondMsg { + return BondMsg{ + Address: addr, + Stake: stake, + PubKey: pubKey, + } +} + +func (msg BondMsg) Type() string { + return "staking" +} + +func (msg BondMsg) ValidateBasic() sdk.Error { + if msg.Stake.IsZero() { + return ErrEmptyStake() + } + + if msg.PubKey.Empty() { + return sdk.ErrInvalidPubKey("BondMsg.PubKey must not be empty") + } + + return nil +} + +func (msg BondMsg) Get(key interface{}) interface{} { + return nil +} + +func (msg BondMsg) GetSignBytes() []byte { + bz, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return bz +} + +func (msg BondMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Address} +} + +// ------------------------- +// UnbondMsg + +type UnbondMsg struct { + Address sdk.Address `json:"address"` +} + +func NewUnbondMsg(addr sdk.Address) UnbondMsg { + return UnbondMsg{ + Address: addr, + } +} + +func (msg UnbondMsg) Type() string { + return "staking" +} + +func (msg UnbondMsg) ValidateBasic() sdk.Error { + return nil +} + +func (msg UnbondMsg) Get(key interface{}) interface{} { + return nil +} + +func (msg UnbondMsg) GetSignBytes() []byte { + bz, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return bz +} + +func (msg UnbondMsg) GetSigners() []sdk.Address { + return []sdk.Address{msg.Address} +} diff --git a/x/staking/types_test.go b/x/staking/types_test.go new file mode 100644 index 000000000000..7c9dd228c085 --- /dev/null +++ b/x/staking/types_test.go @@ -0,0 +1,31 @@ +package staking + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestBondMsgValidation(t *testing.T) { + privKey := crypto.GenPrivKeyEd25519() + cases := []struct { + valid bool + bondMsg BondMsg + }{ + {true, NewBondMsg(sdk.Address{}, sdk.Coin{"mycoin", 5}, privKey.PubKey())}, + {false, NewBondMsg(sdk.Address{}, sdk.Coin{"mycoin", 0}, privKey.PubKey())}, + } + + for i, tc := range cases { + err := tc.bondMsg.ValidateBasic() + if tc.valid { + assert.Nil(t, err, "%d: %+v", i, err) + } else { + assert.NotNil(t, err, "%d", i) + } + } +}