diff --git a/.circleci/config.yml b/.circleci/config.yml index a8e718b10..9efb455e1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 defaults: &linux_defaults working_directory: /go/src/github.com/cosmos/cosmos-sdk docker: - - image: circleci/golang:1.11.4 + - image: circleci/golang:1.11.5 environment: GOBIN: /tmp/workspace/bin @@ -17,7 +17,7 @@ macos_config: &macos_defaults xcode: "10.1.0" working_directory: /Users/distiller/project/src/github.com/cosmos/cosmos-sdk environment: - GO_VERSION: "1.11.4" + GO_VERSION: "1.11.5" set_macos_env: &macos_env run: @@ -82,6 +82,7 @@ jobs: name: Get metalinter command: | export PATH="$GOBIN:$PATH" + make devtools-clean make devtools - run: name: Lint source @@ -171,7 +172,7 @@ jobs: name: Test multi-seed Gaia simulation long command: | export PATH="$GOBIN:$PATH" - scripts/multisim.sh 800 50 TestFullGaiaSimulation + scripts/multisim.sh 500 50 TestFullGaiaSimulation test_sim_gaia_multi_seed: <<: *linux_defaults @@ -200,11 +201,10 @@ jobs: name: Run tests command: | export PATH="$GOBIN:$PATH" - make install export VERSION="$(git describe --tags --long | sed 's/v\(.*\)/\1/')" - for pkg in $(go list github.com/cosmos/cosmos-sdk/... | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test | grep -v '/simulation' | circleci tests split --split-by=timings); do - id=$(basename "$pkg") - GOCACHE=off go test -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" | tee "/tmp/logs/$id-$RANDOM.log" + for pkg in $(go list ./... | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test | grep -v '/simulation' | circleci tests split --split-by=timings); do + id=$(echo "$pkg" | sed 's|[/.]|_|g') + GOCACHE=off go test -timeout 8m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic -tags='ledger test_ledger_mock' "$pkg" | tee "/tmp/logs/$id-$RANDOM.log" done - persist_to_workspace: root: /tmp/workspace @@ -226,10 +226,20 @@ jobs: command: | set -ex + echo "--> Concatenating profiles:" + ls /tmp/workspace/profiles/ 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: filter out DONTCOVER + command: | + excludelist="$(find ./ -type f -name '*.go' | xargs grep -l 'DONTCOVER' | xargs realpath --relative-to=$GOPATH/src)" + for filename in ${excludelist}; do + echo "Excluding ${filename} ..." + sed -i "\%${filename}:%d" coverage.txt + done - run: name: upload command: bash <(curl -s https://codecov.io/bash) -f coverage.txt @@ -243,7 +253,7 @@ jobs: GOPATH: /home/circleci/.go_workspace/ GOOS: linux GOARCH: amd64 - GO_VERSION: "1.11.4" + GO_VERSION: "1.11.5" parallelism: 1 steps: - checkout @@ -380,9 +390,7 @@ workflows: - test_cover: requires: - setup_dependencies - - localnet: - requires: - - setup_dependencies + - localnet - upload_coverage: requires: - test_cover diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dcdf7080b..11f9e598b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,4 +4,6 @@ * @ebuchman @rigelrozanski @cwgoes # Precious documentation -/docs/ @zramsay @jolesbi +/docs/README.md @zramsay +/docs/DOCS_README.md @zramsay +/docs/.vuepress/ @zramsay diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2867593..a88cf406d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,127 @@ # Changelog +## 0.31.0 + +BREAKING CHANGES + +* Gaia REST API (`gaiacli advanced rest-server`) + * [\#3284](https://github.com/cosmos/cosmos-sdk/issues/3284) Rename the `name` + field to `from` in the `base_req` body. + * [\#3485](https://github.com/cosmos/cosmos-sdk/pull/3485) Error responses are now JSON objects. + * [\#3477][distribution] endpoint changed "all_delegation_rewards" -> "delegator_total_rewards" + +* Gaia CLI (`gaiacli`) + - [#3399](https://github.com/cosmos/cosmos-sdk/pull/3399) Add `gaiad validate-genesis` command to facilitate checking of genesis files + - [\#1894](https://github.com/cosmos/cosmos-sdk/issues/1894) `version` prints out short info by default. Add `--long` flag. Proper handling of `--format` flag introduced. + - [\#3465](https://github.com/cosmos/cosmos-sdk/issues/3465) `gaiacli rest-server` switched back to insecure mode by default: + - `--insecure` flag is removed. + - `--tls` is now used to enable secure layer. + - [\#3451](https://github.com/cosmos/cosmos-sdk/pull/3451) `gaiacli` now returns transactions in plain text including tags. + - [\#3497](https://github.com/cosmos/cosmos-sdk/issues/3497) `gaiad init` now takes moniker as required arguments, not as parameter. + * [\#3501](https://github.com/cosmos/cosmos-sdk/issues/3501) Change validator + address Bech32 encoding to consensus address in `tendermint-validator-set`. + +* Gaia + * [\#3457](https://github.com/cosmos/cosmos-sdk/issues/3457) Changed governance tally validatorGovInfo to use sdk.Int power instead of sdk.Dec + * [\#3495](https://github.com/cosmos/cosmos-sdk/issues/3495) Added Validator Minimum Self Delegation + * Reintroduce OR semantics for tx fees + +* SDK + * [\#2513](https://github.com/cosmos/cosmos-sdk/issues/2513) Tendermint updates are adjusted by 10^-6 relative to staking tokens, + * [\#3487](https://github.com/cosmos/cosmos-sdk/pull/3487) Move HTTP/REST utilities out of client/utils into a new dedicated client/rest package. + * [\#3490](https://github.com/cosmos/cosmos-sdk/issues/3490) ReadRESTReq() returns bool to avoid callers to write error responses twice. + * [\#3502](https://github.com/cosmos/cosmos-sdk/pull/3502) Fixes issue when comparing genesis states + * [\#3514](https://github.com/cosmos/cosmos-sdk/pull/3514) Various clean ups: + - Replace all GetKeyBase\* functions family in favor of NewKeyBaseFromDir and NewKeyBaseFromHomeFlag. + - Remove Get prefix from all TxBuilder's getters. + * [\#3522](https://github.com/cosmos/cosmos-sdk/pull/3522) Get rid of double negatives: Coins.IsNotNegative() -> Coins.IsAnyNegative(). + * [\#3561](https://github.com/cosmos/cosmos-sdk/issues/3561) Don't unnecessarily store denominations in staking + + +FEATURES + +* Gaia REST API + * [\#2358](https://github.com/cosmos/cosmos-sdk/issues/2358) Add distribution module REST interface + +* Gaia CLI (`gaiacli`) + * [\#3429](https://github.com/cosmos/cosmos-sdk/issues/3429) Support querying + for all delegator distribution rewards. + * [\#3449](https://github.com/cosmos/cosmos-sdk/issues/3449) Proof verification now works with absence proofs + * [\#3484](https://github.com/cosmos/cosmos-sdk/issues/3484) Add support + vesting accounts to the add-genesis-account command. + +* Gaia + - [\#3397](https://github.com/cosmos/cosmos-sdk/pull/3397) Implement genesis file sanitization to avoid failures at chain init. + * [\#3428](https://github.com/cosmos/cosmos-sdk/issues/3428) Run the simulation from a particular genesis state loaded from a file + +* SDK + * [\#3270](https://github.com/cosmos/cosmos-sdk/issues/3270) [x/staking] limit number of ongoing unbonding delegations /redelegations per pair/trio + * [\#3477][distribution] new query endpoint "delegator_validators" + * [\#3514](https://github.com/cosmos/cosmos-sdk/pull/3514) Provided a lazy loading implementation of Keybase that locks the underlying + storage only for the time needed to perform the required operation. Also added Keybase reference to TxBuilder struct. + * [types] [\#2580](https://github.com/cosmos/cosmos-sdk/issues/2580) Addresses now Bech32 empty addresses to an empty string + + +IMPROVEMENTS + +* Gaia REST API + * [\#3284](https://github.com/cosmos/cosmos-sdk/issues/3284) Update Gaia Lite + REST service to support the following: + * Automatic account number and sequence population when fields are omitted + * Generate only functionality no longer requires access to a local Keybase + * `from` field in the `base_req` body can be a Keybase name or account address + * [\#3423](https://github.com/cosmos/cosmos-sdk/issues/3423) Allow simulation + (auto gas) to work with generate only. + * [\#3514](https://github.com/cosmos/cosmos-sdk/pull/3514) REST server calls to keybase does not lock the underlying storage anymore. + * [\#3523](https://github.com/cosmos/cosmos-sdk/pull/3523) Added `/tx/encode` endpoint to serialize a JSON tx to base64-encoded Amino. + +* Gaia CLI (`gaiacli`) + * [\#3476](https://github.com/cosmos/cosmos-sdk/issues/3476) New `withdraw-all-rewards` command to withdraw all delegations rewards for delegators. + * [\#3497](https://github.com/cosmos/cosmos-sdk/issues/3497) `gaiad gentx` supports `--ip` and `--node-id` flags to override defaults. + * [\#3518](https://github.com/cosmos/cosmos-sdk/issues/3518) Fix flow in + `keys add` to show the mnemonic by default. + * [\#3517](https://github.com/cosmos/cosmos-sdk/pull/3517) Increased test coverage + * [\#3523](https://github.com/cosmos/cosmos-sdk/pull/3523) Added `tx encode` command to serialize a JSON tx to base64-encoded Amino. + +* Gaia + * [\#3418](https://github.com/cosmos/cosmos-sdk/issues/3418) Add vesting account + genesis validation checks to `GaiaValidateGenesisState`. + * [\#3420](https://github.com/cosmos/cosmos-sdk/issues/3420) Added maximum length to governance proposal descriptions and titles + * [\#3256](https://github.com/cosmos/cosmos-sdk/issues/3256) Add gas consumption + for tx size in the ante handler. + * [\#3454](https://github.com/cosmos/cosmos-sdk/pull/3454) Add `--jail-whitelist` to `gaiad export` to enable testing of complex exports + * [\#3424](https://github.com/cosmos/cosmos-sdk/issues/3424) Allow generation of gentxs with empty memo field. + * [\#3507](https://github.com/cosmos/cosmos-sdk/issues/3507) General cleanup, removal of unnecessary struct fields, undelegation bugfix, and comment clarification in x/staking and x/slashing + +* SDK + * [\#2605] x/params add subkey accessing + * [\#2986](https://github.com/cosmos/cosmos-sdk/pull/2986) Store Refactor + * [\#3435](https://github.com/cosmos/cosmos-sdk/issues/3435) Test that store implementations do not allow nil values + * [\#2509](https://github.com/cosmos/cosmos-sdk/issues/2509) Sanitize all usage of Dec.RoundInt64() + * [\#556](https://github.com/cosmos/cosmos-sdk/issues/556) Increase `BaseApp` + test coverage. + * [\#3357](https://github.com/cosmos/cosmos-sdk/issues/3357) develop state-transitions.md for staking spec, missing states added to `state.md` + * [\#3552](https://github.com/cosmos/cosmos-sdk/pull/3552) Validate bit length when + deserializing `Int` types. + + +BUG FIXES + +* Gaia CLI (`gaiacli`) + - [\#3417](https://github.com/cosmos/cosmos-sdk/pull/3417) Fix `q slashing signing-info` panic by ensuring safety of user input and properly returning not found error + - [\#3345](https://github.com/cosmos/cosmos-sdk/issues/3345) Upgrade ledger-cosmos-go dependency to v0.9.3 to pull + https://github.com/ZondaX/ledger-cosmos-go/commit/ed9aa39ce8df31bad1448c72d3d226bf2cb1a8d1 in order to fix a derivation path issue that causes `gaiacli keys add --recover` + to malfunction. + - [\#3419](https://github.com/cosmos/cosmos-sdk/pull/3419) Fix `q distr slashes` panic + - [\#3453](https://github.com/cosmos/cosmos-sdk/pull/3453) The `rest-server` command didn't respect persistent flags such as `--chain-id` and `--trust-node` if they were + passed on the command line. + - [\#3441](https://github.com/cosmos/cosmos-sdk/pull/3431) Improved resource management and connection handling (ledger devices). Fixes issue with DER vs BER signatures. + +* Gaia + * [\#3486](https://github.com/cosmos/cosmos-sdk/pull/3486) Use AmountOf in + vesting accounts instead of zipping/aligning denominations. + + ## 0.30.0 BREAKING CHANGES diff --git a/Gopkg.lock b/Gopkg.lock index fa9eec6a6..adfaff719 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,22 +1,6 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. -[[projects]] - digest = "1:7736fc6da04620727f8f3aa2ced8d77be8e074a302820937aa5993848c769b27" - name = "github.com/ZondaX/hid-go" - packages = ["."] - pruneopts = "UT" - revision = "48b08affede2cea076a3cf13b2e3f72ed262b743" - version = "v0.4.0" - -[[projects]] - digest = "1:1ba351898f7efc68c7c9ff3145b920e478f716b077fdaaf06b967c5d883fa988" - name = "github.com/ZondaX/ledger-go" - packages = ["."] - pruneopts = "UT" - revision = "c3225ab10c2f53397d4aa419a588466493572b22" - version = "v0.4.0" - [[projects]] branch = "master" digest = "1:09a7f74eb6bb3c0f14d8926610c87f569c5cff68e978d30e9a3540aeb626fdf0" @@ -42,12 +26,11 @@ version = "v0.1.0" [[projects]] - branch = "master" digest = "1:093bf93a65962e8191e3e8cd8fc6c363f83d43caca9739c906531ba7210a9904" name = "github.com/btcsuite/btcd" packages = ["btcec"] pruneopts = "UT" - revision = "7d2daa5bfef28c5e282571bc06416516936115ee" + revision = "ed77733ec07dfc8a513741138419b8d9d3de9d2d" [[projects]] digest = "1:386de157f7d19259a7f9c81f26ce011223ce0f090353c1152ffdf730d7d10ac2" @@ -71,6 +54,14 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + digest = "1:fed20bf7f0da387c96d4cfc140a95572e5aba4bb984beb7de910e090ae39849b" + name = "github.com/ethereum/go-ethereum" + packages = ["crypto/secp256k1"] + pruneopts = "UT" + revision = "7fa3509e2eaf1a4ebc12344590e5699406690f15" + version = "v1.8.22" + [[projects]] digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" name = "github.com/fsnotify/fsnotify" @@ -149,20 +140,12 @@ revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" [[projects]] - digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" - name = "github.com/gorilla/context" - packages = ["."] - pruneopts = "UT" - revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" - version = "v1.1.1" - -[[projects]] - digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" + digest = "1:ca59b1175189b3f0e9f1793d2c350114be36eaabbe5b9f554b35edee1de50aea" name = "github.com/gorilla/mux" packages = ["."] pruneopts = "UT" - revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" - version = "v1.6.2" + revision = "a7962380ca08b5a188038c69871b8d3fbdf31e89" + version = "v1.7.0" [[projects]] digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" @@ -296,11 +279,10 @@ name = "github.com/prometheus/client_model" packages = ["go"] pruneopts = "UT" - revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" + revision = "fd36f4220a901265f90734c3183c5f0c91daa0b8" [[projects]] - branch = "master" - digest = "1:db712fde5d12d6cdbdf14b777f0c230f4ff5ab0be8e35b239fc319953ed577a4" + digest = "1:35cf6bdf68db765988baa9c4f10cc5d7dda1126a54bd62e252dbcd0b1fc8da90" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -308,11 +290,12 @@ "model", ] pruneopts = "UT" - revision = "4724e9255275ce38f7179b2478abeae4e28c904f" + revision = "cfeb6f9992ffa54aaa4f2170ade4067ee478b250" + version = "v0.2.0" [[projects]] branch = "master" - digest = "1:d39e7c7677b161c2dd4c635a2ac196460608c7d8ba5337cc8cae5825a2681f8f" + digest = "1:c65f369bae3dff3a0382e38f3fe4f62cdfecba59cb6429ee323b75afdd4f3ba3" name = "github.com/prometheus/procfs" packages = [ ".", @@ -321,7 +304,7 @@ "xfs", ] pruneopts = "UT" - revision = "1dc9a6cbc91aacc3e8b2d63db4d2e957a5394ac4" + revision = "de1b801bf34b80cd00f14087dc5a994bfe0296bc" [[projects]] digest = "1:ea0700160aca4ef099f4e06686a665a87691f4248dddd40796925eda2e46bd64" @@ -347,15 +330,15 @@ version = "v1.6.0" [[projects]] - digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd" + digest = "1:3e39bafd6c2f4bf3c76c3bfd16a2e09e016510ad5db90dc02b88e2f565d6d595" name = "github.com/spf13/afero" packages = [ ".", "mem", ] pruneopts = "UT" - revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd" - version = "v1.1.2" + revision = "f4711e4db9e9a1d3887343acb72b2bbfc2f686f5" + version = "v1.2.1" [[projects]] digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc" @@ -453,7 +436,7 @@ version = "v0.12.0" [[projects]] - digest = "1:22a0fe58c626dd09549eb9451688fab5a2c8bef04d478c907f747d6151d431fd" + digest = "1:89f6fe8d02b427996828fbf43720ed1297a2e92c930b98dd302767b5ad796579" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -519,15 +502,31 @@ "version", ] pruneopts = "UT" - revision = "v0.29.0" + revision = "v0.30.0-rc0" [[projects]] - digest = "1:a7485b2a69f996923f9d3406a9a853fd8eb31818515e985a830d71f88f6a925b" + digest = "1:b73f5e117bc7c6e8fc47128f20db48a873324ad5cfeeebfc505e85c58682b5e4" + name = "github.com/zondax/hid" + packages = ["."] + pruneopts = "T" + revision = "302fd402163c34626286195dfa9adac758334acc" + version = "v0.9.0" + +[[projects]] + digest = "1:fca24169988a61ea725d1326de30910d8049fe68bcbc194d28803f9a76dda380" name = "github.com/zondax/ledger-cosmos-go" packages = ["."] pruneopts = "UT" - revision = "d4aed6d929a703bb555a2d79fe9c470afe61f648" - version = "v0.9.2" + revision = "69fdb8ce5e5b9d9c3b22b9248e117b231d4f06dd" + version = "v0.9.7" + +[[projects]] + digest = "1:f8e4c0b959174a1fa5946b12f1f2ac7ea5651bef20a9e4a8dac55dbffcaa6cd6" + name = "github.com/zondax/ledger-go" + packages = ["."] + pruneopts = "UT" + revision = "69c15f1333a9b6866e5f66096561c7d138894bc5" + version = "v0.8.0" [[projects]] digest = "1:6f6dc6060c4e9ba73cf28aa88f12a69a030d3d19d518ef8e931879eaa099628d" @@ -612,7 +611,7 @@ revision = "383e8b2c3b9e36c4076b235b32537292176bae20" [[projects]] - digest = "1:9edd250a3c46675d0679d87540b30c9ed253b19bd1fd1af08f4f5fb3c79fc487" + digest = "1:9ab5a33d8cb5c120602a34d2e985ce17956a4e8c2edce7e6961568f95e40c09a" name = "google.golang.org/grpc" packages = [ ".", @@ -648,8 +647,8 @@ "tap", ] pruneopts = "UT" - revision = "df014850f6dee74ba2fc94874043a9f3f75fbfd8" - version = "v1.17.0" + revision = "a02b0774206b209466313a0b525d2c738fe407eb" + version = "v1.18.0" [[projects]] digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" @@ -680,7 +679,7 @@ "github.com/spf13/viper", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", - "github.com/syndtr/goleveldb/leveldb/opt", + "github.com/tendermint/btcd/btcec", "github.com/tendermint/go-amino", "github.com/tendermint/iavl", "github.com/tendermint/tendermint/abci/server", diff --git a/Gopkg.toml b/Gopkg.toml index 54a64d419..b59da1a61 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -40,14 +40,18 @@ [[override]] name = "github.com/tendermint/tendermint" - revision = "v0.29.0" + revision = "v0.30.0-rc0" [[constraint]] name = "github.com/zondax/ledger-cosmos-go" - version = "=v0.9.2" + version = "=v0.9.7" ## deps without releases: +[[constraint]] + name = "github.com/btcsuite/btcd" + revision = "ed77733ec07dfc8a513741138419b8d9d3de9d2d" + [[override]] name = "golang.org/x/crypto" source = "https://github.com/tendermint/crypto" @@ -84,3 +88,6 @@ [prune] go-tests = true unused-packages = true + [[prune.project]] + name = "github.com/zondax/hid" + unused-packages = false diff --git a/Makefile b/Makefile index 2f8086fd7..de913e82c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PACKAGES_NOSIMULATION=$(shell go list ./... | grep -v '/simulation') PACKAGES_SIMTEST=$(shell go list ./... | grep '/simulation') -VERSION := $(subst v,,$(shell git describe --tags --long)) +VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') COMMIT := $(shell git log -1 --format='%H') BUILD_TAGS = netgo CAT := $(if $(filter $(OS),Windows_NT),type,cat) @@ -15,7 +15,7 @@ GOTOOLS = \ github.com/rakyll/statik GOBIN ?= $(GOPATH)/bin -all: devtools get_vendor_deps install test_lint test +all: devtools vendor-deps install test_lint test # The below include contains the tools target. include scripts/Makefile @@ -99,6 +99,7 @@ update_dev_tools: go get -u github.com/tendermint/lint/golint devtools: devtools-stamp +devtools-clean: tools-clean devtools-stamp: tools @echo "--> Downloading linters (this may take awhile)" $(GOPATH)/src/github.com/alecthomas/gometalinter/scripts/install.sh -b $(GOBIN) @@ -122,7 +123,10 @@ draw_deps: tools @goviz -i github.com/cosmos/cosmos-sdk/cmd/gaia/cmd/gaiad -d 2 | dot -Tpng -o dependency-graph.png clean: - rm -f devtools-stamp vendor-deps + rm -f devtools-stamp vendor-deps snapcraft-local.yaml + +distclean: clean + rm -rf vendor/ ######################################## ### Documentation @@ -140,8 +144,14 @@ test: test_unit test_cli: @go test -p 4 `go list github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test` -tags=cli_test +test_ledger: + # First test with mock + @go test `go list github.com/cosmos/cosmos-sdk/crypto` -tags='cgo ledger test_ledger_mock' + # Now test with a real device + @go test -v `go list github.com/cosmos/cosmos-sdk/crypto` -tags='cgo ledger' + test_unit: - @VERSION=$(VERSION) go test $(PACKAGES_NOSIMULATION) + @VERSION=$(VERSION) go test $(PACKAGES_NOSIMULATION) -tags='test_ledger_mock' test_race: @VERSION=$(VERSION) go test -race $(PACKAGES_NOSIMULATION) @@ -150,9 +160,15 @@ test_sim_gaia_nondeterminism: @echo "Running nondeterminism test..." @go test ./cmd/gaia/app -run TestAppStateDeterminism -SimulationEnabled=true -v -timeout 10m +test_sim_gaia_custom_genesis_fast: + @echo "Running custom genesis simulation..." + @echo "By default, ${HOME}/.gaiad/config/genesis.json will be used." + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationGenesis=${HOME}/.gaiad/config/genesis.json \ + -SimulationEnabled=true -SimulationNumBlocks=100 -SimulationBlockSize=200 -SimulationCommit=true -SimulationSeed=99 -SimulationPeriod=5 -v -timeout 24h + test_sim_gaia_fast: @echo "Running quick Gaia simulation. This may take several minutes..." - @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=1000 -SimulationBlockSize=200 -SimulationCommit=true -SimulationSeed=99 -v -timeout 24h + @go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationNumBlocks=100 -SimulationBlockSize=200 -SimulationCommit=true -SimulationSeed=99 -SimulationPeriod=5 -v -timeout 24h test_sim_gaia_import_export: @echo "Running Gaia import/export simulation. This may take several minutes..." @@ -162,6 +178,11 @@ test_sim_gaia_simulation_after_import: @echo "Running Gaia simulation-after-import. This may take several minutes..." @bash scripts/multisim.sh 50 5 TestGaiaSimulationAfterImport +test_sim_gaia_custom_genesis_multi_seed: + @echo "Running multi-seed custom genesis simulation..." + @echo "By default, ${HOME}/.gaiad/config/genesis.json will be used." + @bash scripts/multisim.sh 400 5 TestFullGaiaSimulation ${HOME}/.gaiad/config/genesis.json + test_sim_gaia_multi_seed: @echo "Running multi-seed Gaia simulation. This may take awhile!" @bash scripts/multisim.sh 400 5 TestFullGaiaSimulation @@ -171,14 +192,16 @@ SIM_BLOCK_SIZE ?= 200 SIM_COMMIT ?= true test_sim_gaia_benchmark: @echo "Running Gaia benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" - @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h + @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ \ + -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h test_sim_gaia_profile: @echo "Running Gaia benchmark for numBlocks=$(SIM_NUM_BLOCKS), blockSize=$(SIM_BLOCK_SIZE). This may take awhile!" - @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out + @go test -benchmem -run=^$$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$$ \ + -SimulationEnabled=true -SimulationNumBlocks=$(SIM_NUM_BLOCKS) -SimulationBlockSize=$(SIM_BLOCK_SIZE) -SimulationCommit=$(SIM_COMMIT) -timeout 24h -cpuprofile cpu.out -memprofile mem.out test_cover: - @export VERSION=$(VERSION); bash tests/test_cover.sh + @export VERSION=$(VERSION); bash -x tests/test_cover.sh test_lint: gometalinter --config=tools/gometalinter.json ./... @@ -235,12 +258,21 @@ localnet-start: localnet-stop localnet-stop: docker-compose down + +######################################## +### Packaging + +snapcraft-local.yaml: snapcraft-local.yaml.in + sed "s/@VERSION@/${VERSION}/g" < $< > $@ + # 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: build install install_debug dist \ +.PHONY: build install install_debug dist clean distclean \ check_tools check_dev_tools get_vendor_deps draw_deps test test_cli test_unit \ test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update \ build-linux build-docker-gaiadnode localnet-start localnet-stop \ format check-ledger test_sim_gaia_nondeterminism test_sim_modules test_sim_gaia_fast \ -test_sim_gaia_multi_seed test_sim_gaia_import_export update_tools update_dev_tools +test_sim_gaia_custom_genesis_fast test_sim_gaia_custom_genesis_multi_seed \ +test_sim_gaia_multi_seed test_sim_gaia_import_export update_tools update_dev_tools \ +devtools-clean diff --git a/PENDING.md b/PENDING.md index 4d87301f0..164614789 100644 --- a/PENDING.md +++ b/PENDING.md @@ -2,9 +2,9 @@ BREAKING CHANGES -* Gaia REST API (`gaiacli advanced rest-server`) +* Gaia REST API -* Gaia CLI (`gaiacli`) +* Gaia CLI * Gaia @@ -12,12 +12,11 @@ BREAKING CHANGES * Tendermint - FEATURES -* Gaia REST API (`gaiacli advanced rest-server`) +* Gaia REST API -* Gaia CLI (`gaiacli`) +* Gaia CLI * Gaia @@ -28,9 +27,9 @@ FEATURES IMPROVEMENTS -* Gaia REST API (`gaiacli advanced rest-server`) +* Gaia REST API -* Gaia CLI (`gaiacli`) +* Gaia CLI * Gaia @@ -41,12 +40,12 @@ IMPROVEMENTS BUG FIXES -* Gaia REST API (`gaiacli advanced rest-server`) +* Gaia REST API -* Gaia CLI (`gaiacli`) +* Gaia CLI * Gaia * SDK -* Tendermint +* Tendermint \ No newline at end of file diff --git a/README.md b/README.md index 2ab5cf4e2..d23a1edc8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![version](https://img.shields.io/github/tag/cosmos/cosmos-sdk.svg)](https://github.com/cosmos/cosmos-sdk/releases/latest) [![CircleCI](https://circleci.com/gh/cosmos/cosmos-sdk/tree/master.svg?style=shield)](https://circleci.com/gh/cosmos/cosmos-sdk/tree/master) +[![Snap Status](https://build.snapcraft.io/badge/cosmos/cosmos-sdk.svg)](https://build.snapcraft.io/user/cosmos/cosmos-sdk) [![codecov](https://codecov.io/gh/cosmos/cosmos-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/cosmos/cosmos-sdk) [![Go Report Card](https://goreportcard.com/badge/github.com/cosmos/cosmos-sdk)](https://goreportcard.com/report/github.com/cosmos/cosmos-sdk) [![license](https://img.shields.io/github/license/cosmos/cosmos-sdk.svg)](https://github.com/cosmos/cosmos-sdk/blob/master/LICENSE) @@ -17,7 +18,7 @@ It is being used to build `Gaia`, the first implementation of the Cosmos Hub. **WARNING**: The SDK has mostly stabilized, but we are still making some breaking changes. -**Note**: Requires [Go 1.11.4+](https://golang.org/dl/) +**Note**: Requires [Go 1.11.5+](https://golang.org/dl/) ## Cosmos Hub Public Testnet diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index cfeb97214..c7f7cda86 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -3,11 +3,13 @@ package baseapp import ( "fmt" "io" + "reflect" "runtime/debug" "strings" + "errors" + "github.com/gogo/protobuf/proto" - "github.com/pkg/errors" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/tmhash" @@ -41,7 +43,7 @@ const ( // BaseApp reflects the ABCI application implementation. type BaseApp struct { // initialized on creation - Logger log.Logger + logger log.Logger name string // application name from abci.Info db dbm.DB // common DB backend cms sdk.CommitMultiStore // Main (uncached) state @@ -50,19 +52,18 @@ type BaseApp struct { txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx // set upon LoadVersion or LoadLatestVersion. - mainKey *sdk.KVStoreKey // Main KVStore in cms + baseKey *sdk.KVStoreKey // Main KVStore in cms - // may be nil - anteHandler sdk.AnteHandler // ante handler for fee and auth - initChainer sdk.InitChainer // initialize state with validators and state blob - beginBlocker sdk.BeginBlocker // logic to run before any txs - endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes - addrPeerFilter sdk.PeerFilter // filter peers by address and port - pubkeyPeerFilter sdk.PeerFilter // filter peers by public key - fauxMerkleMode bool // if true, IAVL MountStores uses MountStoresDB for simulation speed. + anteHandler sdk.AnteHandler // ante handler for fee and auth + initChainer sdk.InitChainer // initialize state with validators and state blob + beginBlocker sdk.BeginBlocker // logic to run before any txs + endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes + addrPeerFilter sdk.PeerFilter // filter peers by address and port + idPeerFilter sdk.PeerFilter // filter peers by node ID + fauxMerkleMode bool // if true, IAVL MountStores uses MountStoresDB for simulation speed. - //-------------------- - // Volatile + // -------------------- + // Volatile state // checkState is set on initialization and reset on Commit. // deliverState is set in InitChain and BeginBlock and cleared on Commit. // See methods setCheckState and setDeliverState. @@ -71,27 +72,30 @@ type BaseApp struct { voteInfos []abci.VoteInfo // absent validators from begin block // consensus params - // TODO move this in the future to baseapp param store on main store. + // TODO: Move this in the future to baseapp param store on main store. consensusParams *abci.ConsensusParams // The minimum gas prices a validator is willing to accept for processing a // transaction. This is mainly used for DoS and spam prevention. minGasPrices sdk.DecCoins - // flag for sealing + // flag for sealing options and parameters to a BaseApp sealed bool } var _ abci.Application = (*BaseApp)(nil) -// NewBaseApp returns a reference to an initialized BaseApp. +// NewBaseApp returns a reference to an initialized BaseApp. It accepts a +// variadic number of option functions, which act on the BaseApp to set +// configuration choices. // // NOTE: The db is used to store the version number for now. -// Accepts a user-defined txDecoder -// Accepts variable number of option functions, which act on the BaseApp to set configuration choices -func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp)) *BaseApp { +func NewBaseApp( + name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp), +) *BaseApp { + app := &BaseApp{ - Logger: logger, + logger: logger, name: name, db: db, cms: store.NewCommitMultiStore(db), @@ -103,112 +107,120 @@ func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecod for _, option := range options { option(app) } + return app } -// BaseApp Name +// Name returns the name of the BaseApp. func (app *BaseApp) Name() string { return app.name } +// Logger returns the logger of the BaseApp. +func (app *BaseApp) Logger() log.Logger { + return app.logger +} + // SetCommitMultiStoreTracer sets the store tracer on the BaseApp's underlying // CommitMultiStore. func (app *BaseApp) SetCommitMultiStoreTracer(w io.Writer) { - app.cms.WithTracer(w) + app.cms.SetTracer(w) } -// Mount IAVL or DB stores to the provided keys in the BaseApp multistore -func (app *BaseApp) MountStores(keys ...*sdk.KVStoreKey) { +// MountStores mounts all IAVL or DB stores to the provided keys in the BaseApp +// multistore. +func (app *BaseApp) MountStores(keys ...sdk.StoreKey) { for _, key := range keys { - if !app.fauxMerkleMode { - app.MountStore(key, sdk.StoreTypeIAVL) - } else { - // StoreTypeDB doesn't do anything upon commit, and it doesn't - // retain history, but it's useful for faster simulation. - app.MountStore(key, sdk.StoreTypeDB) + switch key.(type) { + case *sdk.KVStoreKey: + if !app.fauxMerkleMode { + app.MountStore(key, sdk.StoreTypeIAVL) + } else { + // StoreTypeDB doesn't do anything upon commit, and it doesn't + // retain history, but it's useful for faster simulation. + app.MountStore(key, sdk.StoreTypeDB) + } + case *sdk.TransientStoreKey: + app.MountStore(key, sdk.StoreTypeTransient) + default: + panic("Unrecognized store key type " + reflect.TypeOf(key).Name()) } } } -// Mount stores to the provided keys in the BaseApp multistore -func (app *BaseApp) MountStoresTransient(keys ...*sdk.TransientStoreKey) { - for _, key := range keys { - app.MountStore(key, sdk.StoreTypeTransient) - } -} - -// Mount a store to the provided key in the BaseApp multistore, using a specified DB +// MountStoreWithDB mounts a store to the provided key in the BaseApp +// multistore, using a specified DB. func (app *BaseApp) MountStoreWithDB(key sdk.StoreKey, typ sdk.StoreType, db dbm.DB) { app.cms.MountStoreWithDB(key, typ, db) } -// Mount a store to the provided key in the BaseApp multistore, using the default DB +// MountStore mounts a store to the provided key in the BaseApp multistore, +// using the default DB. func (app *BaseApp) MountStore(key sdk.StoreKey, typ sdk.StoreType) { app.cms.MountStoreWithDB(key, typ, nil) } -// load latest application version -// panics if called more than once on a running baseapp -func (app *BaseApp) LoadLatestVersion(mainKey *sdk.KVStoreKey) error { +// LoadLatestVersion loads the latest application version. It will panic if +// called more than once on a running BaseApp. +func (app *BaseApp) LoadLatestVersion(baseKey *sdk.KVStoreKey) error { err := app.cms.LoadLatestVersion() if err != nil { return err } - return app.initFromMainStore(mainKey) + return app.initFromMainStore(baseKey) } -// load application version -// panics if called more than once on a running baseapp -func (app *BaseApp) LoadVersion(version int64, mainKey *sdk.KVStoreKey) error { +// LoadVersion loads the BaseApp application version. It will panic if called +// more than once on a running baseapp. +func (app *BaseApp) LoadVersion(version int64, baseKey *sdk.KVStoreKey) error { err := app.cms.LoadVersion(version) if err != nil { return err } - return app.initFromMainStore(mainKey) + return app.initFromMainStore(baseKey) } -// the last CommitID of the multistore +// LastCommitID returns the last CommitID of the multistore. func (app *BaseApp) LastCommitID() sdk.CommitID { return app.cms.LastCommitID() } -// the last committed block height +// LastBlockHeight returns the last committed block height. func (app *BaseApp) LastBlockHeight() int64 { return app.cms.LastCommitID().Version } // initializes the remaining logic from app.cms -func (app *BaseApp) initFromMainStore(mainKey *sdk.KVStoreKey) error { - - // main store should exist. - mainStore := app.cms.GetKVStore(mainKey) +func (app *BaseApp) initFromMainStore(baseKey *sdk.KVStoreKey) error { + mainStore := app.cms.GetKVStore(baseKey) if mainStore == nil { return errors.New("baseapp expects MultiStore with 'main' KVStore") } - // memoize mainKey - if app.mainKey != nil { - panic("app.mainKey expected to be nil; duplicate init?") + // memoize baseKey + if app.baseKey != nil { + panic("app.baseKey expected to be nil; duplicate init?") } - app.mainKey = mainKey + app.baseKey = baseKey - // load consensus params from the main store + // Load the consensus params from the main store. If the consensus params are + // nil, it will be saved later during InitChain. + // + // TODO: assert that InitChain hasn't yet been called. consensusParamsBz := mainStore.Get(mainConsensusParamsKey) if consensusParamsBz != nil { var consensusParams = &abci.ConsensusParams{} + err := proto.Unmarshal(consensusParamsBz, consensusParams) if err != nil { panic(err) } + app.setConsensusParams(consensusParams) - } else { - // It will get saved later during InitChain. - // TODO assert that InitChain hasn't yet been called. } - // Needed for `gaiad export`, which inits from store but never calls initchain + // needed for `gaiad export`, which inits from store but never calls initchain app.setCheckState(abci.Header{}) - app.Seal() return nil @@ -218,42 +230,45 @@ func (app *BaseApp) setMinGasPrices(gasPrices sdk.DecCoins) { app.minGasPrices = gasPrices } -// NewContext returns a new Context with the correct store, the given header, and nil txBytes. -func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { - if isCheckTx { - return sdk.NewContext(app.checkState.ms, header, true, app.Logger). - WithMinGasPrices(app.minGasPrices) +// Router returns the router of the BaseApp. +func (app *BaseApp) Router() Router { + if app.sealed { + // We cannot return a router when the app is sealed because we can't have + // any routes modified which would cause unexpected routing behavior. + panic("Router() on sealed BaseApp") } - - return sdk.NewContext(app.deliverState.ms, header, false, app.Logger) + return app.router } -type state struct { - ms sdk.CacheMultiStore - ctx sdk.Context -} +// QueryRouter returns the QueryRouter of a BaseApp. +func (app *BaseApp) QueryRouter() QueryRouter { return app.queryRouter } -func (st *state) CacheMultiStore() sdk.CacheMultiStore { - return st.ms.CacheMultiStore() -} +// Seal seals a BaseApp. It prohibits any further modifications to a BaseApp. +func (app *BaseApp) Seal() { app.sealed = true } -func (st *state) Context() sdk.Context { - return st.ctx -} +// IsSealed returns true if the BaseApp is sealed and false otherwise. +func (app *BaseApp) IsSealed() bool { return app.sealed } +// setCheckState sets checkState with the cached multistore and +// the context wrapping it. +// It is called by InitChain() and Commit() func (app *BaseApp) setCheckState(header abci.Header) { ms := app.cms.CacheMultiStore() app.checkState = &state{ ms: ms, - ctx: sdk.NewContext(ms, header, true, app.Logger).WithMinGasPrices(app.minGasPrices), + ctx: sdk.NewContext(ms, header, true, app.logger).WithMinGasPrices(app.minGasPrices), } } +// setCheckState sets checkState with the cached multistore and +// the context wrapping it. +// It is called by InitChain() and BeginBlock(), +// and deliverState is set nil on Commit(). func (app *BaseApp) setDeliverState(header abci.Header) { ms := app.cms.CacheMultiStore() app.deliverState = &state{ ms: ms, - ctx: sdk.NewContext(ms, header, false, app.Logger), + ctx: sdk.NewContext(ms, header, false, app.logger), } } @@ -268,7 +283,7 @@ func (app *BaseApp) storeConsensusParams(consensusParams *abci.ConsensusParams) if err != nil { panic(err) } - mainStore := app.cms.GetKVStore(app.mainKey) + mainStore := app.cms.GetKVStore(app.baseKey) mainStore.Set(mainConsensusParamsKey, consensusParamsBz) } @@ -280,11 +295,10 @@ func (app *BaseApp) getMaximumBlockGas() (maxGas uint64) { return uint64(app.consensusParams.BlockSize.MaxGas) } -//______________________________________________________________________________ - +// ---------------------------------------------------------------------------- // ABCI -// Implements ABCI +// Info implements the ABCI interface. func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo { lastCommitID := app.cms.LastCommitID() @@ -295,23 +309,23 @@ func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo { } } -// Implements ABCI +// SetOption implements the ABCI interface. func (app *BaseApp) SetOption(req abci.RequestSetOption) (res abci.ResponseSetOption) { - // TODO: Implement + // TODO: Implement! return } -// Implements ABCI -// InitChain runs the initialization logic directly on the CommitMultiStore. +// InitChain implements the ABCI interface. It runs the initialization logic +// directly on the CommitMultiStore. func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) { - // Stash the consensus params in the cms main store and memoize. + // stash the consensus params in the cms main store and memoize if req.ConsensusParams != nil { app.setConsensusParams(req.ConsensusParams) app.storeConsensusParams(req.ConsensusParams) } - // Initialize the deliver state and check state with ChainID and run initChain + // initialize the deliver state and check state with ChainID and run initChain app.setDeliverState(abci.Header{ChainID: req.ChainId}) app.setCheckState(abci.Header{ChainID: req.ChainId}) @@ -325,12 +339,12 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC res = app.initChainer(app.deliverState.ctx, req) - // NOTE: we don't commit, but BeginBlock for block 1 - // starts from this deliverState + // NOTE: We don't commit, but BeginBlock for block 1 starts from this + // deliverState. return } -// Filter peers by address / port +// FilterPeerByAddrPort filters peers by address/port. func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { if app.addrPeerFilter != nil { return app.addrPeerFilter(info) @@ -338,15 +352,16 @@ func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { return abci.ResponseQuery{} } -// Filter peers by public key -func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery { - if app.pubkeyPeerFilter != nil { - return app.pubkeyPeerFilter(info) +// FilterPeerByIDfilters peers by node ID. +func (app *BaseApp) FilterPeerByID(info string) abci.ResponseQuery { + if app.idPeerFilter != nil { + return app.idPeerFilter(info) } return abci.ResponseQuery{} } -// Splits a string path using the delimter '/'. i.e. "this/is/funny" becomes []string{"this", "is", "funny"} +// Splits a string path using the delimiter '/'. +// e.g. "this/is/funny" becomes []string{"this", "is", "funny"} func splitPath(requestPath string) (path []string) { path = strings.Split(requestPath, "/") // first element is empty string @@ -356,22 +371,26 @@ func splitPath(requestPath string) (path []string) { return path } -// Implements ABCI. -// Delegates to CommitMultiStore if it implements Queryable +// Query implements the ABCI interface. It delegates to CommitMultiStore if it +// implements Queryable. func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { path := splitPath(req.Path) if len(path) == 0 { msg := "no query path provided" return sdk.ErrUnknownRequest(msg).QueryResult() } + switch path[0] { // "/app" prefix for special application queries case "app": return handleQueryApp(app, path, req) + case "store": return handleQueryStore(app, path, req) + case "p2p": return handleQueryP2P(app, path, req) + case "custom": return handleQueryCustom(app, path, req) } @@ -383,6 +402,7 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { if len(path) >= 2 { var result sdk.Result + switch path[1] { case "simulate": txBytes := req.Data @@ -390,19 +410,20 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc if err != nil { result = err.Result() } else { - result = app.Simulate(tx) + result = app.Simulate(txBytes, tx) } + case "version": return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), Codespace: string(sdk.CodespaceRoot), - Value: []byte(version.GetVersion()), + Value: []byte(version.Version), } + default: result = sdk.ErrUnknownRequest(fmt.Sprintf("Unknown query: %s", path)).Result() } - // Encode with json value := codec.Cdc.MustMarshalBinaryLengthPrefixed(result) return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), @@ -410,6 +431,7 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc Value: value, } } + msg := "Expected second parameter to be either simulate or version, neither was present" return sdk.ErrUnknownRequest(msg).QueryResult() } @@ -421,51 +443,57 @@ func handleQueryStore(app *BaseApp, path []string, req abci.RequestQuery) (res a msg := "multistore doesn't support queries" return sdk.ErrUnknownRequest(msg).QueryResult() } + req.Path = "/" + strings.Join(path[1:], "/") return queryable.Query(req) } -// nolint: unparam -func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { +func handleQueryP2P(app *BaseApp, path []string, _ abci.RequestQuery) (res abci.ResponseQuery) { // "/p2p" prefix for p2p queries if len(path) >= 4 { - if path[1] == "filter" { - if path[2] == "addr" { - return app.FilterPeerByAddrPort(path[3]) + cmd, typ, arg := path[1], path[2], path[3] + switch cmd { + case "filter": + switch typ { + case "addr": + return app.FilterPeerByAddrPort(arg) + case "id": + return app.FilterPeerByID(arg) } - if path[2] == "pubkey" { - // TODO: this should be changed to `id` - // NOTE: this changed in tendermint and we didn't notice... - return app.FilterPeerByPubKey(path[3]) - } - } else { + default: msg := "Expected second parameter to be filter" return sdk.ErrUnknownRequest(msg).QueryResult() } } - msg := "Expected path is p2p filter " + msg := "Expected path is p2p filter " return sdk.ErrUnknownRequest(msg).QueryResult() } func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { - // path[0] should be "custom" because "/custom" prefix is required for keeper queries. - // the queryRouter routes using path[1]. For example, in the path "custom/gov/proposal", queryRouter routes using "gov" + // path[0] should be "custom" because "/custom" prefix is required for keeper + // queries. + // + // The queryRouter routes using path[1]. For example, in the path + // "custom/gov/proposal", queryRouter routes using "gov". if len(path) < 2 || path[1] == "" { return sdk.ErrUnknownRequest("No route for custom query specified").QueryResult() } + querier := app.queryRouter.Route(path[1]) if querier == nil { return sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult() } - // Cache wrap the commit-multistore for safety. + // cache wrap the commit-multistore for safety ctx := sdk.NewContext( - app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger, + app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.logger, ).WithMinGasPrices(app.minGasPrices) // Passes the rest of the path as an argument to the querier. - // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path + // + // For example, in the path "custom/gov/proposal/test", the gov querier gets + // []string{"proposal", "test"} as the path. resBytes, err := querier(ctx, path[2:], req) if err != nil { return abci.ResponseQuery{ @@ -474,6 +502,7 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res Log: err.ABCILog(), } } + return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), Value: resBytes, @@ -483,8 +512,7 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res // BeginBlock implements the ABCI application interface. func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeginBlock) { if app.cms.TracingEnabled() { - app.cms.ResetTraceContext() - app.cms.WithTracingContext(sdk.TraceContext( + app.cms.SetTracingContext(sdk.TraceContext( map[string]interface{}{"blockHeight": req.Header.Height}, )) } @@ -517,20 +545,20 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg } // set the signed validators for addition to context in deliverTx - // TODO: communicate this result to the address to pubkey map in slashing app.voteInfos = req.LastCommitInfo.GetVotes() return } -// CheckTx implements ABCI -// CheckTx runs the "basic checks" to see whether or not a transaction can possibly be executed, -// first decoding, then the ante handler (which checks signatures/fees/ValidateBasic), -// then finally the route match to see whether a handler exists. CheckTx does not run the actual -// Msg handler function(s). +// CheckTx implements the ABCI interface. It runs the "basic checks" to see +// whether or not a transaction can possibly be executed, first decoding, then +// the ante handler (which checks signatures/fees/ValidateBasic), then finally +// the route match to see whether a handler exists. +// +// NOTE:CheckTx does not run the actual Msg handler function(s). func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { - // Decode the Tx. var result sdk.Result - var tx, err = app.txDecoder(txBytes) + + tx, err := app.txDecoder(txBytes) if err != nil { result = err.Result() } else { @@ -547,22 +575,17 @@ func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { } } -// Implements ABCI +// DeliverTx implements the ABCI interface. func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { - - // Decode the Tx. - var tx, err = app.txDecoder(txBytes) var result sdk.Result + + tx, err := app.txDecoder(txBytes) if err != nil { result = err.Result() } else { result = app.runTx(runTxModeDeliver, txBytes, tx) } - // Even though the Result.Code is not OK, there are still effects, - // namely fee deductions and sequence incrementing. - - // Tell the blockchain engine (i.e. Tendermint). return abci.ResponseDeliverTx{ Code: uint32(result.Code), Codespace: string(result.Codespace), @@ -574,11 +597,10 @@ func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { } } -// Basic validator for msgs +// validateBasicTxMsgs executes basic validator calls for messages. func validateBasicTxMsgs(msgs []sdk.Msg) sdk.Error { if msgs == nil || len(msgs) == 0 { - // TODO: probably shouldn't be ErrInternal. Maybe new ErrInvalidMessage, or ? - return sdk.ErrInternal("Tx.GetMsgs() must return at least one message in list") + return sdk.ErrUnknownRequest("Tx.GetMsgs() must return at least one message in list") } for _, msg := range msgs { @@ -598,22 +620,25 @@ func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) (ctx sdk.Con WithTxBytes(txBytes). WithVoteInfos(app.voteInfos). WithConsensusParams(app.consensusParams) + if mode == runTxModeSimulate { ctx, _ = ctx.CacheContext() } + return } -// Iterates through msgs and executes them +// runMsgs iterates through all the messages and executes them. func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (result sdk.Result) { - // accumulate results logs := make([]string, 0, len(msgs)) + var data []byte // NOTE: we just append them all (?!) var tags sdk.Tags // also just append them all var code sdk.CodeType var codespace sdk.CodespaceType + for msgIdx, msg := range msgs { - // Match route. + // match message route msgRoute := msg.Route() handler := app.router.Route(msgRoute) if handler == nil { @@ -621,20 +646,20 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re } var msgResult sdk.Result - // Skip actual execution for CheckTx + + // skip actual execution for CheckTx mode if mode != runTxModeCheck { msgResult = handler(ctx, msg) } - // NOTE: GasWanted is determined by ante handler and - // GasUsed by the GasMeter + // NOTE: GasWanted is determined by ante handler and GasUsed by the GasMeter. - // Append Data and Tags + // Result.Data must be length prefixed in order to separate each result data = append(data, msgResult.Data...) - tags = append(tags, sdk.MakeTag(sdk.TagAction, []byte(msg.Type()))) + tags = append(tags, sdk.MakeTag(sdk.TagAction, msg.Type())) tags = append(tags, msgResult.Tags...) - // Stop execution and return on first failed message. + // stop execution and return on first failed message if !msgResult.IsOK() { logs = append(logs, fmt.Sprintf("Msg %d failed: %s", msgIdx, msgResult.Log)) code = msgResult.Code @@ -642,19 +667,17 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re break } - // Construct usable logs in multi-message transactions. + // construct usable logs in multi-message transactions logs = append(logs, fmt.Sprintf("Msg %d: %s", msgIdx, msgResult.Log)) } - // Set the final gas values. result = sdk.Result{ Code: code, Codespace: codespace, Data: data, Log: strings.Join(logs, "\n"), GasUsed: ctx.GasMeter().GasConsumed(), - // TODO: FeeAmount/FeeDenom - Tags: tags, + Tags: tags, } return result @@ -679,7 +702,7 @@ func (app *BaseApp) cacheTxContext(ctx sdk.Context, txBytes []byte) ( // TODO: https://github.com/cosmos/cosmos-sdk/issues/2824 msCache := ms.CacheMultiStore() if msCache.TracingEnabled() { - msCache = msCache.WithTracingContext( + msCache = msCache.SetTracingContext( sdk.TraceContext( map[string]interface{}{ "txHash": fmt.Sprintf("%X", tmhash.Sum(txBytes)), @@ -719,7 +742,10 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk if r := recover(); r != nil { switch rType := r.(type) { case sdk.ErrorOutOfGas: - log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + log := fmt.Sprintf( + "out of gas in location: %v; gasWanted: %d, gasUsed: %d", + rType.Descriptor, gasWanted, ctx.GasMeter().GasConsumed(), + ) result = sdk.ErrOutOfGas(log).Result() default: log := fmt.Sprintf("recovered: %v\nstack:\n%v", r, string(debug.Stack())) @@ -731,11 +757,11 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk result.GasUsed = ctx.GasMeter().GasConsumed() }() - // If BlockGasMeter() panics it will be caught by the above recover and - // return an error - in any case BlockGasMeter will consume gas past - // the limit. - // NOTE: this must exist in a separate defer function for the - // above recovery to recover from this one + // If BlockGasMeter() panics it will be caught by the above recover and will + // return an error - in any case BlockGasMeter will consume gas past the limit. + // + // NOTE: This must exist in a separate defer function for the above recovery + // to recover from this one. defer func() { if mode == runTxModeDeliver { ctx.BlockGasMeter().ConsumeGas( @@ -744,7 +770,7 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk ) if ctx.BlockGasMeter().GasConsumed() < startingGas { - panic(sdk.ErrorGasOverflow{"tx gas summation"}) + panic(sdk.ErrorGasOverflow{Descriptor: "tx gas summation"}) } } }() @@ -754,7 +780,6 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk return err.Result() } - // Execute the ante handler if one is defined. if app.anteHandler != nil { var anteCtx sdk.Context var msCache sdk.CacheMultiStore @@ -780,12 +805,13 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk ctx = newCtx.WithMultiStore(ms) } + gasWanted = result.GasWanted + if abort { return result } msCache.Write() - gasWanted = result.GasWanted } if mode == runTxModeCheck { @@ -810,10 +836,10 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk return } -// EndBlock implements the ABCI application interface. +// EndBlock implements the ABCI interface. func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { if app.deliverState.ms.TracingEnabled() { - app.deliverState.ms = app.deliverState.ms.ResetTraceContext().(sdk.CacheMultiStore) + app.deliverState.ms = app.deliverState.ms.SetTracingContext(nil).(sdk.CacheMultiStore) } if app.endBlocker != nil { @@ -823,27 +849,41 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc return } -// Implements ABCI +// Commit implements the ABCI interface. func (app *BaseApp) Commit() (res abci.ResponseCommit) { header := app.deliverState.ctx.BlockHeader() - // Write the Deliver state and commit the MultiStore + // write the Deliver state and commit the MultiStore app.deliverState.ms.Write() commitID := app.cms.Commit() - // TODO: this is missing a module identifier and dumps byte array - app.Logger.Debug("Commit synced", - "commit", fmt.Sprintf("%X", commitID), - ) + app.logger.Debug("Commit synced", "commit", fmt.Sprintf("%X", commitID)) - // Reset the Check state to the latest committed + // Reset the Check state to the latest committed. + // // NOTE: safe because Tendermint holds a lock on the mempool for Commit. // Use the header from this latest block. app.setCheckState(header) - // Empty the Deliver state + // empty/reset the deliver state app.deliverState = nil return abci.ResponseCommit{ Data: commitID.Hash, } } + +// ---------------------------------------------------------------------------- +// State + +type state struct { + ms sdk.CacheMultiStore + ctx sdk.Context +} + +func (st *state) CacheMultiStore() sdk.CacheMultiStore { + return st.ms.CacheMultiStore() +} + +func (st *state) Context() sdk.Context { + return st.ctx +} diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index acccd1fbf..7e64c114e 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -7,7 +7,7 @@ import ( "os" "testing" - "github.com/cosmos/cosmos-sdk/store" + store "github.com/cosmos/cosmos-sdk/store/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,14 +21,10 @@ import ( ) var ( - // make some cap keys capKey1 = sdk.NewKVStoreKey("key1") capKey2 = sdk.NewKVStoreKey("key2") ) -//------------------------------------------------------------------------------------------ -// Helpers for setup. Most tests should be able to use setupBaseApp - func defaultLogger() log.Logger { return log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") } @@ -70,9 +66,6 @@ func setupBaseApp(t *testing.T, options ...func(*BaseApp)) *BaseApp { return app } -//------------------------------------------------------------------------------------------ -// test mounting and loading stores - func TestMountStores(t *testing.T) { app := setupBaseApp(t) @@ -137,6 +130,41 @@ func TestLoadVersion(t *testing.T) { testLoadVersionHelper(t, app, int64(2), commitID2) } +func TestLoadVersionInvalid(t *testing.T) { + logger := log.NewNopLogger() + pruningOpt := SetPruning(store.PruneSyncable) + db := dbm.NewMemDB() + name := t.Name() + app := NewBaseApp(name, logger, db, nil, pruningOpt) + + capKey := sdk.NewKVStoreKey(MainStoreKey) + app.MountStores(capKey) + err := app.LoadLatestVersion(capKey) + require.Nil(t, err) + + // require error when loading an invalid version + err = app.LoadVersion(-1, capKey) + require.Error(t, err) + + header := abci.Header{Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + res := app.Commit() + commitID1 := sdk.CommitID{1, res.Data} + + // create a new app with the stores mounted under the same cap key + app = NewBaseApp(name, logger, db, nil, pruningOpt) + app.MountStores(capKey) + + // require we can load the latest version + err = app.LoadVersion(1, capKey) + require.Nil(t, err) + testLoadVersionHelper(t, app, int64(1), commitID1) + + // require error when loading an invalid version + err = app.LoadVersion(2, capKey) + require.Error(t, err) +} + func testLoadVersionHelper(t *testing.T, app *BaseApp, expectedHeight int64, expectedID sdk.CommitID) { lastHeight := app.LastBlockHeight() lastID := app.LastCommitID() @@ -157,39 +185,21 @@ func testChangeNameHelper(name string) func(*BaseApp) { } } -// Test that the app hash is static -// TODO: https://github.com/cosmos/cosmos-sdk/issues/520 -/*func TestStaticAppHash(t *testing.T) { - app := newBaseApp(t.Name()) - - // make a cap key and mount the store - capKey := sdk.NewKVStoreKey(MainStoreKey) - app.MountStores(capKey) - err := app.LoadLatestVersion(capKey) // needed to make stores non-nil - require.Nil(t, err) - - // execute some blocks - header := abci.Header{Height: 1} - app.BeginBlock(abci.RequestBeginBlock{Header: header}) - res := app.Commit() - commitID1 := sdk.CommitID{1, res.Data} - - header = abci.Header{Height: 2} - app.BeginBlock(abci.RequestBeginBlock{Header: header}) - res = app.Commit() - commitID2 := sdk.CommitID{2, res.Data} - - require.Equal(t, commitID1.Hash, commitID2.Hash) -} -*/ - -//------------------------------------------------------------------------------------------ -// test some basic abci/baseapp functionality - // Test that txs can be unmarshalled and read and that // correct error codes are returned when not func TestTxDecoder(t *testing.T) { - // TODO + codec := codec.New() + registerTestCodec(codec) + + app := newBaseApp(t.Name()) + tx := newTxCounter(1, 0) + txBytes := codec.MustMarshalBinaryLengthPrefixed(tx) + + dTx, err := app.txDecoder(txBytes) + require.NoError(t, err) + + cTx := dTx.(txTest) + require.Equal(t, tx.Counter, cTx.Counter) } // Test that Info returns the latest committed state. @@ -210,8 +220,46 @@ func TestInfo(t *testing.T) { // TODO } -//------------------------------------------------------------------------------------------ -// InitChain, BeginBlock, EndBlock +func TestBaseAppOptionSeal(t *testing.T) { + app := setupBaseApp(t) + + require.Panics(t, func() { + app.SetName("") + }) + require.Panics(t, func() { + app.SetDB(nil) + }) + require.Panics(t, func() { + app.SetCMS(nil) + }) + require.Panics(t, func() { + app.SetInitChainer(nil) + }) + require.Panics(t, func() { + app.SetBeginBlocker(nil) + }) + require.Panics(t, func() { + app.SetEndBlocker(nil) + }) + require.Panics(t, func() { + app.SetAnteHandler(nil) + }) + require.Panics(t, func() { + app.SetAddrPeerFilter(nil) + }) + require.Panics(t, func() { + app.SetIDPeerFilter(nil) + }) + require.Panics(t, func() { + app.SetFauxMerkleMode() + }) +} + +func TestSetMinGasPrices(t *testing.T) { + minGasPrices := sdk.DecCoins{sdk.NewDecCoin("stake", 5000)} + app := newBaseApp(t.Name(), SetMinGasPrices(minGasPrices.String())) + require.Equal(t, minGasPrices, app.minGasPrices) +} func TestInitChainer(t *testing.T) { name := t.Name() @@ -280,11 +328,6 @@ func TestInitChainer(t *testing.T) { require.Equal(t, value, res.Value) } -//------------------------------------------------------------------------------------------ -// Mock tx, msgs, and mapper for the baseapp tests. -// Self-contained, just uses counters. -// We don't care about signatures, coins, accounts, etc. in the baseapp. - // Simple tx with a list of Msgs. type txTest struct { Msgs []sdk.Msg @@ -417,9 +460,6 @@ func handlerMsgCounter(t *testing.T, capKey *sdk.KVStoreKey, deliverKey []byte) } } -//----------------------------------------------------------------- -// simple int mapper - func i2b(i int64) []byte { return []byte{byte(i)} } @@ -655,20 +695,20 @@ func TestSimulateTx(t *testing.T) { app.BeginBlock(abci.RequestBeginBlock{}) tx := newTxCounter(count, count) + txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx) + require.Nil(t, err) // simulate a message, check gas reported - result := app.Simulate(tx) + result := app.Simulate(txBytes, tx) require.True(t, result.IsOK(), result.Log) require.Equal(t, gasConsumed, result.GasUsed) // simulate again, same result - result = app.Simulate(tx) + result = app.Simulate(txBytes, tx) require.True(t, result.IsOK(), result.Log) require.Equal(t, gasConsumed, result.GasUsed) // simulate by calling Query with encoded tx - txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx) - require.Nil(t, err) query := abci.RequestQuery{ Path: "/app/simulate", Data: txBytes, @@ -686,10 +726,6 @@ func TestSimulateTx(t *testing.T) { } } -//------------------------------------------------------------------------------------------- -// Tx failure cases -// TODO: add more - func TestRunInvalidTransaction(t *testing.T) { anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { @@ -707,7 +743,7 @@ func TestRunInvalidTransaction(t *testing.T) { { emptyTx := &txTest{} err := app.Deliver(emptyTx) - require.EqualValues(t, sdk.CodeInternal, err.Code) + require.EqualValues(t, sdk.CodeUnknownRequest, err.Code) require.EqualValues(t, sdk.CodespaceRoot, err.Codespace) } @@ -777,11 +813,6 @@ func TestTxGasLimits(t *testing.T) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasGranted)) - // NOTE/TODO/XXX: - // AnteHandlers must have their own defer/recover in order - // for the BaseApp to know how much gas was used used! - // This is because the GasMeter is created in the AnteHandler, - // but if it panics the context won't be set properly in runTx's recover ... defer func() { if r := recover(); r != nil { switch rType := r.(type) { @@ -866,11 +897,6 @@ func TestMaxBlockGasLimits(t *testing.T) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasGranted)) - // NOTE/TODO/XXX: - // AnteHandlers must have their own defer/recover in order - // for the BaseApp to know how much gas was used used! - // This is because the GasMeter is created in the AnteHandler, - // but if it panics the context won't be set properly in runTx's recover ... defer func() { if r := recover(); r != nil { switch rType := r.(type) { @@ -1031,3 +1057,162 @@ func TestBaseAppAnteHandler(t *testing.T) { app.EndBlock(abci.RequestEndBlock{}) app.Commit() } + +func TestGasConsumptionBadTx(t *testing.T) { + gasWanted := uint64(5) + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasWanted)) + + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + res = sdk.ErrOutOfGas(log).Result() + res.GasWanted = gasWanted + res.GasUsed = newCtx.GasMeter().GasConsumed() + default: + panic(r) + } + } + }() + + txTest := tx.(txTest) + newCtx.GasMeter().ConsumeGas(uint64(txTest.Counter), "counter-ante") + if txTest.FailOnAnte { + return newCtx, sdk.ErrInternal("ante handler failure").Result(), true + } + + res = sdk.Result{ + GasWanted: gasWanted, + } + return + }) + } + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + count := msg.(msgCounter).Counter + ctx.GasMeter().ConsumeGas(uint64(count), "counter-handler") + return sdk.Result{} + }) + } + + cdc := codec.New() + registerTestCodec(cdc) + + app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{ + ConsensusParams: &abci.ConsensusParams{ + BlockSize: &abci.BlockSizeParams{ + MaxGas: 9, + }, + }, + }) + + app.InitChain(abci.RequestInitChain{}) + app.BeginBlock(abci.RequestBeginBlock{}) + + tx := newTxCounter(5, 0) + tx.setFailOnAnte(true) + txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx) + require.NoError(t, err) + + res := app.DeliverTx(txBytes) + require.False(t, res.IsOK(), fmt.Sprintf("%v", res)) + + // require next tx to fail due to black gas limit + tx = newTxCounter(5, 0) + txBytes, err = cdc.MarshalBinaryLengthPrefixed(tx) + require.NoError(t, err) + + res = app.DeliverTx(txBytes) + require.False(t, res.IsOK(), fmt.Sprintf("%v", res)) +} + +// Test that we can only query from the latest committed state. +func TestQuery(t *testing.T) { + key, value := []byte("hello"), []byte("goodbye") + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + store := ctx.KVStore(capKey1) + store.Set(key, value) + return + }) + } + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + store := ctx.KVStore(capKey1) + store.Set(key, value) + return sdk.Result{} + }) + } + + app := setupBaseApp(t, anteOpt, routerOpt) + + app.InitChain(abci.RequestInitChain{}) + + // NOTE: "/store/key1" tells us KVStore + // and the final "/key" says to use the data as the + // key in the given KVStore ... + query := abci.RequestQuery{ + Path: "/store/key1/key", + Data: key, + } + tx := newTxCounter(0, 0) + + // query is empty before we do anything + res := app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query is still empty after a CheckTx + resTx := app.Check(tx) + require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) + res = app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query is still empty after a DeliverTx before we commit + app.BeginBlock(abci.RequestBeginBlock{}) + resTx = app.Deliver(tx) + require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) + res = app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query returns correct value after Commit + app.Commit() + res = app.Query(query) + require.Equal(t, value, res.Value) +} + +// Test p2p filter queries +func TestP2PQuery(t *testing.T) { + addrPeerFilterOpt := func(bapp *BaseApp) { + bapp.SetAddrPeerFilter(func(addrport string) abci.ResponseQuery { + require.Equal(t, "1.1.1.1:8000", addrport) + return abci.ResponseQuery{Code: uint32(3)} + }) + } + + idPeerFilterOpt := func(bapp *BaseApp) { + bapp.SetIDPeerFilter(func(id string) abci.ResponseQuery { + require.Equal(t, "testid", id) + return abci.ResponseQuery{Code: uint32(4)} + }) + } + + app := setupBaseApp(t, addrPeerFilterOpt, idPeerFilterOpt) + + addrQuery := abci.RequestQuery{ + Path: "/p2p/filter/addr/1.1.1.1:8000", + } + res := app.Query(addrQuery) + require.Equal(t, uint32(3), res.Code) + + idQuery := abci.RequestQuery{ + Path: "/p2p/filter/id/testid", + } + res = app.Query(idQuery) + require.Equal(t, uint32(4), res.Code) +} diff --git a/baseapp/helpers.go b/baseapp/helpers.go index 0ac99887a..25f59cee5 100644 --- a/baseapp/helpers.go +++ b/baseapp/helpers.go @@ -1,21 +1,23 @@ package baseapp import ( - "github.com/tendermint/tendermint/abci/server" + "regexp" + abci "github.com/tendermint/tendermint/abci/types" - cmn "github.com/tendermint/tendermint/libs/common" sdk "github.com/cosmos/cosmos-sdk/types" ) +var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString + // nolint - Mostly for testing func (app *BaseApp) Check(tx sdk.Tx) (result sdk.Result) { return app.runTx(runTxModeCheck, nil, tx) } // nolint - full tx execution -func (app *BaseApp) Simulate(tx sdk.Tx) (result sdk.Result) { - return app.runTx(runTxModeSimulate, nil, tx) +func (app *BaseApp) Simulate(txBytes []byte, tx sdk.Tx) (result sdk.Result) { + return app.runTx(runTxModeSimulate, txBytes, tx) } // nolint @@ -23,27 +25,13 @@ func (app *BaseApp) Deliver(tx sdk.Tx) (result sdk.Result) { return app.runTx(runTxModeDeliver, nil, tx) } -// RunForever BasecoinApp execution and cleanup -func RunForever(app abci.Application) { - - // Start the ABCI server - srv, err := server.NewServer("0.0.0.0:26658", "socket", app) - if err != nil { - cmn.Exit(err.Error()) - return - } - err = srv.Start() - if err != nil { - cmn.Exit(err.Error()) - return +// Context with current {check, deliver}State of the app +// used by tests +func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { + if isCheckTx { + return sdk.NewContext(app.checkState.ms, header, true, app.logger). + WithMinGasPrices(app.minGasPrices) } - // Wait forever - cmn.TrapSignal(func() { - // Cleanup - err := srv.Stop() - if err != nil { - cmn.Exit(err.Error()) - } - }) + return sdk.NewContext(app.deliverState.ms, header, false, app.logger) } diff --git a/baseapp/options.go b/baseapp/options.go index 8e6687335..a40d6eba5 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -18,7 +18,7 @@ func SetPruning(opts sdk.PruningOptions) func(*BaseApp) { return func(bap *BaseApp) { bap.cms.SetPruning(opts) } } -// SetMinimumGasPrices returns an option that sets the minimum gas prices on the app. +// SetMinGasPrices returns an option that sets the minimum gas prices on the app. func SetMinGasPrices(gasPricesStr string) func(*BaseApp) { gasPrices, err := sdk.ParseDecCoins(gasPricesStr) if err != nil { @@ -84,11 +84,11 @@ func (app *BaseApp) SetAddrPeerFilter(pf sdk.PeerFilter) { app.addrPeerFilter = pf } -func (app *BaseApp) SetPubKeyPeerFilter(pf sdk.PeerFilter) { +func (app *BaseApp) SetIDPeerFilter(pf sdk.PeerFilter) { if app.sealed { - panic("SetPubKeyPeerFilter() on sealed BaseApp") + panic("SetIDPeerFilter() on sealed BaseApp") } - app.pubkeyPeerFilter = pf + app.idPeerFilter = pf } func (app *BaseApp) SetFauxMerkleMode() { @@ -97,25 +97,3 @@ func (app *BaseApp) SetFauxMerkleMode() { } app.fauxMerkleMode = true } - -//---------------------------------------- -// TODO: move these out of this file? - -func (app *BaseApp) Router() Router { - if app.sealed { - panic("Router() on sealed BaseApp") - } - return app.router -} - -func (app *BaseApp) QueryRouter() QueryRouter { - return app.queryRouter -} - -func (app *BaseApp) Seal() { app.sealed = true } -func (app *BaseApp) IsSealed() bool { return app.sealed } -func (app *BaseApp) enforceSeal() { - if !app.sealed { - panic("enforceSeal() on BaseApp but not sealed") - } -} diff --git a/baseapp/query.go b/baseapp/query.go deleted file mode 100644 index 6527fda4f..000000000 --- a/baseapp/query.go +++ /dev/null @@ -1,54 +0,0 @@ -package baseapp - -/* - XXX Make this work with MultiStore. - XXX It will require some interfaces updates in store/types.go. - - if len(reqQuery.Data) == 0 { - resQuery.Log = "Query cannot be zero length" - resQuery.Code = abci.CodeType_EncodingError - return - } - - // set the query response height to current - tree := app.state.Committed() - - height := reqQuery.Height - if height == 0 { - // TODO: once the rpc actually passes in non-zero - // heights we can use to query right after a tx - // we must retrun most recent, even if apphash - // is not yet in the blockchain - - withProof := app.CommittedHeight() - 1 - if tree.Tree.VersionExists(withProof) { - height = withProof - } else { - height = app.CommittedHeight() - } - } - resQuery.Height = height - - switch reqQuery.Path { - case "/store", "/key": // Get by key - key := reqQuery.Data // Data holds the key bytes - resQuery.Key = key - if reqQuery.Prove { - value, proof, err := tree.GetVersionedWithProof(key, height) - if err != nil { - resQuery.Log = err.Error() - break - } - resQuery.Value = value - resQuery.Proof = proof.Bytes() - } else { - value := tree.Get(key) - resQuery.Value = value - } - - default: - resQuery.Code = abci.CodeType_UnknownRequest - resQuery.Log = cmn.Fmt("Unexpected Query path: %v", reqQuery.Path) - } - return -*/ diff --git a/baseapp/query_test.go b/baseapp/query_test.go deleted file mode 100644 index fed7ae477..000000000 --- a/baseapp/query_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package baseapp - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Test that we can only query from the latest committed state. -func TestQuery(t *testing.T) { - key, value := []byte("hello"), []byte("goodbye") - anteOpt := func(bapp *BaseApp) { - bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { - store := ctx.KVStore(capKey1) - store.Set(key, value) - return - }) - } - - routerOpt := func(bapp *BaseApp) { - bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - store := ctx.KVStore(capKey1) - store.Set(key, value) - return sdk.Result{} - }) - } - - app := setupBaseApp(t, anteOpt, routerOpt) - - app.InitChain(abci.RequestInitChain{}) - - // NOTE: "/store/key1" tells us KVStore - // and the final "/key" says to use the data as the - // key in the given KVStore ... - query := abci.RequestQuery{ - Path: "/store/key1/key", - Data: key, - } - tx := newTxCounter(0, 0) - - // query is empty before we do anything - res := app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query is still empty after a CheckTx - resTx := app.Check(tx) - require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) - res = app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query is still empty after a DeliverTx before we commit - app.BeginBlock(abci.RequestBeginBlock{}) - resTx = app.Deliver(tx) - require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) - res = app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query returns correct value after Commit - app.Commit() - res = app.Query(query) - require.Equal(t, value, res.Value) -} - -// Test p2p filter queries -func TestP2PQuery(t *testing.T) { - addrPeerFilterOpt := func(bapp *BaseApp) { - bapp.SetAddrPeerFilter(func(addrport string) abci.ResponseQuery { - require.Equal(t, "1.1.1.1:8000", addrport) - return abci.ResponseQuery{Code: uint32(3)} - }) - } - - pubkeyPeerFilterOpt := func(bapp *BaseApp) { - bapp.SetPubKeyPeerFilter(func(pubkey string) abci.ResponseQuery { - require.Equal(t, "testpubkey", pubkey) - return abci.ResponseQuery{Code: uint32(4)} - }) - } - - app := setupBaseApp(t, addrPeerFilterOpt, pubkeyPeerFilterOpt) - - addrQuery := abci.RequestQuery{ - Path: "/p2p/filter/addr/1.1.1.1:8000", - } - res := app.Query(addrQuery) - require.Equal(t, uint32(3), res.Code) - - pubkeyQuery := abci.RequestQuery{ - Path: "/p2p/filter/pubkey/testpubkey", - } - res = app.Query(pubkeyQuery) - require.Equal(t, uint32(4), res.Code) -} diff --git a/baseapp/queryrouter.go b/baseapp/queryrouter.go index 23cfad072..178646b7e 100644 --- a/baseapp/queryrouter.go +++ b/baseapp/queryrouter.go @@ -1,6 +1,8 @@ package baseapp import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -10,32 +12,34 @@ type QueryRouter interface { Route(path string) (h sdk.Querier) } -type queryrouter struct { +type queryRouter struct { routes map[string]sdk.Querier } -// nolint -// NewRouter - create new router -// TODO either make Function unexported or make return type (router) Exported -func NewQueryRouter() *queryrouter { - return &queryrouter{ +// NewQueryRouter returns a reference to a new queryRouter. +// +// TODO: Either make the function private or make return type (queryRouter) public. +func NewQueryRouter() *queryRouter { // nolint: golint + return &queryRouter{ routes: map[string]sdk.Querier{}, } } -// AddRoute - Adds an sdk.Querier to the route provided. Panics on duplicate -func (rtr *queryrouter) AddRoute(r string, q sdk.Querier) QueryRouter { - if !isAlphaNumeric(r) { +// AddRoute adds a query path to the router with a given Querier. It will panic +// if a duplicate route is given. The route must be alphanumeric. +func (qrt *queryRouter) AddRoute(path string, q sdk.Querier) QueryRouter { + if !isAlphaNumeric(path) { panic("route expressions can only contain alphanumeric characters") } - if rtr.routes[r] != nil { - panic("route has already been initialized") + if qrt.routes[path] != nil { + panic(fmt.Sprintf("route %s has already been initialized", path)) } - rtr.routes[r] = q - return rtr + + qrt.routes[path] = q + return qrt } -// Returns the sdk.Querier for a certain route path -func (rtr *queryrouter) Route(path string) (h sdk.Querier) { - return rtr.routes[path] +// Route returns the Querier for a given query route path. +func (qrt *queryRouter) Route(path string) sdk.Querier { + return qrt.routes[path] } diff --git a/baseapp/queryrouter_test.go b/baseapp/queryrouter_test.go new file mode 100644 index 000000000..774302831 --- /dev/null +++ b/baseapp/queryrouter_test.go @@ -0,0 +1,33 @@ +package baseapp + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var testQuerier = func(_ sdk.Context, _ []string, _ abci.RequestQuery) (res []byte, err sdk.Error) { + return nil, nil +} + +func TestQueryRouter(t *testing.T) { + qr := NewQueryRouter() + + // require panic on invalid route + require.Panics(t, func() { + qr.AddRoute("*", testQuerier) + }) + + qr.AddRoute("testRoute", testQuerier) + q := qr.Route("testRoute") + require.NotNil(t, q) + + // require panic on duplicate route + require.Panics(t, func() { + qr.AddRoute("testRoute", testQuerier) + }) +} diff --git a/baseapp/router.go b/baseapp/router.go index 4be3aec74..5e829d73c 100644 --- a/baseapp/router.go +++ b/baseapp/router.go @@ -1,7 +1,7 @@ package baseapp import ( - "regexp" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -12,44 +12,36 @@ type Router interface { Route(path string) (h sdk.Handler) } -// map a transaction type to a handler and an initgenesis function -type route struct { - r string - h sdk.Handler -} - type router struct { - routes []route + routes map[string]sdk.Handler } -// nolint -// NewRouter - create new router -// TODO either make Function unexported or make return type (router) Exported -func NewRouter() *router { +// NewRouter returns a reference to a new router. +// +// TODO: Either make the function private or make return type (router) public. +func NewRouter() *router { // nolint: golint return &router{ - routes: make([]route, 0), + routes: make(map[string]sdk.Handler), } } -var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString - -// AddRoute - TODO add description -func (rtr *router) AddRoute(r string, h sdk.Handler) Router { - if !isAlphaNumeric(r) { +// AddRoute adds a route path to the router with a given handler. The route must +// be alphanumeric. +func (rtr *router) AddRoute(path string, h sdk.Handler) Router { + if !isAlphaNumeric(path) { panic("route expressions can only contain alphanumeric characters") } - rtr.routes = append(rtr.routes, route{r, h}) + if rtr.routes[path] != nil { + panic(fmt.Sprintf("route %s has already been initialized", path)) + } + rtr.routes[path] = h return rtr } -// Route - TODO add description -// TODO handle expressive matches. -func (rtr *router) Route(path string) (h sdk.Handler) { - for _, route := range rtr.routes { - if route.r == path { - return route.h - } - } - return nil +// Route returns a handler for a given route path. +// +// TODO: Handle expressive matches. +func (rtr *router) Route(path string) sdk.Handler { + return rtr.routes[path] } diff --git a/baseapp/router_test.go b/baseapp/router_test.go new file mode 100644 index 000000000..86e09cf21 --- /dev/null +++ b/baseapp/router_test.go @@ -0,0 +1,31 @@ +package baseapp + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var testHandler = func(_ sdk.Context, _ sdk.Msg) sdk.Result { + return sdk.Result{} +} + +func TestRouter(t *testing.T) { + rtr := NewRouter() + + // require panic on invalid route + require.Panics(t, func() { + rtr.AddRoute("*", testHandler) + }) + + rtr.AddRoute("testRoute", testHandler) + h := rtr.Route("testRoute") + require.NotNil(t, h) + + // require panic on duplicate route + require.Panics(t, func() { + rtr.AddRoute("testRoute", testHandler) + }) +} diff --git a/baseapp/testdata/genesis.json b/baseapp/testdata/genesis.json deleted file mode 100644 index ee8879fd2..000000000 --- a/baseapp/testdata/genesis.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chain_id": "foo_bar_chain", - "app_options": { - "accounts": [{ - "pub_key": { - "type": "ed25519", - "data": "6880db93598e283a67c4d88fc67a8858aa2de70f713fe94a5109e29c137100c2" - }, - "coins": [ - { - "denom": "blank", - "amount": 12345 - }, - { - "denom": "ETH", - "amount": 654321 - } - ] - }], - "plugin_options": ["plugin1/key1", "value1", "plugin1/key2", "value2"] - } -} diff --git a/baseapp/testdata/genesis2.json b/baseapp/testdata/genesis2.json deleted file mode 100644 index 18ec87164..000000000 --- a/baseapp/testdata/genesis2.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "chain_id": "addr_accounts_chain", - "app_options": { - "accounts": [{ - "name": "alice", - "pub_key": { - "type": "ed25519", - "data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36" - }, - "coins": [ - { - "denom": "one", - "amount": 111 - } - ] - }, { - "name": "bob", - "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", - "coins": [ - { - "denom": "two", - "amount": 222 - } - ] - }, { - "name": "sam", - "pub_key": { - "type": "secp256k1", - "data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E" - }, - "coins": [ - { - "denom": "four", - "amount": 444 - } - ] - }] - } -} diff --git a/baseapp/testdata/genesis2b.json b/baseapp/testdata/genesis2b.json deleted file mode 100644 index a880b3c64..000000000 --- a/baseapp/testdata/genesis2b.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "chain_id": "addr_accounts_chain", - "app_options": { - "accounts": [{ - "name": "alice", - "pub_key": { - "type": "ed25519", - "data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36" - }, - "coins": [ - { - "denom": "one", - "amount": 111 - } - ] - }, { - "name": "bob", - "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", - "coins": [ - { - "denom": "two", - "amount": 222 - } - ] - }, { - "name": "carl", - "address": "1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9", - "pub_key": { - "type": "ed25519", - "data": "177C0AC45E86257F0708DC085D592AB22AAEECD1D26381B757F7C96135921858" - }, - "coins": [ - { - "denom": "three", - "amount": 333 - } - ] - }, { - "name": "sam", - "pub_key": { - "type": "secp256k1", - "data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E" - }, - "coins": [ - { - "denom": "four", - "amount": 444 - } - ] - }] - } -} diff --git a/baseapp/testdata/genesis3.json b/baseapp/testdata/genesis3.json deleted file mode 100644 index b58b1d740..000000000 --- a/baseapp/testdata/genesis3.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "chain_id": "foo_bar_chain" -} diff --git a/client/config.go b/client/config.go index 675a35296..ce57434a5 100644 --- a/client/config.go +++ b/client/config.go @@ -9,8 +9,6 @@ import ( "github.com/tendermint/tendermint/libs/cli" - "github.com/cosmos/cosmos-sdk/cmd/gaia/app" - "github.com/pelletier/go-toml" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -32,7 +30,7 @@ func init() { // ConfigCmd returns a CLI command to interactively create a // Gaia CLI config file. -func ConfigCmd() *cobra.Command { +func ConfigCmd(defaultCLIHome string) *cobra.Command { cmd := &cobra.Command{ Use: "config [value]", Short: "Create or query a Gaia CLI configuration file", @@ -40,7 +38,7 @@ func ConfigCmd() *cobra.Command { Args: cobra.RangeArgs(0, 2), } - cmd.Flags().String(cli.HomeFlag, app.DefaultCLIHome, + cmd.Flags().String(cli.HomeFlag, defaultCLIHome, "set client's home directory for configuration") cmd.Flags().Bool(flagGet, false, "print configuration value or its default if unset") diff --git a/client/context/broadcast.go b/client/context/broadcast.go index c844e519d..0503c8e59 100644 --- a/client/context/broadcast.go +++ b/client/context/broadcast.go @@ -2,169 +2,79 @@ package context import ( "fmt" - "io" - "github.com/pkg/errors" - - abci "github.com/tendermint/tendermint/abci/types" - ctypes "github.com/tendermint/tendermint/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) -// TODO: This should get deleted eventually, and perhaps -// ctypes.ResultBroadcastTx be stripped of unused fields, and -// ctypes.ResultBroadcastTxCommit returned for tendermint RPC BroadcastTxSync. -// -// The motivation is that we want a unified type to return, and the better -// option is the one that can hold CheckTx/DeliverTx responses optionally. -func resultBroadcastTxToCommit(res *ctypes.ResultBroadcastTx) *ctypes.ResultBroadcastTxCommit { - return &ctypes.ResultBroadcastTxCommit{ - Hash: res.Hash, - // NOTE: other fields are unused for async. - } -} - // BroadcastTx broadcasts a transactions either synchronously or asynchronously // based on the context parameters. The result of the broadcast is parsed into // an intermediate structure which is logged if the context has a logger // defined. -func (ctx CLIContext) BroadcastTx(txBytes []byte) (*ctypes.ResultBroadcastTxCommit, error) { +func (ctx CLIContext) BroadcastTx(txBytes []byte) (res sdk.TxResponse, err error) { if ctx.Async { - res, err := ctx.broadcastTxAsync(txBytes) - if err != nil { - return nil, err + if res, err = ctx.BroadcastTxAsync(txBytes); err != nil { + return } - - resCommit := resultBroadcastTxToCommit(res) - return resCommit, err + return } - return ctx.broadcastTxCommit(txBytes) + if res, err = ctx.BroadcastTxAndAwaitCommit(txBytes); err != nil { + return + } + + return } // BroadcastTxAndAwaitCommit broadcasts transaction bytes to a Tendermint node // and waits for a commit. -func (ctx CLIContext) BroadcastTxAndAwaitCommit(tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { +func (ctx CLIContext) BroadcastTxAndAwaitCommit(tx []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { - return nil, err + return sdk.TxResponse{}, err } res, err := node.BroadcastTxCommit(tx) if err != nil { - return res, err + return sdk.NewResponseFormatBroadcastTxCommit(res), err } if !res.CheckTx.IsOK() { - return res, errors.Errorf(res.CheckTx.Log) + return sdk.NewResponseFormatBroadcastTxCommit(res), fmt.Errorf(res.CheckTx.Log) } if !res.DeliverTx.IsOK() { - return res, errors.Errorf(res.DeliverTx.Log) + return sdk.NewResponseFormatBroadcastTxCommit(res), fmt.Errorf(res.DeliverTx.Log) } - return res, err + return sdk.NewResponseFormatBroadcastTxCommit(res), err } -// BroadcastTxSync broadcasts transaction bytes to a Tendermint node -// synchronously. -func (ctx CLIContext) BroadcastTxSync(tx []byte) (*ctypes.ResultBroadcastTx, error) { +// BroadcastTxSync broadcasts transaction bytes to a Tendermint node synchronously. +func (ctx CLIContext) BroadcastTxSync(tx []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { - return nil, err + return sdk.TxResponse{}, err } res, err := node.BroadcastTxSync(tx) if err != nil { - return res, err + return sdk.NewResponseFormatBroadcastTx(res), err } - return res, err + return sdk.NewResponseFormatBroadcastTx(res), err } -// BroadcastTxAsync broadcasts transaction bytes to a Tendermint node -// asynchronously. -func (ctx CLIContext) BroadcastTxAsync(tx []byte) (*ctypes.ResultBroadcastTx, error) { +// BroadcastTxAsync broadcasts transaction bytes to a Tendermint node asynchronously. +func (ctx CLIContext) BroadcastTxAsync(tx []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { - return nil, err + return sdk.TxResponse{}, err } res, err := node.BroadcastTxAsync(tx) if err != nil { - return res, err + return sdk.NewResponseFormatBroadcastTx(res), err } - return res, err -} - -func (ctx CLIContext) broadcastTxAsync(txBytes []byte) (*ctypes.ResultBroadcastTx, error) { - res, err := ctx.BroadcastTxAsync(txBytes) - if err != nil { - return res, err - } - - if ctx.Output != nil { - if ctx.OutputFormat == "json" { - type toJSON struct { - TxHash string - } - - resJSON := toJSON{res.Hash.String()} - bz, err := ctx.Codec.MarshalJSON(resJSON) - if err != nil { - return res, err - } - - ctx.Output.Write(bz) - io.WriteString(ctx.Output, "\n") - } else { - io.WriteString(ctx.Output, fmt.Sprintf("async tx sent (tx hash: %s)\n", res.Hash)) - } - } - - return res, nil -} - -func (ctx CLIContext) broadcastTxCommit(txBytes []byte) (*ctypes.ResultBroadcastTxCommit, error) { - res, err := ctx.BroadcastTxAndAwaitCommit(txBytes) - if err != nil { - return res, err - } - - if ctx.OutputFormat == "json" { - // Since JSON is intended for automated scripts, always include response in - // JSON mode. - type toJSON struct { - Height int64 - TxHash string - Response abci.ResponseDeliverTx - } - - if ctx.Output != nil { - resJSON := toJSON{res.Height, res.Hash.String(), res.DeliverTx} - bz, err := ctx.Codec.MarshalJSON(resJSON) - if err != nil { - return res, err - } - - ctx.Output.Write(bz) - io.WriteString(ctx.Output, "\n") - } - - return res, nil - } - - if ctx.Output != nil { - resStr := fmt.Sprintf("Committed at block %d (tx hash: %s)\n", res.Height, res.Hash.String()) - - if ctx.PrintResponse { - resStr = fmt.Sprintf("Committed at block %d (tx hash: %s, response: %+v)\n", - res.Height, res.Hash.String(), res.DeliverTx, - ) - } - - io.WriteString(ctx.Output, resStr) - } - - return res, nil + return sdk.NewResponseFormatBroadcastTx(res), err } diff --git a/client/context/context.go b/client/context/context.go index 6950122c4..7364adbff 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -8,7 +8,9 @@ import ( "path/filepath" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/codec" + cryptokeys "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/spf13/viper" @@ -19,13 +21,12 @@ import ( tmliteProxy "github.com/tendermint/tendermint/lite/proxy" rpcclient "github.com/tendermint/tendermint/rpc/client" - "github.com/cosmos/cosmos-sdk/client/keys" - cskeys "github.com/cosmos/cosmos-sdk/crypto/keys" - "github.com/cosmos/cosmos-sdk/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) var ( - verifier tmlite.Verifier + verifier tmlite.Verifier + verifierHome string ) // CLIContext implements a typical CLI context created in SDK modules for @@ -34,6 +35,7 @@ type CLIContext struct { Codec *codec.Codec AccDecoder auth.AccountDecoder Client rpcclient.Client + Keybase cryptokeys.Keybase Output io.Writer OutputFormat string Height int64 @@ -45,10 +47,11 @@ type CLIContext struct { Async bool PrintResponse bool Verifier tmlite.Verifier + VerifierHome string Simulate bool GenerateOnly bool - fromAddress types.AccAddress - fromName string + FromAddress sdk.AccAddress + FromName string Indent bool } @@ -63,11 +66,16 @@ func NewCLIContext() CLIContext { } from := viper.GetString(client.FlagFrom) - fromAddress, fromName := fromFields(from) + fromAddress, fromName, err := GetFromFields(from) + if err != nil { + fmt.Printf("failed to get from fields: %v", err) + os.Exit(1) + } // We need to use a single verifier for all contexts - if verifier == nil { + if verifier == nil || verifierHome != viper.GetString(cli.HomeFlag) { verifier = createVerifier() + verifierHome = viper.GetString(cli.HomeFlag) } return CLIContext{ @@ -85,8 +93,8 @@ func NewCLIContext() CLIContext { Verifier: verifier, Simulate: viper.GetBool(client.FlagDryRun), GenerateOnly: viper.GetBool(client.FlagGenerateOnly), - fromAddress: fromAddress, - fromName: fromName, + FromAddress: fromAddress, + FromName: fromName, Indent: viper.GetBool(client.FlagIndentResponse), } } @@ -137,37 +145,6 @@ func createVerifier() tmlite.Verifier { return verifier } -func fromFields(from string) (fromAddr types.AccAddress, fromName string) { - if from == "" { - return nil, "" - } - - keybase, err := keys.GetKeyBase() - if err != nil { - fmt.Println("no keybase found") - os.Exit(1) - } - - var info cskeys.Info - if addr, err := types.AccAddressFromBech32(from); err == nil { - info, err = keybase.GetByAddress(addr) - if err != nil { - fmt.Printf("could not find key %s\n", from) - os.Exit(1) - } - } else { - info, err = keybase.Get(from) - if err != nil { - fmt.Printf("could not find key %s\n", from) - os.Exit(1) - } - } - - fromAddr = info.GetAddress() - fromName = info.GetName() - return -} - // WithCodec returns a copy of the context with an updated codec. func (ctx CLIContext) WithCodec(cdc *codec.Codec) CLIContext { ctx.Codec = cdc @@ -255,6 +232,19 @@ func (ctx CLIContext) WithSimulation(simulate bool) CLIContext { return ctx } +// WithFromName returns a copy of the context with an updated from account name. +func (ctx CLIContext) WithFromName(name string) CLIContext { + ctx.FromName = name + return ctx +} + +// WithFromAddress returns a copy of the context with an updated from account +// address. +func (ctx CLIContext) WithFromAddress(addr sdk.AccAddress) CLIContext { + ctx.FromAddress = addr + return ctx +} + // PrintOutput prints output while respecting output and indent flags // NOTE: pass in marshalled structs that have been unmarshaled // because this function will panic on marshaling errors @@ -279,3 +269,31 @@ func (ctx CLIContext) PrintOutput(toPrint fmt.Stringer) (err error) { fmt.Println(string(out)) return } + +// GetFromFields returns a from account address and Keybase name given either +// an address or key name. +func GetFromFields(from string) (sdk.AccAddress, string, error) { + if from == "" { + return nil, "", nil + } + + keybase, err := keys.NewKeyBaseFromHomeFlag() + if err != nil { + return nil, "", err + } + + var info cryptokeys.Info + if addr, err := sdk.AccAddressFromBech32(from); err == nil { + info, err = keybase.GetByAddress(addr) + if err != nil { + return nil, "", err + } + } else { + info, err = keybase.Get(from) + if err != nil { + return nil, "", err + } + } + + return info.GetAddress(), info.GetName(), nil +} diff --git a/client/context/errors.go b/client/context/errors.go index f28454ca0..f8f6b6324 100644 --- a/client/context/errors.go +++ b/client/context/errors.go @@ -1,7 +1,7 @@ package context import ( - "github.com/pkg/errors" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -9,7 +9,7 @@ import ( // ErrInvalidAccount returns a standardized error reflecting that a given // account address does not exist. func ErrInvalidAccount(addr sdk.AccAddress) error { - return errors.Errorf(`No account with address %s was found in the state. + return fmt.Errorf(`No account with address %s was found in the state. Are you sure there has been a transaction involving it?`, addr) } @@ -17,6 +17,6 @@ Are you sure there has been a transaction involving it?`, addr) // height can't be verified. The reason is that the base checkpoint of the certifier is // newer than the given height func ErrVerifyCommit(height int64) error { - return errors.Errorf(`The height of base truststore in gaia-lite is higher than height %d. + return fmt.Errorf(`The height of base truststore in gaia-lite is higher than height %d. Can't verify blockchain proof at this height. Please set --trust-node to true and try again`, height) } diff --git a/client/context/query.go b/client/context/query.go index aba8df190..a2fc7ff78 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -18,7 +18,7 @@ import ( rpcclient "github.com/tendermint/tendermint/rpc/client" tmtypes "github.com/tendermint/tendermint/types" - "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/store/rootmulti" ) // GetNode returns an RPC client. If the context's client is not defined, an @@ -82,13 +82,13 @@ func (ctx CLIContext) GetAccount(address []byte) (auth.Account, error) { } // GetFromAddress returns the from address from the context's name. -func (ctx CLIContext) GetFromAddress() (sdk.AccAddress, error) { - return ctx.fromAddress, nil +func (ctx CLIContext) GetFromAddress() sdk.AccAddress { + return ctx.FromAddress } // GetFromName returns the key name for the current context. -func (ctx CLIContext) GetFromName() (string, error) { - return ctx.fromName, nil +func (ctx CLIContext) GetFromName() string { + return ctx.FromName } // GetAccountNumber returns the next account number for the given account @@ -116,11 +116,7 @@ func (ctx CLIContext) GetAccountSequence(address []byte) (uint64, error) { // EnsureAccountExists ensures that an account exists for a given context. An // error is returned if it does not. func (ctx CLIContext) EnsureAccountExists() error { - addr, err := ctx.GetFromAddress() - if err != nil { - return err - } - + addr := ctx.GetFromAddress() accountBytes, err := ctx.QueryStore(auth.AddressStoreKey(addr), ctx.AccountStore) if err != nil { return err @@ -169,7 +165,7 @@ func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, err erro resp := result.Response if !resp.IsOK() { - return res, errors.Errorf(resp.Log) + return res, errors.New(resp.Log) } // data from trusted node or subspace query doesn't need verification @@ -211,7 +207,7 @@ func (ctx CLIContext) verifyProof(queryPath string, resp abci.ResponseQuery) err } // TODO: Instead of reconstructing, stash on CLIContext field? - prt := store.DefaultProofRuntime() + prt := rootmulti.DefaultProofRuntime() // TODO: Better convention for path? storeName, err := parseQueryStorePath(queryPath) @@ -223,6 +219,13 @@ func (ctx CLIContext) verifyProof(queryPath string, resp abci.ResponseQuery) err kp = kp.AppendKey([]byte(storeName), merkle.KeyEncodingURL) kp = kp.AppendKey(resp.Key, merkle.KeyEncodingURL) + if resp.Value == nil { + err = prt.VerifyAbsence(resp.Proof, commit.Header.AppHash, kp.String()) + if err != nil { + return errors.Wrap(err, "failed to prove merkle proof") + } + return nil + } err = prt.VerifyValue(resp.Proof, commit.Header.AppHash, kp.String(), resp.Value) if err != nil { return errors.Wrap(err, "failed to prove merkle proof") @@ -251,7 +254,7 @@ func isQueryStoreWithProof(path string) bool { return false case paths[0] != "store": return false - case store.RequireProof("/" + paths[2]): + case rootmulti.RequireProof("/" + paths[2]): return true } diff --git a/client/errors.go b/client/errors.go new file mode 100644 index 000000000..d01d57429 --- /dev/null +++ b/client/errors.go @@ -0,0 +1,9 @@ +package client + +import "errors" + +// common errors for CLI and REST clients +var ( + ErrInvalidGasAdjustment = errors.New("invalid gas adjustment") + ErrInvalidSigner = errors.New("tx intended signer does not match the given signer") +) diff --git a/client/flags.go b/client/flags.go index 9ee121c20..c87a8933c 100644 --- a/client/flags.go +++ b/client/flags.go @@ -11,9 +11,9 @@ import ( // nolint const ( - // DefaultGasAdjustment is applied to gas estimates to avoid tx - // execution failures due to state changes that might - // occur between the tx simulation and the actual run. + // DefaultGasAdjustment is applied to gas estimates to avoid tx execution + // failures due to state changes that might occur between the tx simulation + // and the actual run. DefaultGasAdjustment = 1.0 DefaultGasLimit = 200000 GasFlagAuto = "auto" @@ -40,7 +40,7 @@ const ( FlagListenAddr = "laddr" FlagCORS = "cors" FlagMaxOpenConnections = "max-open" - FlagInsecure = "insecure" + FlagTLS = "tls" FlagSSLHosts = "ssl-hosts" FlagSSLCertFile = "ssl-certfile" FlagSSLKeyFile = "ssl-keyfile" @@ -103,21 +103,15 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { // RegisterRestServerFlags registers the flags required for rest server func RegisterRestServerFlags(cmd *cobra.Command) *cobra.Command { + cmd = GetCommands(cmd)[0] cmd.Flags().String(FlagListenAddr, "tcp://localhost:1317", "The address for the server to listen on") - cmd.Flags().Bool(FlagInsecure, false, "Do not set up SSL/TLS layer") + cmd.Flags().Bool(FlagTLS, false, "Enable SSL/TLS layer") cmd.Flags().String(FlagSSLHosts, "", "Comma-separated hostnames and IPs to generate a certificate for") cmd.Flags().String(FlagSSLCertFile, "", "Path to a SSL certificate file. If not supplied, a self-signed certificate will be generated.") cmd.Flags().String(FlagSSLKeyFile, "", "Path to a key file; ignored if a certificate file is not supplied.") cmd.Flags().String(FlagCORS, "", "Set the domains that can make CORS requests (* for all)") - cmd.Flags().String(FlagChainID, "", "Chain ID of Tendermint node") - cmd.Flags().String(FlagNode, "tcp://localhost:26657", "Address of the node to connect to") cmd.Flags().Int(FlagMaxOpenConnections, 1000, "The number of maximum open connections") - cmd.Flags().Bool(FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") - cmd.Flags().Bool(FlagIndentResponse, false, "Add indent to JSON response") - viper.BindPFlag(FlagTrustNode, cmd.Flags().Lookup(FlagTrustNode)) - viper.BindPFlag(FlagChainID, cmd.Flags().Lookup(FlagChainID)) - viper.BindPFlag(FlagNode, cmd.Flags().Lookup(FlagNode)) return cmd } diff --git a/client/input.go b/client/input.go index 46c838e2e..f229d7d3b 100644 --- a/client/input.go +++ b/client/input.go @@ -6,18 +6,35 @@ import ( "os" "strings" + "errors" + "github.com/bgentry/speakeasy" "github.com/mattn/go-isatty" - "github.com/pkg/errors" ) // MinPassLength is the minimum acceptable password length const MinPassLength = 8 +var currentStdin *bufio.Reader + +func init() { + currentStdin = bufio.NewReader(os.Stdin) +} + // BufferStdin is used to allow reading prompts for stdin // multiple times, when we read from non-tty func BufferStdin() *bufio.Reader { - return bufio.NewReader(os.Stdin) + return currentStdin +} + +// OverrideStdin allows to temporarily override stdin +func OverrideStdin(newStdin *bufio.Reader) (cleanUp func()) { + prevStdin := currentStdin + currentStdin = newStdin + cleanUp = func() { + currentStdin = prevStdin + } + return cleanUp } // GetPassword will prompt for a password one-time (to sign a tx) @@ -36,18 +53,12 @@ func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { if len(pass) < MinPassLength { // Return the given password to the upstream client so it can handle a // non-STDIN failure gracefully. - return pass, errors.Errorf("password must be at least %d characters", MinPassLength) + return pass, fmt.Errorf("password must be at least %d characters", MinPassLength) } return pass, nil } -// GetSeed will request a seed phrase from stdin and trims off -// leading/trailing spaces -func GetSeed(prompt string, buf *bufio.Reader) (string, error) { - return GetString(prompt, buf) -} - // GetCheckPassword will prompt for a password twice to verify they // match (for creating a new password). // It enforces the password length. Only parses password once if diff --git a/client/keys.go b/client/keys.go index 1fc594a2a..3e0b61d46 100644 --- a/client/keys.go +++ b/client/keys.go @@ -6,18 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys" ) -// 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, - ) - return keybase -} - // MockKeyBase generates an in-memory keybase that will be discarded // useful for --dry-run to generate a seed phrase without // storing the key func MockKeyBase() keys.Keybase { - return GetKeyBase(dbm.NewMemDB()) + return keys.New(dbm.NewMemDB()) } diff --git a/client/keys/add.go b/client/keys/add.go index b3af588dc..d9dfe985f 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -9,26 +9,26 @@ import ( "os" "sort" - "github.com/tendermint/tendermint/crypto/multisig" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + + "errors" - "github.com/cosmos/go-bip39" "github.com/gorilla/mux" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/libs/cli" - "github.com/cosmos/cosmos-sdk/client" - ccrypto "github.com/cosmos/cosmos-sdk/crypto" - "github.com/cosmos/cosmos-sdk/crypto/keys" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/go-bip39" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/multisig" + "github.com/tendermint/tendermint/libs/cli" ) const ( flagInteractive = "interactive" - flagBIP44Path = "bip44-path" flagRecover = "recover" flagNoBackup = "no-backup" flagDryRun = "dry-run" @@ -38,6 +38,11 @@ const ( flagNoSort = "nosort" ) +const ( + maxValidAccountValue = int(0x80000000 - 1) + maxValidIndexalue = int(0x80000000 - 1) +) + func addKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "add ", @@ -68,7 +73,6 @@ the flag --nosort is set. cmd.Flags().String(FlagPublicKey, "", "Parse a public key in bech32 format and save it to disk") cmd.Flags().BoolP(flagInteractive, "i", false, "Interactively prompt user for BIP39 passphrase and mnemonic") cmd.Flags().Bool(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device") - cmd.Flags().String(flagBIP44Path, "44'/118'/0'/0/0", "BIP44 path from which to derive a private key") cmd.Flags().Bool(flagRecover, false, "Provide seed phrase to recover existing key instead of creating") cmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)") cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") @@ -86,7 +90,7 @@ input output - armor encrypted private key (saved to file) */ -func runAddCmd(cmd *cobra.Command, args []string) error { +func runAddCmd(_ *cobra.Command, args []string) error { var kb keys.Keybase var err error var encryptPassword string @@ -95,24 +99,25 @@ func runAddCmd(cmd *cobra.Command, args []string) error { name := args[0] interactive := viper.GetBool(flagInteractive) + showMnemonic := !viper.GetBool(flagNoBackup) if viper.GetBool(flagDryRun) { // we throw this away, so don't enforce args, // we want to get a new random seed phrase quickly kb = client.MockKeyBase() - encryptPassword = "throwing-this-key-away" + encryptPassword = app.DefaultKeyPass } else { - kb, err = GetKeyBaseWithWritePerm() + kb, err = NewKeyBaseFromHomeFlag() if err != nil { return err } - _, err := kb.Get(name) + _, err = kb.Get(name) if err == nil { // account exists, ask for user confirmation - if response, err := client.GetConfirmation( - fmt.Sprintf("override the existing name %s", name), buf); err != nil || !response { - return err + if response, err2 := client.GetConfirmation( + fmt.Sprintf("override the existing name %s", name), buf); err2 != nil || !response { + return err2 } } @@ -144,6 +149,7 @@ func runAddCmd(cmd *cobra.Command, args []string) error { if _, err := kb.CreateOffline(name, pk); err != nil { return err } + fmt.Fprintf(os.Stderr, "Key %q saved to disk.", name) return nil } @@ -164,51 +170,37 @@ func runAddCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - kb.CreateOffline(name, pk) + _, err = kb.CreateOffline(name, pk) + if err != nil { + return err + } return nil } - bipFlag := cmd.Flags().Lookup(flagBIP44Path) - bip44Params, err := getBIP44ParamsAndPath(bipFlag.Value.String(), bipFlag.Changed || !interactive) - if err != nil { - return err - } + account := uint32(viper.GetInt(flagAccount)) + index := uint32(viper.GetInt(flagIndex)) - // If we're using ledger, only thing we need is the path. So generate key and - // we're done. + // If we're using ledger, only thing we need is the path. So generate key and we're done. if viper.GetBool(client.FlagUseLedger) { - account := uint32(viper.GetInt(flagAccount)) - index := uint32(viper.GetInt(flagIndex)) - path := ccrypto.DerivationPath{44, 118, account, 0, index} - info, err := kb.CreateLedger(name, path, keys.Secp256k1) + info, err := kb.CreateLedger(name, keys.Secp256k1, account, index) if err != nil { return err } - printCreate(info, "") - return nil - } - - // Recover key from seed passphrase - if viper.GetBool(flagRecover) { - seed, err := client.GetSeed( - "Enter your recovery seed phrase:", buf) - if err != nil { - return err - } - info, err := kb.CreateKey(name, seed, encryptPassword) - if err != nil { - return err - } - // print out results without the seed phrase - viper.Set(flagNoBackup, true) - printCreate(info, "") - return nil + return printCreate(info, false, "") } + // Get bip39 mnemonic var mnemonic string - if interactive { - mnemonic, err = client.GetString("Enter your bip39 mnemonic, or hit enter to generate one.", buf) + var bip39Passphrase string + + if interactive || viper.GetBool(flagRecover) { + bip39Message := "Enter your bip39 mnemonic" + if !viper.GetBool(flagRecover) { + bip39Message = "Enter your bip39 mnemonic, or hit enter to generate one." + } + + mnemonic, err = client.GetString(bip39Message, buf) if err != nil { return err } @@ -227,8 +219,12 @@ func runAddCmd(cmd *cobra.Command, args []string) error { } } - // get bip39 passphrase - var bip39Passphrase string + if !bip39.IsMnemonicValid(mnemonic) { + fmt.Fprintf(os.Stderr, "Error: Mnemonic is not valid") + return nil + } + + // override bip39 passphrase if interactive { bip39Passphrase, err = client.GetString( "Enter your bip39 passphrase. This is combined with the mnemonic to derive the seed. "+ @@ -250,173 +246,158 @@ func runAddCmd(cmd *cobra.Command, args []string) error { } } - info, err := kb.Derive(name, mnemonic, bip39Passphrase, encryptPassword, *bip44Params) + info, err := kb.CreateAccount(name, mnemonic, keys.DefaultBIP39Passphrase, encryptPassword, account, index) if err != nil { return err } - printCreate(info, mnemonic) - return nil -} -func getBIP44ParamsAndPath(path string, flagSet bool) (*hd.BIP44Params, error) { - buf := client.BufferStdin() - bip44Path := path - - // if it wasn't set in the flag, give it a chance to overide interactively - if !flagSet { - var err error - - bip44Path, err = client.GetString(fmt.Sprintf("Enter your bip44 path. Default is %s\n", path), buf) - if err != nil { - return nil, err - } - - if len(bip44Path) == 0 { - bip44Path = path - } + // Recover key from seed passphrase + if viper.GetBool(flagRecover) { + // Hide mnemonic from output + showMnemonic = false + mnemonic = "" } - bip44params, err := hd.NewParamsFromPath(bip44Path) - if err != nil { - return nil, err - } - - return bip44params, nil + return printCreate(info, showMnemonic, mnemonic) } -func printCreate(info keys.Info, seed string) { +func printCreate(info keys.Info, showMnemonic bool, mnemonic string) error { output := viper.Get(cli.OutputFlag) + switch output { - case "text": - fmt.Fprintln(os.Stderr, "") + case OutputFormatText: + fmt.Fprintln(os.Stderr) printKeyInfo(info, Bech32KeyOutput) - // print seed unless requested not to. - if !viper.GetBool(client.FlagUseLedger) && !viper.GetBool(flagNoBackup) { - fmt.Fprintln(os.Stderr, "\n**Important** write this seed phrase in a safe place.") + // print mnemonic unless requested not to. + if showMnemonic { + fmt.Fprintln(os.Stderr, "\n**Important** write this mnemonic phrase in a safe place.") fmt.Fprintln(os.Stderr, "It is the only way to recover your account if you ever forget your password.") - fmt.Fprintln(os.Stderr) - fmt.Fprintln(os.Stderr, seed) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, mnemonic) } - case "json": + case OutputFormatJSON: out, err := Bech32KeyOutput(info) if err != nil { - panic(err) + return err } - if !viper.GetBool(flagNoBackup) { - out.Seed = seed + + if showMnemonic { + out.Mnemonic = mnemonic } + var jsonString []byte if viper.GetBool(client.FlagIndentResponse) { jsonString, err = cdc.MarshalJSONIndent(out, "", " ") } else { jsonString, err = cdc.MarshalJSON(out) } + if err != nil { - panic(err) // really shouldn't happen... + return err } fmt.Fprintln(os.Stderr, string(jsonString)) default: - panic(fmt.Sprintf("I can't speak: %s", output)) + return fmt.Errorf("I can't speak: %s", output) } -} -// function to just a new seed to display in the UI before actually persisting it in the keybase -func getSeed(algo keys.SigningAlgo) string { - kb := client.MockKeyBase() - pass := "throwing-this-key-away" - name := "inmemorykey" - _, seed, _ := kb.CreateMnemonic(name, keys.English, pass, algo) - return seed -} - -func printPrefixed(msg string) { - fmt.Fprintln(os.Stderr, msg) -} - -func printStep() { - printPrefixed("-------------------------------------") + return nil } ///////////////////////////// // REST -// new key request REST body -type NewKeyBody struct { - Name string `json:"name"` - Password string `json:"password"` - Seed string `json:"seed"` +// function to just create a new seed to display in the UI before actually persisting it in the keybase +func generateMnemonic(algo keys.SigningAlgo) string { + kb := client.MockKeyBase() + pass := app.DefaultKeyPass + name := "inmemorykey" + _, seed, _ := kb.CreateMnemonic(name, keys.English, pass, algo) + return seed +} + +// CheckAndWriteErrorResponse will check for errors and return +// a given error message when corresponding +//TODO: Move to utils/rest or similar +func CheckAndWriteErrorResponse(w http.ResponseWriter, httpErr int, err error) bool { + if err != nil { + w.WriteHeader(httpErr) + _, _ = w.Write([]byte(err.Error())) + return true + } + return false } // add new key REST handler func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var kb keys.Keybase - var m NewKeyBody + var m AddNewKey - kb, err := GetKeyBaseWithWritePerm() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + kb, err := NewKeyBaseFromHomeFlag() + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } body, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + err = json.Unmarshal(body, &m) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + + // Check parameters if m.Name == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingName() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingName()) return } if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingPassword() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingPassword()) return } - // check if already exists - infos, err := kb.List() - for _, info := range infos { - if info.GetName() == m.Name { - w.WriteHeader(http.StatusConflict) - err = errKeyNameConflict(m.Name) - w.Write([]byte(err.Error())) - return - } + mnemonic := m.Mnemonic + // if mnemonic is empty, generate one + if mnemonic == "" { + mnemonic = generateMnemonic(keys.Secp256k1) + } + if !bip39.IsMnemonicValid(mnemonic) { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidMnemonic()) + } + + if m.Account < 0 || m.Account > maxValidAccountValue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidAccountNumber()) + return + } + + if m.Index < 0 || m.Index > maxValidIndexalue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidIndexNumber()) + return + } + + _, err = kb.Get(m.Name) + if err == nil { + CheckAndWriteErrorResponse(w, http.StatusConflict, errKeyNameConflict(m.Name)) + return } // create account - seed := m.Seed - if seed == "" { - seed = getSeed(keys.Secp256k1) - } - info, err := kb.CreateKey(m.Name, seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + account := uint32(m.Account) + index := uint32(m.Index) + info, err := kb.CreateAccount(m.Name, mnemonic, keys.DefaultBIP39Passphrase, m.Password, account, index) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } - keyOutput.Seed = seed + keyOutput.Mnemonic = mnemonic PostProcessResponse(w, cdc, keyOutput, indent) } @@ -426,22 +407,17 @@ func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) algoType := vars["type"] + // algo type defaults to secp256k1 if algoType == "" { algoType = "secp256k1" } - algo := keys.SigningAlgo(algoType) - seed := getSeed(algo) + algo := keys.SigningAlgo(algoType) + seed := generateMnemonic(algo) w.Header().Set("Content-Type", "application/json") - w.Write([]byte(seed)) -} - -// RecoverKeyBody is recover key request REST body -type RecoverKeyBody struct { - Password string `json:"password"` - Seed string `json:"seed"` + _, _ = w.Write([]byte(seed)) } // RecoverRequestHandler performs key recover request @@ -449,67 +425,66 @@ func RecoverRequestHandler(indent bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] - var m RecoverKeyBody + var m RecoverKey + body, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - return - } - err = cdc.UnmarshalJSON(body, &m) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + err = cdc.UnmarshalJSON(body, &m) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { + return + } + + kb, err := NewKeyBaseFromHomeFlag() + CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) + if name == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingName() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingName()) return } if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingPassword() - w.Write([]byte(err.Error())) - return - } - if m.Seed == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingSeed() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingPassword()) return } - kb, err := GetKeyBaseWithWritePerm() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - // check if already exists - infos, err := kb.List() - for _, info := range infos { - if info.GetName() == name { - w.WriteHeader(http.StatusConflict) - err = errKeyNameConflict(name) - w.Write([]byte(err.Error())) - return - } + mnemonic := m.Mnemonic + if !bip39.IsMnemonicValid(mnemonic) { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidMnemonic()) } - info, err := kb.CreateKey(name, m.Seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if m.Mnemonic == "" { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingMnemonic()) + return + } + + if m.Account < 0 || m.Account > maxValidAccountValue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidAccountNumber()) + return + } + + if m.Index < 0 || m.Index > maxValidIndexalue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidIndexNumber()) + return + } + + _, err = kb.Get(name) + if err == nil { + CheckAndWriteErrorResponse(w, http.StatusConflict, errKeyNameConflict(name)) + return + } + + account := uint32(m.Account) + index := uint32(m.Index) + + info, err := kb.CreateAccount(name, mnemonic, keys.DefaultBIP39Passphrase, m.Password, account, index) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } diff --git a/client/keys/add_test.go b/client/keys/add_test.go new file mode 100644 index 000000000..f20ba27b1 --- /dev/null +++ b/client/keys/add_test.go @@ -0,0 +1,103 @@ +package keys + +import ( + "bufio" + "net/http" + "strings" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/tests" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/stretchr/testify/assert" +) + +func Test_runAddCmdBasic(t *testing.T) { + cmd := addKeyCommand() + assert.NotNil(t, cmd) + + // Missing input (enter password) + err := runAddCmd(cmd, []string{"keyname"}) + assert.EqualError(t, err, "EOF") + + // Prepare a keybase + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(cli.HomeFlag, kbHome) + + /// Test Text + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp1 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\ntest1234\n"))) + defer cleanUp1() + err = runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + /// Test Text - Replace? >> FAIL + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\ntest1234\n"))) + defer cleanUp2() + err = runAddCmd(cmd, []string{"keyname1"}) + assert.Error(t, err) + + /// Test Text - Replace? Answer >> PASS + viper.Set(cli.OutputFlag, OutputFormatText) + // Now enter password + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\ntest1234\ntest1234\n"))) + defer cleanUp3() + err = runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + // Check JSON + viper.Set(cli.OutputFlag, OutputFormatJSON) + // Now enter password + cleanUp4 := client.OverrideStdin(bufio.NewReader(strings.NewReader("test1234\ntest1234\n"))) + defer cleanUp4() + err = runAddCmd(cmd, []string{"keyname2"}) + assert.NoError(t, err) +} + +type MockResponseWriter struct { + dataHeaderStatus int + dataBody []byte +} + +func (MockResponseWriter) Header() http.Header { + panic("Unexpected call!") +} + +func (w *MockResponseWriter) Write(data []byte) (int, error) { + w.dataBody = append(w.dataBody, data...) + return len(data), nil +} + +func (w *MockResponseWriter) WriteHeader(statusCode int) { + w.dataHeaderStatus = statusCode +} + +func TestCheckAndWriteErrorResponse(t *testing.T) { + mockRW := MockResponseWriter{} + + mockRW.WriteHeader(100) + assert.Equal(t, 100, mockRW.dataHeaderStatus) + + detected := CheckAndWriteErrorResponse(&mockRW, http.StatusBadRequest, errors.New("some ERROR")) + require.True(t, detected) + require.Equal(t, http.StatusBadRequest, mockRW.dataHeaderStatus) + require.Equal(t, "some ERROR", string(mockRW.dataBody[:])) + + mockRW = MockResponseWriter{} + detected = CheckAndWriteErrorResponse(&mockRW, http.StatusBadRequest, nil) + require.False(t, detected) + require.Equal(t, 0, mockRW.dataHeaderStatus) + require.Equal(t, "", string(mockRW.dataBody[:])) +} diff --git a/client/keys/codec_test.go b/client/keys/codec_test.go new file mode 100644 index 000000000..3af946e59 --- /dev/null +++ b/client/keys/codec_test.go @@ -0,0 +1,100 @@ +package keys + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +type testCases struct { + Keys []KeyOutput + Answers []KeyOutput + JSON [][]byte +} + +func getTestCases() testCases { + return testCases{ + []KeyOutput{ + {"A", "B", "C", "D", "E"}, + {"A", "B", "C", "D", ""}, + {"", "B", "C", "D", ""}, + {"", "", "", "", ""}, + }, + make([]KeyOutput, 4), + [][]byte{ + []byte(`{"name":"A","type":"B","address":"C","pub_key":"D","mnemonic":"E"}`), + []byte(`{"name":"A","type":"B","address":"C","pub_key":"D"}`), + []byte(`{"name":"","type":"B","address":"C","pub_key":"D"}`), + []byte(`{"name":"","type":"","address":"","pub_key":""}`), + }, + } +} + +func TestMarshalJSON(t *testing.T) { + type args struct { + o KeyOutput + } + + data := getTestCases() + + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + {"basic", args{data.Keys[0]}, []byte(data.JSON[0]), false}, + {"mnemonic is optional", args{data.Keys[1]}, []byte(data.JSON[1]), false}, + + // REVIEW: Are the next results expected?? + {"empty name", args{data.Keys[2]}, []byte(data.JSON[2]), false}, + {"empty object", args{data.Keys[3]}, []byte(data.JSON[3]), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MarshalJSON(tt.args.o) + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + fmt.Printf("%s\n", got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnmarshalJSON(t *testing.T) { + type args struct { + bz []byte + ptr interface{} + } + + data := getTestCases() + + tests := []struct { + name string + args args + wantErr bool + }{ + {"basic", args{data.JSON[0], &data.Answers[0]}, false}, + {"mnemonic is optional", args{data.JSON[1], &data.Answers[1]}, false}, + + // REVIEW: Are the next results expected?? + {"empty name", args{data.JSON[2], &data.Answers[2]}, false}, + {"empty object", args{data.JSON[3], &data.Answers[3]}, false}, + } + for idx, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UnmarshalJSON(tt.args.bz, tt.args.ptr); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + + // Confirm deserialized objects are the same + require.Equal(t, data.Keys[idx], data.Answers[idx]) + }) + } +} diff --git a/client/keys/delete.go b/client/keys/delete.go index 5f3ff4f09..614bcce7c 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -13,8 +13,8 @@ import ( "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client" - keys "github.com/cosmos/cosmos-sdk/crypto/keys" - keyerror "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" "github.com/spf13/cobra" ) @@ -49,7 +49,7 @@ gaiacli. func runDeleteCmd(cmd *cobra.Command, args []string) error { name := args[0] - kb, err := GetKeyBaseWithWritePerm() + kb, err := NewKeyBaseFromHomeFlag() if err != nil { return err } @@ -91,6 +91,17 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { return nil } +func confirmDeletion(buf *bufio.Reader) error { + answer, err := client.GetConfirmation("Key reference will be deleted. Continue?", buf) + if err != nil { + return err + } + if !answer { + return errors.New("aborted") + } + return nil +} + //////////////////////// // REST @@ -110,42 +121,31 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { err := decoder.Decode(&m) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } - kb, err = GetKeyBaseWithWritePerm() + kb, err = NewKeyBaseFromHomeFlag() if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } err = kb.Delete(name, m.Password, false) if keyerror.IsErrKeyNotFound(err) { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } else if keyerror.IsErrWrongPassword(err) { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } else if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } w.WriteHeader(http.StatusOK) } - -func confirmDeletion(buf *bufio.Reader) error { - answer, err := client.GetConfirmation("Key reference will be deleted. Continue?", buf) - if err != nil { - return err - } - if !answer { - return errors.New("aborted") - } - return nil -} diff --git a/client/keys/delete_test.go b/client/keys/delete_test.go new file mode 100644 index 000000000..ad7b5367f --- /dev/null +++ b/client/keys/delete_test.go @@ -0,0 +1,102 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/cli" +) + +func Test_runDeleteCmd(t *testing.T) { + deleteKeyCommand := deleteKeyCommand() + + yesF, _ := deleteKeyCommand.Flags().GetBool(flagYes) + forceF, _ := deleteKeyCommand.Flags().GetBool(flagForce) + assert.False(t, yesF) + assert.False(t, forceF) + + fakeKeyName1 := "runDeleteCmd_Key1" + fakeKeyName2 := "runDeleteCmd_Key2" + + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(cli.HomeFlag, kbHome) + + // Now + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + + err = runDeleteCmd(deleteKeyCommand, []string{"blah"}) + require.Error(t, err) + require.Equal(t, "Key blah not found", err.Error()) + + // User confirmation missing + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName1}) + require.Error(t, err) + require.Equal(t, "EOF", err.Error()) + + { + _, err = kb.Get(fakeKeyName1) + require.NoError(t, err) + + // Now there is a confirmation + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("y\n"))) + defer cleanUp() + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName1}) + require.NoError(t, err) + + _, err = kb.Get(fakeKeyName1) + require.Error(t, err) // Key1 is gone + } + + viper.Set(flagYes, true) + _, err = kb.Get(fakeKeyName2) + require.NoError(t, err) + err = runDeleteCmd(deleteKeyCommand, []string{fakeKeyName2}) + require.NoError(t, err) + _, err = kb.Get(fakeKeyName2) + require.Error(t, err) // Key2 is gone + + // TODO: Write another case for !keys.Local +} + +func Test_confirmDeletion(t *testing.T) { + type args struct { + buf *bufio.Reader + } + + answerYes := bufio.NewReader(strings.NewReader("y\n")) + answerYes2 := bufio.NewReader(strings.NewReader("Y\n")) + answerNo := bufio.NewReader(strings.NewReader("n\n")) + answerInvalid := bufio.NewReader(strings.NewReader("245\n")) + + tests := []struct { + name string + args args + wantErr bool + }{ + {"Y", args{answerYes}, false}, + {"y", args{answerYes2}, false}, + {"N", args{answerNo}, true}, + {"BAD", args{answerInvalid}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := confirmDeletion(tt.args.buf); (err != nil) != tt.wantErr { + t.Errorf("confirmDeletion() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/client/keys/errors.go b/client/keys/errors.go index 9c6139d7a..a603b1d1a 100644 --- a/client/keys/errors.go +++ b/client/keys/errors.go @@ -3,7 +3,7 @@ package keys import "fmt" func errKeyNameConflict(name string) error { - return fmt.Errorf("acount with name %s already exists", name) + return fmt.Errorf("account with name %s already exists", name) } func errMissingName() error { @@ -14,6 +14,18 @@ func errMissingPassword() error { return fmt.Errorf("you have to specify a password for the locally stored account") } -func errMissingSeed() error { - return fmt.Errorf("you have to specify seed for key recover") +func errMissingMnemonic() error { + return fmt.Errorf("you have to specify a mnemonic for key recovery") +} + +func errInvalidMnemonic() error { + return fmt.Errorf("the mnemonic is invalid") +} + +func errInvalidAccountNumber() error { + return fmt.Errorf("the account number is invalid") +} + +func errInvalidIndexNumber() error { + return fmt.Errorf("the index number is invalid") } diff --git a/client/keys/keys/keys.db/LOCK b/client/keys/keys/keys.db/LOCK new file mode 100644 index 000000000..e69de29bb diff --git a/client/keys/list.go b/client/keys/list.go index 9d01d500b..81a45d0b7 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -6,19 +6,18 @@ import ( "github.com/spf13/cobra" ) -// CMD - -// listKeysCmd represents the list command -var listKeysCmd = &cobra.Command{ - Use: "list", - Short: "List all keys", - Long: `Return a list of all public keys stored by this key manager +func listKeysCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all keys", + Long: `Return a list of all public keys stored by this key manager along with their associated name and address.`, - RunE: runListCmd, + RunE: runListCmd, + } } func runListCmd(cmd *cobra.Command, args []string) error { - kb, err := GetKeyBase() + kb, err := NewKeyBaseFromHomeFlag() if err != nil { return err } @@ -36,16 +35,16 @@ func runListCmd(cmd *cobra.Command, args []string) error { // query key list REST handler func QueryKeysRequestHandler(indent bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - kb, err := GetKeyBase() + kb, err := NewKeyBaseFromHomeFlag() if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } infos, err := kb.List() if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } // an empty list will be JSONized as null, but we want to keep the empty list @@ -56,7 +55,7 @@ func QueryKeysRequestHandler(indent bool) http.HandlerFunc { keysOutput, err := Bech32KeysOutput(infos) if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } PostProcessResponse(w, cdc, keysOutput, indent) diff --git a/client/keys/list_test.go b/client/keys/list_test.go new file mode 100644 index 000000000..9fdab6a75 --- /dev/null +++ b/client/keys/list_test.go @@ -0,0 +1,55 @@ +package keys + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/stretchr/testify/assert" + + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/spf13/cobra" +) + +func Test_runListCmd(t *testing.T) { + type args struct { + cmd *cobra.Command + args []string + } + + cmdBasic := listKeysCmd() + + // Prepare some keybases + kbHome1, cleanUp1 := tests.NewTestCaseDir(t) + defer cleanUp1() + // Do nothing, leave home1 empty + + kbHome2, cleanUp2 := tests.NewTestCaseDir(t) + defer cleanUp2() + viper.Set(cli.HomeFlag, kbHome2) + + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount("something", tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + + testData := []struct { + name string + kbDir string + args args + wantErr bool + }{ + {"invalid keybase", "/dev/null", args{cmdBasic, []string{}}, true}, + {"keybase: empty", kbHome1, args{cmdBasic, []string{}}, false}, + {"keybase: w/key", kbHome2, args{cmdBasic, []string{}}, false}, + } + for _, tt := range testData { + t.Run(tt.name, func(t *testing.T) { + viper.Set(cli.HomeFlag, tt.kbDir) + if err := runListCmd(tt.args.cmd, tt.args.args); (err != nil) != tt.wantErr { + t.Errorf("runListCmd() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/client/keys/mnemonic.go b/client/keys/mnemonic.go index 32a6856bb..b9d434d2b 100644 --- a/client/keys/mnemonic.go +++ b/client/keys/mnemonic.go @@ -4,11 +4,10 @@ import ( "crypto/sha256" "fmt" + bip39 "github.com/bartekn/go-bip39" "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" - - bip39 "github.com/bartekn/go-bip39" ) const ( @@ -45,9 +44,7 @@ func runMnemonicCmd(cmd *cobra.Command, args []string) error { if len(inputEntropy) < 43 { return fmt.Errorf("256-bits is 43 characters in Base-64, and 100 in Base-6. You entered %v, and probably want more", len(inputEntropy)) } - conf, err := client.GetConfirmation( - fmt.Sprintf("> Input length: %d", len(inputEntropy)), - buf) + conf, err := client.GetConfirmation(fmt.Sprintf("> Input length: %d", len(inputEntropy)), buf) if err != nil { return err } @@ -58,7 +55,6 @@ func runMnemonicCmd(cmd *cobra.Command, args []string) error { // hash input entropy to get entropy seed hashedEntropy := sha256.Sum256([]byte(inputEntropy)) entropySeed = hashedEntropy[:] - printStep() } else { // read entropy seed straight from crypto.Rand var err error diff --git a/client/keys/mnemonic_test.go b/client/keys/mnemonic_test.go new file mode 100644 index 000000000..64fd404a1 --- /dev/null +++ b/client/keys/mnemonic_test.go @@ -0,0 +1,59 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func Test_RunMnemonicCmdNormal(t *testing.T) { + cmdBasic := mnemonicKeyCommand() + err := runMnemonicCmd(cmdBasic, []string{}) + require.NoError(t, err) +} + +func Test_RunMnemonicCmdUser(t *testing.T) { + cmdUser := mnemonicKeyCommand() + err := cmdUser.Flags().Set(flagUserEntropy, "1") + assert.NoError(t, err) + + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + require.Equal(t, "EOF", err.Error()) + + // Try again + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("Hi!\n"))) + defer cleanUp() + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + require.Equal(t, + "256-bits is 43 characters in Base-64, and 100 in Base-6. You entered 3, and probably want more", + err.Error()) + + // Now provide "good" entropy :) + fakeEntropy := strings.Repeat(":)", 40) + "\ny\n" // entropy + accept count + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp2() + err = runMnemonicCmd(cmdUser, []string{}) + require.NoError(t, err) + + // Now provide "good" entropy but no answer + fakeEntropy = strings.Repeat(":)", 40) + "\n" // entropy + accept count + cleanUp3 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp3() + err = runMnemonicCmd(cmdUser, []string{}) + require.Error(t, err) + + // Now provide "good" entropy but say no + fakeEntropy = strings.Repeat(":)", 40) + "\nn\n" // entropy + accept count + cleanUp4 := client.OverrideStdin(bufio.NewReader(strings.NewReader(fakeEntropy))) + defer cleanUp4() + err = runMnemonicCmd(cmdUser, []string{}) + require.NoError(t, err) +} diff --git a/client/keys/root.go b/client/keys/root.go index f747d84a0..5b70025fd 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -22,7 +22,7 @@ func Commands() *cobra.Command { cmd.AddCommand( mnemonicKeyCommand(), addKeyCommand(), - listKeysCmd, + listKeysCmd(), showKeysCmd(), client.LineBreak, deleteKeyCommand(), diff --git a/client/keys/root_test.go b/client/keys/root_test.go new file mode 100644 index 000000000..ef7671593 --- /dev/null +++ b/client/keys/root_test.go @@ -0,0 +1,22 @@ +package keys + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/gorilla/mux" +) + +func TestCommands(t *testing.T) { + rootCommands := Commands() + assert.NotNil(t, rootCommands) + + // Commands are registered + assert.Equal(t, 7, len(rootCommands.Commands())) +} + +func TestRegisterRoutes(t *testing.T) { + fakeRouter := mux.Router{} + RegisterRoutes(&fakeRouter, false) +} diff --git a/client/keys/show.go b/client/keys/show.go index 9583dd07a..4c1ddf216 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -8,8 +8,9 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys" + "errors" + "github.com/gorilla/mux" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/tendermint/tendermint/crypto/multisig" @@ -92,7 +93,12 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) { isShowAddr := viper.GetBool(FlagAddress) isShowPubKey := viper.GetBool(FlagPublicKey) - isOutputSet := cmd.Flag(cli.OutputFlag).Changed + + isOutputSet := false + tmp := cmd.Flag(cli.OutputFlag) + if tmp != nil { + isOutputSet = tmp.Changed + } if isShowAddr && isShowPubKey { return errors.New("cannot use both --address and --pubkey at once") @@ -160,25 +166,25 @@ func GetKeyRequestHandler(indent bool) http.HandlerFunc { bechKeyOut, err := getBechKeyOut(bechPrefix) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } info, err := GetKeyInfo(name) if keyerror.IsErrKeyNotFound(err) { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } else if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } keyOutput, err := bechKeyOut(info) if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } diff --git a/client/keys/show_test.go b/client/keys/show_test.go new file mode 100644 index 000000000..21d3048ba --- /dev/null +++ b/client/keys/show_test.go @@ -0,0 +1,145 @@ +package keys + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/tendermint/tendermint/crypto/secp256k1" + + "github.com/stretchr/testify/assert" + + "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +func Test_multiSigKey_Properties(t *testing.T) { + tmpKey1 := secp256k1.GenPrivKeySecp256k1([]byte("mySecret")) + + tmp := multiSigKey{ + name: "myMultisig", + key: tmpKey1.PubKey(), + } + + assert.Equal(t, "myMultisig", tmp.GetName()) + assert.Equal(t, keys.TypeLocal, tmp.GetType()) + assert.Equal(t, "015ABFFB09DB738A45745A91E8C401423ECE4016", tmp.GetPubKey().Address().String()) + assert.Equal(t, "cosmos1q9dtl7cfmdec53t5t2g733qpgglvusqk6xdntl", tmp.GetAddress().String()) +} + +func Test_showKeysCmd(t *testing.T) { + cmd := showKeysCmd() + assert.NotNil(t, cmd) + assert.Equal(t, "false", cmd.Flag(FlagAddress).DefValue) + assert.Equal(t, "false", cmd.Flag(FlagPublicKey).DefValue) +} + +func Test_runShowCmd(t *testing.T) { + cmd := showKeysCmd() + + err := runShowCmd(cmd, []string{"invalid"}) + assert.EqualError(t, err, "Key invalid not found") + + err = runShowCmd(cmd, []string{"invalid1", "invalid2"}) + assert.EqualError(t, err, "Key invalid1 not found") + + // Prepare a key base + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(cli.HomeFlag, kbHome) + + fakeKeyName1 := "runShowCmd_Key1" + fakeKeyName2 := "runShowCmd_Key2" + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + + // Now try single key + err = runShowCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "invalid Bech32 prefix encoding provided: ") + + // Now try single key - set bech to acc + viper.Set(FlagBechPrefix, "acc") + err = runShowCmd(cmd, []string{fakeKeyName1}) + assert.NoError(t, err) + + // Now try multisig key - set bech to acc + viper.Set(FlagBechPrefix, "acc") + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.EqualError(t, err, "threshold must be a positive integer") + + // Now try multisig key - set bech to acc + threshold=2 + viper.Set(FlagBechPrefix, "acc") + viper.Set(flagMultiSigThreshold, 2) + err = runShowCmd(cmd, []string{fakeKeyName1, fakeKeyName2}) + assert.NoError(t, err) + + // TODO: Capture stdout and compare +} + +func Test_validateMultisigThreshold(t *testing.T) { + type args struct { + k int + nKeys int + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"zeros", args{0, 0}, true}, + {"1-0", args{1, 0}, true}, + {"1-1", args{1, 1}, false}, + {"1-2", args{1, 1}, false}, + {"1-2", args{2, 1}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateMultisigThreshold(tt.args.k, tt.args.nKeys); (err != nil) != tt.wantErr { + t.Errorf("validateMultisigThreshold() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_getBechKeyOut(t *testing.T) { + type args struct { + bechPrefix string + } + tests := []struct { + name string + args args + want bechKeyOutFn + wantErr bool + }{ + {"empty", args{""}, nil, true}, + {"wrong", args{"???"}, nil, true}, + {"acc", args{"acc"}, Bech32KeyOutput, false}, + {"val", args{"val"}, Bech32ValKeyOutput, false}, + {"cons", args{"cons"}, Bech32ConsKeyOutput, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getBechKeyOut(tt.args.bechPrefix) + if (err != nil) != tt.wantErr { + t.Errorf("getBechKeyOut() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + assert.NotNil(t, got) + } + + // TODO: Still not possible to compare functions + // Maybe in next release: https://github.com/stretchr/testify/issues/182 + //if &got != &tt.want { + // t.Errorf("getBechKeyOut() = %v, want %v", got, tt.want) + //} + }) + } +} diff --git a/client/keys/types.go b/client/keys/types.go new file mode 100644 index 000000000..d4a1032c2 --- /dev/null +++ b/client/keys/types.go @@ -0,0 +1,38 @@ +package keys + +// used for outputting keys.Info over REST +type KeyOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Address string `json:"address"` + PubKey string `json:"pub_key"` + Mnemonic string `json:"mnemonic,omitempty"` +} + +// AddNewKey request a new key +type AddNewKey struct { + Name string `json:"name"` + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// RecoverKeyBody recovers a key +type RecoverKey struct { + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// UpdateKeyReq requests updating a key +type UpdateKeyReq struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +// DeleteKeyReq requests deleting a key +type DeleteKeyReq struct { + Password string `json:"password"` +} diff --git a/client/keys/update.go b/client/keys/update.go index e72da240c..d3a03f22c 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client" - keys "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/spf13/cobra" @@ -29,7 +29,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { name := args[0] buf := client.BufferStdin() - kb, err := GetKeyBaseWithWritePerm() + kb, err := NewKeyBaseFromHomeFlag() if err != nil { return err } @@ -73,14 +73,14 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { err := decoder.Decode(&m) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } - kb, err = GetKeyBaseWithWritePerm() + kb, err = NewKeyBaseFromHomeFlag() if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } @@ -89,15 +89,15 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { err = kb.Update(name, m.OldPassword, getNewpass) if keyerror.IsErrKeyNotFound(err) { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } else if keyerror.IsErrWrongPassword(err) { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } else if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } diff --git a/client/keys/update_test.go b/client/keys/update_test.go new file mode 100644 index 000000000..e1c6784f1 --- /dev/null +++ b/client/keys/update_test.go @@ -0,0 +1,63 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/cosmos/cosmos-sdk/tests" + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/cli" + + "github.com/stretchr/testify/assert" +) + +func Test_updateKeyCommand(t *testing.T) { + cmd := updateKeyCommand() + assert.NotNil(t, cmd) + // No flags or defaults to validate +} + +func Test_runUpdateCmd(t *testing.T) { + fakeKeyName1 := "runUpdateCmd_Key1" + fakeKeyName2 := "runUpdateCmd_Key2" + + cmd := updateKeyCommand() + + // fails because it requests a password + err := runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "EOF") + + cleanUp := client.OverrideStdin(bufio.NewReader(strings.NewReader("pass1234\n"))) + defer cleanUp() + + // try again + err = runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "Key runUpdateCmd_Key1 not found") + + // Prepare a key base + // Now add a temporary keybase + kbHome, cleanUp1 := tests.NewTestCaseDir(t) + defer cleanUp1() + viper.Set(cli.HomeFlag, kbHome) + + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) + assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1) + assert.NoError(t, err) + + // Try again now that we have keys + cleanUp2 := client.OverrideStdin(bufio.NewReader(strings.NewReader("pass1234\nNew1234\nNew1234"))) + defer cleanUp2() + + // Incorrect key type + err = runUpdateCmd(cmd, []string{fakeKeyName1}) + assert.EqualError(t, err, "locally stored key required. Received: keys.offlineInfo") + + // TODO: Check for other type types? + +} diff --git a/client/keys/utils.go b/client/keys/utils.go index 78899f5f8..d772b8151 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -6,9 +6,7 @@ import ( "path/filepath" "github.com/spf13/viper" - "github.com/syndtr/goleveldb/leveldb/opt" "github.com/tendermint/tendermint/libs/cli" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -17,17 +15,18 @@ import ( ) // KeyDBName is the directory under root where we store the keys -const KeyDBName = "keys" - -// keybase is used to make GetKeyBase a singleton -var keybase keys.Keybase +const ( + KeyDBName = "keys" + OutputFormatText = "text" + OutputFormatJSON = "json" +) type bechKeyOutFn func(keyInfo keys.Info) (KeyOutput, error) // GetKeyInfo returns key info for a given name. An error is returned if the // keybase cannot be retrieved or getting the info fails. func GetKeyInfo(name string) (keys.Info, error) { - keybase, err := GetKeyBase() + keybase, err := NewKeyBaseFromHomeFlag() if err != nil { return nil, err } @@ -73,58 +72,22 @@ func ReadPassphraseFromStdin(name string) (string, error) { return passphrase, nil } -// TODO make keybase take a database not load from the directory - -// GetKeyBase initializes a read-only KeyBase based on the configuration. -func GetKeyBase() (keys.Keybase, error) { +// NewKeyBaseFromHomeFlag initializes a Keybase based on the configuration. +func NewKeyBaseFromHomeFlag() (keys.Keybase, error) { rootDir := viper.GetString(cli.HomeFlag) - return GetKeyBaseFromDir(rootDir) + return NewKeyBaseFromDir(rootDir) } -// GetKeyBaseWithWritePerm initialize a keybase based on the configuration with write permissions. -func GetKeyBaseWithWritePerm() (keys.Keybase, error) { - rootDir := viper.GetString(cli.HomeFlag) - return GetKeyBaseFromDirWithWritePerm(rootDir) +// NewKeyBaseFromDir initializes a keybase at a particular dir. +func NewKeyBaseFromDir(rootDir string) (keys.Keybase, error) { + return getLazyKeyBaseFromDir(rootDir) } -// GetKeyBaseFromDirWithWritePerm initializes a keybase at a particular dir with write permissions. -func GetKeyBaseFromDirWithWritePerm(rootDir string) (keys.Keybase, error) { - return getKeyBaseFromDirWithOpts(rootDir, nil) -} +// NewInMemoryKeyBase returns a storage-less keybase. +func NewInMemoryKeyBase() keys.Keybase { return keys.NewInMemory() } -// GetKeyBaseFromDir initializes a read-only keybase at a particular dir. -func GetKeyBaseFromDir(rootDir string) (keys.Keybase, error) { - // Disabled because of the inability to create a new keys database directory - // in the instance of when ReadOnly is set to true. - // - // ref: syndtr/goleveldb#240 - // return getKeyBaseFromDirWithOpts(rootDir, &opt.Options{ReadOnly: true}) - return getKeyBaseFromDirWithOpts(rootDir, nil) -} - -func getKeyBaseFromDirWithOpts(rootDir string, o *opt.Options) (keys.Keybase, error) { - if keybase == nil { - db, err := dbm.NewGoLevelDBWithOpts(KeyDBName, filepath.Join(rootDir, "keys"), o) - if err != nil { - return nil, err - } - keybase = client.GetKeyBase(db) - } - return keybase, nil -} - -// used to set the keybase manually in test -func SetKeyBase(kb keys.Keybase) { - keybase = kb -} - -// used for outputting keys.Info over REST -type KeyOutput struct { - Name string `json:"name"` - Type string `json:"type"` - Address string `json:"address"` - PubKey string `json:"pub_key"` - Seed string `json:"seed,omitempty"` +func getLazyKeyBaseFromDir(rootDir string) (keys.Keybase, error) { + return keys.NewLazyKeybase(KeyDBName, filepath.Join(rootDir, "keys")), nil } // create a list of KeyOutput in bech32 format @@ -198,7 +161,7 @@ func printKeyInfo(keyInfo keys.Info, bechKeyOut bechKeyOutFn) { } switch viper.Get(cli.OutputFlag) { - case "text": + case OutputFormatText: fmt.Printf("NAME:\tTYPE:\tADDRESS:\t\t\t\t\t\tPUBKEY:\n") printKeyOutput(ko) case "json": @@ -217,12 +180,12 @@ func printInfos(infos []keys.Info) { panic(err) } switch viper.Get(cli.OutputFlag) { - case "text": + case OutputFormatText: fmt.Printf("NAME:\tTYPE:\tADDRESS:\t\t\t\t\t\tPUBKEY:\n") for _, ko := range kos { printKeyOutput(ko) } - case "json": + case OutputFormatJSON: out, err := MarshalJSON(kos) if err != nil { panic(err) @@ -266,12 +229,12 @@ func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response inter } if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, _ = w.Write([]byte(err.Error())) return } case []byte: output = response.([]byte) } w.Header().Set("Content-Type", "application/json") - w.Write(output) + _, _ = w.Write(output) } diff --git a/client/keys/utils_test.go b/client/keys/utils_test.go deleted file mode 100644 index 5f3dd4560..000000000 --- a/client/keys/utils_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package keys - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cosmos/cosmos-sdk/crypto/keys" -) - -func TestGetKeyBaseLocks(t *testing.T) { - dir, err := ioutil.TempDir("", "cosmos-sdk-keys") - require.Nil(t, err) - defer os.RemoveAll(dir) - - // Acquire db - kb, err := GetKeyBaseFromDirWithWritePerm(dir) - require.Nil(t, err) - _, _, err = kb.CreateMnemonic("foo", keys.English, "12345678", keys.Secp256k1) - require.Nil(t, err) - // Reset global variable - keybase = nil - // Try to acquire another keybase from the same storage - _, err = GetKeyBaseFromDirWithWritePerm(dir) - require.NotNil(t, err) - _, err = GetKeyBaseFromDirWithWritePerm(dir) - require.NotNil(t, err) - - // Close the db and try to acquire the lock - kb.CloseDB() - kb, err = GetKeyBaseFromDirWithWritePerm(dir) - require.Nil(t, err) - - // Try to acquire another read-only keybase from the same storage - _, err = GetKeyBaseFromDir(dir) - require.Nil(t, err) - - kb.CloseDB() -} diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 0d680ada4..56dd65d3e 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -1,8 +1,8 @@ package lcd import ( + "encoding/base64" "encoding/hex" - "encoding/json" "fmt" "net/http" "os" @@ -13,23 +13,22 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/require" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - client "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/rest" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" - "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" - tests "github.com/cosmos/cosmos-sdk/tests" + "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/x/auth" authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + "github.com/cosmos/cosmos-sdk/x/bank" + dclcommon "github.com/cosmos/cosmos-sdk/x/distribution/client/common" + distrrest "github.com/cosmos/cosmos-sdk/x/distribution/client/rest" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/staking" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) const ( @@ -41,48 +40,115 @@ const ( altPw = "12345678901" ) -var fees = sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 5)} +var fees = sdk.Coins{sdk.NewInt64Coin(staking.DefaultBondDenom, 5)} func init() { mintkey.BcryptSecurityParameter = 1 version.Version = os.Getenv("VERSION") } -func TestKeys(t *testing.T) { - addr, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) +func TestSeedsAreDifferent(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, _ := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() - // get new seed - seed := getKeysSeed(t, port) + mnemonic1 := getKeysSeed(t, port) + mnemonic2 := getKeysSeed(t, port) + + require.NotEqual(t, mnemonic1, mnemonic2) +} + +func TestKeyRecover(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + defer cleanup() + + myName1 := "TestKeyRecover_1" + myName2 := "TestKeyRecover_2" + + mnemonic := getKeysSeed(t, port) + expectedInfo, _ := kb.CreateAccount(myName1, mnemonic, "", pw, 0, 0) + expectedAddress := expectedInfo.GetAddress().String() + expectedPubKey := sdk.MustBech32ifyAccPub(expectedInfo.GetPubKey()) // recover key - doRecoverKey(t, port, name2, pw, seed) + doRecoverKey(t, port, myName2, pw, mnemonic, 0, 0) + + keys := getKeys(t, port) + + require.Equal(t, expectedAddress, keys[0].Address) + require.Equal(t, expectedPubKey, keys[0].PubKey) +} + +func TestKeyRecoverHDPath(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + defer cleanup() + + mnemonic := getKeysSeed(t, port) + + for account := uint32(0); account < 50; account += 13 { + for index := uint32(0); index < 50; index += 15 { + name1Idx := fmt.Sprintf("name1_%d_%d", account, index) + name2Idx := fmt.Sprintf("name2_%d_%d", account, index) + + expectedInfo, _ := kb.CreateAccount(name1Idx, mnemonic, "", pw, account, index) + expectedAddress := expectedInfo.GetAddress().String() + expectedPubKey := sdk.MustBech32ifyAccPub(expectedInfo.GetPubKey()) + + // recover key + doRecoverKey(t, port, name2Idx, pw, mnemonic, account, index) + + keysName2Idx := getKey(t, port, name2Idx) + + require.Equal(t, expectedAddress, keysName2Idx.Address) + require.Equal(t, expectedPubKey, keysName2Idx.PubKey) + } + } +} + +func TestKeys(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr1, _ := CreateAddr(t, name1, pw, kb) + addr1Bech32 := addr1.String() + + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr1}, true) + defer cleanup() + + // get new seed & recover key + mnemonic2 := getKeysSeed(t, port) + doRecoverKey(t, port, name2, pw, mnemonic2, 0, 0) // add key - resp := doKeysPost(t, port, name3, pw, seed) + mnemonic3 := mnemonic2 + resp := doKeysPost(t, port, name3, pw, mnemonic3, 0, 0) - addrBech32 := addr.String() - addr2Bech32 := resp.Address - _, err := sdk.AccAddressFromBech32(addr2Bech32) + addr3Bech32 := resp.Address + _, err = sdk.AccAddressFromBech32(addr3Bech32) require.NoError(t, err, "Failed to return a correct bech32 address") // test if created account is the correct account - expectedInfo, _ := GetKeyBase(t).CreateKey(name3, seed, pw) - expectedAccount := sdk.AccAddress(expectedInfo.GetPubKey().Address().Bytes()) - require.Equal(t, expectedAccount.String(), addr2Bech32) + expectedInfo3, _ := kb.CreateAccount(name3, mnemonic3, "", pw, 0, 0) + expectedAddress3 := sdk.AccAddress(expectedInfo3.GetPubKey().Address()).String() + require.Equal(t, expectedAddress3, addr3Bech32) // existing keys - keys := getKeys(t, port) - require.Equal(t, name1, keys[0].Name, "Did not serve keys name correctly") - require.Equal(t, addrBech32, keys[0].Address, "Did not serve keys Address correctly") - require.Equal(t, name2, keys[1].Name, "Did not serve keys name correctly") - require.Equal(t, addr2Bech32, keys[1].Address, "Did not serve keys Address correctly") + require.Equal(t, name1, getKey(t, port, name1).Name, "Did not serve keys name correctly") + require.Equal(t, addr1Bech32, getKey(t, port, name1).Address, "Did not serve keys Address correctly") + require.Equal(t, name2, getKey(t, port, name2).Name, "Did not serve keys name correctly") + require.Equal(t, addr3Bech32, getKey(t, port, name2).Address, "Did not serve keys Address correctly") + require.Equal(t, name3, getKey(t, port, name3).Name, "Did not serve keys name correctly") + require.Equal(t, addr3Bech32, getKey(t, port, name3).Address, "Did not serve keys Address correctly") // select key key := getKey(t, port, name3) require.Equal(t, name3, key.Name, "Did not serve keys name correctly") - require.Equal(t, addr2Bech32, key.Address, "Did not serve keys Address correctly") + require.Equal(t, addr3Bech32, key.Address, "Did not serve keys Address correctly") // update key updateKey(t, port, name3, pw, altPw, false) @@ -100,7 +166,7 @@ func TestVersion(t *testing.T) { t.SkipNow() } - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() // node info @@ -110,7 +176,7 @@ func TestVersion(t *testing.T) { reg, err := regexp.Compile(`\d+\.\d+\.\d+.*`) require.Nil(t, err) match := reg.MatchString(body) - require.True(t, match, body, fmt.Sprintf("%s", body)) + require.True(t, match, body, body) // node info res, body = Request(t, port, "GET", "/node_version", nil) @@ -123,14 +189,14 @@ func TestVersion(t *testing.T) { } func TestNodeStatus(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() getNodeInfo(t, port) getSyncStatus(t, port, false) } func TestBlock(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() getBlock(t, port, -1, false) getBlock(t, port, 2, false) @@ -138,18 +204,20 @@ func TestBlock(t *testing.T) { } func TestValidators(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() resultVals := getValidatorSets(t, port, -1, false) - require.Contains(t, resultVals.Validators[0].Address.String(), "cosmosvaloper") + require.Contains(t, resultVals.Validators[0].Address.String(), "cosmosvalcons") require.Contains(t, resultVals.Validators[0].PubKey, "cosmosvalconspub") getValidatorSets(t, port, 2, false) getValidatorSets(t, port, 10000000, true) } func TestCoinSend(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() bz, err := hex.DecodeString("8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6") @@ -168,22 +236,21 @@ func TestCoinSend(t *testing.T) { tests.WaitForHeight(resultTx.Height+1, port) // check if tx was committed - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) // query sender acc = getAccount(t, port, addr) coins := acc.GetCoins() expectedBalance := initialBalance[0].Minus(fees[0]) - require.Equal(t, stakingTypes.DefaultBondDenom, coins[0].Denom) + require.Equal(t, staking.DefaultBondDenom, coins[0].Denom) require.Equal(t, expectedBalance.Amount.SubRaw(1), coins[0].Amount) expectedBalance = coins[0] // query receiver acc2 := getAccount(t, port, receiveAddr) coins2 := acc2.GetCoins() - require.Equal(t, stakingTypes.DefaultBondDenom, coins2[0].Denom) + require.Equal(t, staking.DefaultBondDenom, coins2[0].Denom) require.Equal(t, int64(1), coins2[0].Amount.Int64()) // test failure with too little gas @@ -209,41 +276,103 @@ func TestCoinSend(t *testing.T) { require.Equal(t, http.StatusInternalServerError, res.StatusCode, body) // run simulation and test success with estimated gas - res, body, _ = doTransferWithGas(t, port, seed, name1, memo, pw, addr, "10000", 1.0, true, false, fees) + res, body, _ = doTransferWithGas( + t, port, seed, name1, memo, pw, addr, "10000", 1.0, true, false, fees, + ) require.Equal(t, http.StatusOK, res.StatusCode, body) - var responseBody struct { - GasEstimate int64 `json:"gas_estimate"` - } - require.Nil(t, json.Unmarshal([]byte(body), &responseBody)) + + var gasEstResp rest.GasEstimateResponse + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &gasEstResp)) + require.NotZero(t, gasEstResp.GasEstimate) acc = getAccount(t, port, addr) - require.Equal(t, expectedBalance.Amount, acc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount, acc.GetCoins().AmountOf(staking.DefaultBondDenom)) - res, body, _ = doTransferWithGas(t, port, seed, name1, memo, pw, addr, - fmt.Sprintf("%d", responseBody.GasEstimate), 1.0, false, false, fees) + // run successful tx + gas := fmt.Sprintf("%d", gasEstResp.GasEstimate) + res, body, _ = doTransferWithGas(t, port, seed, name1, memo, pw, addr, gas, 1.0, false, false, fees) require.Equal(t, http.StatusOK, res.StatusCode, body) err = cdc.UnmarshalJSON([]byte(body), &resultTx) require.Nil(t, err) tests.WaitForHeight(resultTx.Height+1, port) - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) acc = getAccount(t, port, addr) expectedBalance = expectedBalance.Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(1), acc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.SubRaw(1), acc.GetCoins().AmountOf(staking.DefaultBondDenom)) +} + +func TestCoinSendAccAuto(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) + defer cleanup() + + acc := getAccount(t, port, addr) + initialBalance := acc.GetCoins() + + // send a transfer tx without specifying account number and sequence + res, body, _ := doTransferWithGasAccAuto(t, port, seed, name1, memo, pw, "200000", 1.0, false, false, fees) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // query sender + acc = getAccount(t, port, addr) + coins := acc.GetCoins() + expectedBalance := initialBalance[0].Minus(fees[0]) + + require.Equal(t, staking.DefaultBondDenom, coins[0].Denom) + require.Equal(t, expectedBalance.Amount.SubRaw(1), coins[0].Amount) +} + +func TestCoinMultiSendGenerateOnly(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) + defer cleanup() + + // generate only + res, body, _ := doTransferWithGas(t, port, seed, "", memo, "", addr, "200000", 1, false, true, fees) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var stdTx auth.StdTx + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &stdTx)) + require.Equal(t, len(stdTx.Msgs), 1) + require.Equal(t, stdTx.GetMsgs()[0].Route(), "bank") + require.Equal(t, stdTx.GetMsgs()[0].GetSigners(), []sdk.AccAddress{addr}) + require.Equal(t, 0, len(stdTx.Signatures)) + require.Equal(t, memo, stdTx.Memo) + require.NotZero(t, stdTx.Fee.Gas) + require.IsType(t, stdTx.GetMsgs()[0], bank.MsgSend{}) + require.Equal(t, addr, stdTx.GetMsgs()[0].(bank.MsgSend).FromAddress) } func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) - // generate TX - res, body, _ := doTransferWithGas(t, port, seed, name1, memo, "", addr, client.GasFlagAuto, 1, false, true, fees) + // simulate tx + res, body, _ := doTransferWithGas( + t, port, seed, name1, memo, "", addr, client.GasFlagAuto, 1.0, true, false, fees, + ) require.Equal(t, http.StatusOK, res.StatusCode, body) + + var gasEstResp rest.GasEstimateResponse + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &gasEstResp)) + require.NotZero(t, gasEstResp.GasEstimate) + + // generate tx + gas := fmt.Sprintf("%d", gasEstResp.GasEstimate) + res, body, _ = doTransferWithGas(t, port, seed, name1, memo, "", addr, gas, 1, false, true, fees) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var msg auth.StdTx require.Nil(t, cdc.UnmarshalJSON([]byte(body), &msg)) require.Equal(t, len(msg.Msgs), 1) @@ -251,6 +380,7 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, msg.Msgs[0].GetSigners(), []sdk.AccAddress{addr}) require.Equal(t, 0, len(msg.Signatures)) require.Equal(t, memo, msg.Memo) + require.NotZero(t, msg.Fee.Gas) gasEstimate := int64(msg.Fee.Gas) accnum := acc.GetAccountNumber() @@ -261,13 +391,14 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { payload := authrest.SignBody{ Tx: msg, - BaseReq: utils.NewBaseReq( + BaseReq: rest.NewBaseReq( name1, pw, "", viper.GetString(client.FlagChainID), "", "", accnum, sequence, nil, nil, false, false, ), } json, err := cdc.MarshalJSON(payload) require.Nil(t, err) + res, body = Request(t, port, "POST", "/tx/sign", json) require.Equal(t, http.StatusOK, res.StatusCode, body) require.Nil(t, cdc.UnmarshalJSON([]byte(body), &signedMsg)) @@ -287,19 +418,60 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode, body) // check if tx was committed - var resultTx ctypes.ResultBroadcastTxCommit + var resultTx sdk.TxResponse require.Nil(t, cdc.UnmarshalJSON([]byte(body), &resultTx)) - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) - require.Equal(t, gasEstimate, resultTx.DeliverTx.GasWanted) + require.Equal(t, uint32(0), resultTx.Code) + require.Equal(t, gasEstimate, resultTx.GasWanted) +} + +func TestEncodeTx(t *testing.T) { + // Setup + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) + defer cleanup() + + // Make a transaction to test with + res, body, _ := doTransferWithGas(t, port, seed, name1, memo, "", addr, "2", 1, false, true, fees) + var tx auth.StdTx + cdc.UnmarshalJSON([]byte(body), &tx) + + // Build the request + encodeReq := struct { + Tx auth.StdTx `json:"tx"` + }{Tx: tx} + encodedJSON, _ := cdc.MarshalJSON(encodeReq) + res, body = Request(t, port, "POST", "/tx/encode", encodedJSON) + + // Make sure it came back ok, and that we can decode it back to the transaction + // 200 response + require.Equal(t, http.StatusOK, res.StatusCode, body) + encodeResp := struct { + Tx string `json:"tx"` + }{} + + // No error decoding the JSON + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &encodeResp)) + + // Check that the base64 decodes + decodedBytes, err := base64.StdEncoding.DecodeString(encodeResp.Tx) + require.Nil(t, err) + + // Check that the transaction decodes as expected + var decodedTx auth.StdTx + require.Nil(t, cdc.UnmarshalBinaryLengthPrefixed(decodedBytes, &decodedTx)) + require.Equal(t, memo, decodedTx.Memo) } func TestTxs(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() - var emptyTxs []tx.Info + var emptyTxs []sdk.TxResponse txs := getTransactions(t, port) require.Equal(t, emptyTxs, txs) @@ -319,14 +491,13 @@ func TestTxs(t *testing.T) { tests.WaitForHeight(resultTx.Height+1, port) // check if tx is queryable - tx := getTransaction(t, port, resultTx.Hash.String()) - require.Equal(t, resultTx.Hash, tx.Hash) + tx := getTransaction(t, port, resultTx.TxHash) + require.Equal(t, resultTx.TxHash, tx.TxHash) // query sender txs = getTransactions(t, port, fmt.Sprintf("sender=%s", addr.String())) require.Len(t, txs, 1) require.Equal(t, resultTx.Height, txs[0].Height) - fmt.Println(txs[0]) // query recipient txs = getTransactions(t, port, fmt.Sprintf("recipient=%s", receiveAddr.String())) @@ -335,8 +506,10 @@ func TestTxs(t *testing.T) { } func TestPoolParamsQuery(t *testing.T) { - addr, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, _ := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() defaultParams := staking.DefaultParams() @@ -347,16 +520,21 @@ func TestPoolParamsQuery(t *testing.T) { pool := getStakingPool(t, port) initialPool := staking.InitialPool() - initialPool.NotBondedTokens = initialPool.NotBondedTokens.Add(sdk.NewInt(100)) - initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewInt(100)) // Delegate tx on GaiaAppGenState - initialPool.NotBondedTokens = initialPool.NotBondedTokens.Add(sdk.NewInt(50)) // freeFermionsAcc = 50 on GaiaAppGenState + tokens := staking.TokensFromTendermintPower(100) + freeFermions := staking.TokensFromTendermintPower(50) + initialPool.NotBondedTokens = initialPool.NotBondedTokens.Add(tokens) + initialPool.BondedTokens = initialPool.BondedTokens.Add(tokens) // Delegate tx on GaiaAppGenState + initialPool.NotBondedTokens = initialPool.NotBondedTokens.Add(freeFermions) // freeFermionsAcc = 50 on GaiaAppGenState require.Equal(t, initialPool.BondedTokens, pool.BondedTokens) - require.Equal(t, initialPool.NotBondedTokens, pool.NotBondedTokens) + + //TODO include this test once REST for distribution is online, need to include distribution tokens from inflation + // for this equality to make sense + //require.Equal(t, initialPool.NotBondedTokens, pool.NotBondedTokens) } func TestValidatorsQuery(t *testing.T) { - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() require.Equal(t, 1, len(valPubKeys)) @@ -376,7 +554,7 @@ func TestValidatorsQuery(t *testing.T) { } func TestValidatorQuery(t *testing.T) { - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() require.Equal(t, 1, len(valPubKeys)) require.Equal(t, 1, len(operAddrs)) @@ -386,26 +564,30 @@ func TestValidatorQuery(t *testing.T) { } func TestBonding(t *testing.T) { - addr, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 2, []sdk.AccAddress{addr}) + cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 2, []sdk.AccAddress{addr}, false) + tests.WaitForHeight(1, port) defer cleanup() require.Equal(t, 2, len(valPubKeys)) require.Equal(t, 2, len(operAddrs)) - amt := sdk.NewDec(60) + amt := staking.TokensFromTendermintPower(60) + amtDec := sdk.NewDecFromInt(amt) validator := getValidator(t, port, operAddrs[0]) acc := getAccount(t, port, addr) initialBalance := acc.GetCoins() // create bond TX - resultTx := doDelegate(t, port, name1, pw, addr, operAddrs[0], 60, fees) + delTokens := staking.TokensFromTendermintPower(60) + resultTx := doDelegate(t, port, name1, pw, addr, operAddrs[0], delTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) // query tx txs := getTransactions(t, port, @@ -419,16 +601,16 @@ func TestBonding(t *testing.T) { acc = getAccount(t, port, addr) coins := acc.GetCoins() expectedBalance := initialBalance[0].Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(60), coins.AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(delTokens), coins.AmountOf(staking.DefaultBondDenom)) expectedBalance = coins[0] // query delegation bond := getDelegation(t, port, addr, operAddrs[0]) - require.Equal(t, amt, bond.Shares) + require.Equal(t, amtDec, bond.Shares) delegatorDels := getDelegatorDelegations(t, port, addr) require.Len(t, delegatorDels, 1) - require.Equal(t, amt, delegatorDels[0].Shares) + require.Equal(t, amtDec, delegatorDels[0].Shares) // query all delegations to validator bonds := getValidatorDelegations(t, port, operAddrs[0]) @@ -437,25 +619,25 @@ func TestBonding(t *testing.T) { bondedValidators := getDelegatorValidators(t, port, addr) require.Len(t, bondedValidators, 1) require.Equal(t, operAddrs[0], bondedValidators[0].OperatorAddr) - require.Equal(t, validator.DelegatorShares.Add(amt).String(), bondedValidators[0].DelegatorShares.String()) + require.Equal(t, validator.DelegatorShares.Add(amtDec).String(), bondedValidators[0].DelegatorShares.String()) bondedValidator := getDelegatorValidator(t, port, addr, operAddrs[0]) require.Equal(t, operAddrs[0], bondedValidator.OperatorAddr) // testing unbonding - resultTx = doUndelegate(t, port, name1, pw, addr, operAddrs[0], 30, fees) + unbondingTokens := staking.TokensFromTendermintPower(30) + resultTx = doUndelegate(t, port, name1, pw, addr, operAddrs[0], unbondingTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) // sender should have not received any coins as the unbonding has only just begun acc = getAccount(t, port, addr) coins = acc.GetCoins() expectedBalance = expectedBalance.Minus(fees[0]) require.True(t, - expectedBalance.Amount.LT(coins.AmountOf(stakingTypes.DefaultBondDenom)) || - expectedBalance.Amount.Equal(coins.AmountOf(stakingTypes.DefaultBondDenom)), + expectedBalance.Amount.LT(coins.AmountOf(staking.DefaultBondDenom)) || + expectedBalance.Amount.Equal(coins.AmountOf(staking.DefaultBondDenom)), "should get tokens back from automatic withdrawal after an unbonding delegation", ) expectedBalance = coins[0] @@ -470,21 +652,21 @@ func TestBonding(t *testing.T) { ubd := getUnbondingDelegation(t, port, addr, operAddrs[0]) require.Len(t, ubd.Entries, 1) - require.Equal(t, int64(30), ubd.Entries[0].Balance.Amount.Int64()) + require.Equal(t, delTokens.DivRaw(2), ubd.Entries[0].Balance) // test redelegation - resultTx = doBeginRedelegation(t, port, name1, pw, addr, operAddrs[0], operAddrs[1], 30, fees) + rdTokens := staking.TokensFromTendermintPower(30) + resultTx = doBeginRedelegation(t, port, name1, pw, addr, operAddrs[0], operAddrs[1], rdTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) // verify balance after paying fees acc = getAccount(t, port, addr) expectedBalance = expectedBalance.Minus(fees[0]) require.True(t, - expectedBalance.Amount.LT(coins.AmountOf(stakingTypes.DefaultBondDenom)) || - expectedBalance.Amount.Equal(coins.AmountOf(stakingTypes.DefaultBondDenom)), + expectedBalance.Amount.LT(coins.AmountOf(staking.DefaultBondDenom)) || + expectedBalance.Amount.Equal(coins.AmountOf(staking.DefaultBondDenom)), "should get tokens back from automatic withdrawal after an unbonding delegation", ) @@ -498,34 +680,32 @@ func TestBonding(t *testing.T) { require.Equal(t, resultTx.Height, txs[0].Height) // query delegations, unbondings and redelegations from validator and delegator + rdShares := sdk.NewDecFromInt(rdTokens) delegatorDels = getDelegatorDelegations(t, port, addr) require.Len(t, delegatorDels, 1) - require.Equal(t, "30.000000000000000000", delegatorDels[0].GetShares().String()) + require.Equal(t, rdShares, delegatorDels[0].GetShares()) redelegation := getRedelegations(t, port, addr, operAddrs[0], operAddrs[1]) require.Len(t, redelegation, 1) require.Len(t, redelegation[0].Entries, 1) - require.Equal(t, "30", redelegation[0].Entries[0].Balance.Amount.String()) delegatorUbds := getDelegatorUnbondingDelegations(t, port, addr) require.Len(t, delegatorUbds, 1) require.Len(t, delegatorUbds[0].Entries, 1) - require.Equal(t, "30", delegatorUbds[0].Entries[0].Balance.Amount.String()) + require.Equal(t, rdTokens, delegatorUbds[0].Entries[0].Balance) delegatorReds := getRedelegations(t, port, addr, nil, nil) require.Len(t, delegatorReds, 1) require.Len(t, delegatorReds[0].Entries, 1) - require.Equal(t, "30", delegatorReds[0].Entries[0].Balance.Amount.String()) validatorUbds := getValidatorUnbondingDelegations(t, port, operAddrs[0]) require.Len(t, validatorUbds, 1) require.Len(t, validatorUbds[0].Entries, 1) - require.Equal(t, "30", validatorUbds[0].Entries[0].Balance.Amount.String()) + require.Equal(t, rdTokens, validatorUbds[0].Entries[0].Balance) validatorReds := getRedelegations(t, port, nil, operAddrs[0], nil) require.Len(t, validatorReds, 1) require.Len(t, validatorReds[0].Entries, 1) - require.Equal(t, "30", validatorReds[0].Entries[0].Balance.Amount.String()) // TODO Undonding status not currently implemented // require.Equal(t, sdk.Unbonding, bondedValidators[0].Status) @@ -545,28 +725,30 @@ func TestBonding(t *testing.T) { } func TestSubmitProposal(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) initialBalance := acc.GetCoins() // create SubmitProposal TX - resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, 5, fees) + proposalTokens := staking.TokensFromTendermintPower(5) + resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, proposalTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) // check if tx was committed - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) var proposalID uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID) // verify balance acc = getAccount(t, port, addr) expectedBalance := initialBalance[0].Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(5), acc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(proposalTokens), acc.GetCoins().AmountOf(staking.DefaultBondDenom)) // query proposal proposal := getProposal(t, port, proposalID) @@ -578,29 +760,31 @@ func TestSubmitProposal(t *testing.T) { } func TestDeposit(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) initialBalance := acc.GetCoins() // create SubmitProposal TX - resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, 5, fees) + proposalTokens := staking.TokensFromTendermintPower(5) + resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, proposalTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) // check if tx was committed - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) var proposalID uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID) // verify balance acc = getAccount(t, port, addr) coins := acc.GetCoins() expectedBalance := initialBalance[0].Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(5), coins.AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(proposalTokens), coins.AmountOf(staking.DefaultBondDenom)) expectedBalance = coins[0] // query proposal @@ -608,13 +792,14 @@ func TestDeposit(t *testing.T) { require.Equal(t, "Test", proposal.GetTitle()) // create SubmitProposal TX - resultTx = doDeposit(t, port, seed, name1, pw, addr, proposalID, 5, fees) + depositTokens := staking.TokensFromTendermintPower(5) + resultTx = doDeposit(t, port, seed, name1, pw, addr, proposalID, depositTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) // verify balance after deposit and fee acc = getAccount(t, port, addr) expectedBalance = expectedBalance.Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(5), acc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(depositTokens), acc.GetCoins().AmountOf(staking.DefaultBondDenom)) // query tx txs := getTransactions(t, port, fmt.Sprintf("action=deposit&depositor=%s", addr)) @@ -622,38 +807,41 @@ func TestDeposit(t *testing.T) { require.Equal(t, resultTx.Height, txs[0].Height) // query proposal + totalCoins := sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, staking.TokensFromTendermintPower(10))} proposal = getProposal(t, port, proposalID) - require.True(t, proposal.GetTotalDeposit().IsEqual(sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 10)})) + require.True(t, proposal.GetTotalDeposit().IsEqual(totalCoins)) // query deposit deposit := getDeposit(t, port, proposalID, addr) - require.True(t, deposit.Amount.IsEqual(sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 10)})) + require.True(t, deposit.Amount.IsEqual(totalCoins)) } func TestVote(t *testing.T) { - addr, seed := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, _, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) initialBalance := acc.GetCoins() // create SubmitProposal TX - resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, 10, fees) + proposalTokens := staking.TokensFromTendermintPower(10) + resultTx := doSubmitProposal(t, port, seed, name1, pw, addr, proposalTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) // check if tx was committed - require.Equal(t, uint32(0), resultTx.CheckTx.Code) - require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, uint32(0), resultTx.Code) var proposalID uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID) // verify balance acc = getAccount(t, port, addr) coins := acc.GetCoins() expectedBalance := initialBalance[0].Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(10), coins.AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(proposalTokens), coins.AmountOf(staking.DefaultBondDenom)) expectedBalance = coins[0] // query proposal @@ -669,7 +857,7 @@ func TestVote(t *testing.T) { acc = getAccount(t, port, addr) coins = acc.GetCoins() expectedBalance = expectedBalance.Minus(fees[0]) - require.Equal(t, expectedBalance.Amount, coins.AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount, coins.AmountOf(staking.DefaultBondDenom)) expectedBalance = coins[0] // query tx @@ -682,21 +870,22 @@ func TestVote(t *testing.T) { require.Equal(t, gov.OptionYes, vote.Option) tally := getTally(t, port, proposalID) - require.Equal(t, sdk.ZeroDec(), tally.Yes, "tally should be 0 as the address is not bonded") + require.Equal(t, sdk.ZeroInt(), tally.Yes, "tally should be 0 as the address is not bonded") // create bond TX - resultTx = doDelegate(t, port, name1, pw, addr, operAddrs[0], 60, fees) + delTokens := staking.TokensFromTendermintPower(60) + resultTx = doDelegate(t, port, name1, pw, addr, operAddrs[0], delTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) // verify balance acc = getAccount(t, port, addr) coins = acc.GetCoins() expectedBalance = expectedBalance.Minus(fees[0]) - require.Equal(t, expectedBalance.Amount.SubRaw(60), coins.AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount.Sub(delTokens), coins.AmountOf(staking.DefaultBondDenom)) expectedBalance = coins[0] tally = getTally(t, port, proposalID) - require.Equal(t, sdk.NewDec(60), tally.Yes, "tally should be equal to the amount delegated") + require.Equal(t, delTokens, tally.Yes, "tally should be equal to the amount delegated") // change vote option resultTx = doVote(t, port, seed, name1, pw, addr, proposalID, "No", fees) @@ -705,16 +894,18 @@ func TestVote(t *testing.T) { // verify balance acc = getAccount(t, port, addr) expectedBalance = expectedBalance.Minus(fees[0]) - require.Equal(t, expectedBalance.Amount, acc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom)) + require.Equal(t, expectedBalance.Amount, acc.GetCoins().AmountOf(staking.DefaultBondDenom)) tally = getTally(t, port, proposalID) - require.Equal(t, sdk.ZeroDec(), tally.Yes, "tally should be 0 the user changed the option") - require.Equal(t, sdk.NewDec(60), tally.No, "tally should be equal to the amount delegated") + require.Equal(t, sdk.ZeroInt(), tally.Yes, "tally should be 0 the user changed the option") + require.Equal(t, delTokens, tally.No, "tally should be equal to the amount delegated") } func TestUnjail(t *testing.T) { - addr, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) - cleanup, valPubKeys, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, _ := CreateAddr(t, name1, pw, kb) + cleanup, valPubKeys, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() // XXX: any less than this and it fails @@ -728,31 +919,33 @@ func TestUnjail(t *testing.T) { } func TestProposalsQuery(t *testing.T) { - addrs, seeds, names, passwords := CreateAddrs(t, GetKeyBase(t), 2) + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addrs, seeds, names, passwords := CreateAddrs(t, kb, 2) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addrs[0], addrs[1]}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addrs[0], addrs[1]}, true) defer cleanup() depositParam := getDepositParam(t, port) - halfMinDeposit := depositParam.MinDeposit.AmountOf(stakingTypes.DefaultBondDenom).Int64() / 2 + halfMinDeposit := depositParam.MinDeposit.AmountOf(staking.DefaultBondDenom).DivRaw(2) getVotingParam(t, port) getTallyingParam(t, port) // Addr1 proposes (and deposits) proposals #1 and #2 resultTx := doSubmitProposal(t, port, seeds[0], names[0], passwords[0], addrs[0], halfMinDeposit, fees) var proposalID1 uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID1) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID1) tests.WaitForHeight(resultTx.Height+1, port) resultTx = doSubmitProposal(t, port, seeds[0], names[0], passwords[0], addrs[0], halfMinDeposit, fees) var proposalID2 uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID2) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID2) tests.WaitForHeight(resultTx.Height+1, port) // Addr2 proposes (and deposits) proposals #3 resultTx = doSubmitProposal(t, port, seeds[1], names[1], passwords[1], addrs[1], halfMinDeposit, fees) var proposalID3 uint64 - cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.DeliverTx.GetData(), &proposalID3) + cdc.MustUnmarshalBinaryLengthPrefixed(resultTx.Data, &proposalID3) tests.WaitForHeight(resultTx.Height+1, port) // Addr2 deposits on proposals #2 & #3 @@ -781,7 +974,8 @@ func TestProposalsQuery(t *testing.T) { require.Equal(t, deposit, deposits[0]) // increasing the amount of the deposit should update the existing one - resultTx = doDeposit(t, port, seeds[0], names[0], passwords[0], addrs[0], proposalID1, 1, fees) + depositTokens := staking.TokensFromTendermintPower(1) + resultTx = doDeposit(t, port, seeds[0], names[0], passwords[0], addrs[0], proposalID1, depositTokens, fees) tests.WaitForHeight(resultTx.Height+1, port) deposits = getDeposits(t, port, proposalID1) @@ -849,7 +1043,7 @@ func TestProposalsQuery(t *testing.T) { } func TestSlashingGetParams(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) defer cleanup() res, body := Request(t, port, "GET", "/slashing/parameters", nil) @@ -859,3 +1053,87 @@ func TestSlashingGetParams(t *testing.T) { err := cdc.UnmarshalJSON([]byte(body), ¶ms) require.NoError(t, err) } + +func TestDistributionGetParams(t *testing.T) { + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + defer cleanup() + + res, body := Request(t, port, "GET", "/distribution/parameters", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &dclcommon.PrettyParams{})) +} + +func TestDistributionFlow(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, valAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) + defer cleanup() + + valAddr := valAddrs[0] + operAddr := sdk.AccAddress(valAddr) + + var rewards sdk.DecCoins + res, body := Request(t, port, "GET", fmt.Sprintf("/distribution/outstanding_rewards"), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + var valDistInfo distrrest.ValidatorDistInfo + res, body = Request(t, port, "GET", "/distribution/validators/"+valAddr.String(), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &valDistInfo)) + require.Equal(t, valDistInfo.OperatorAddress.String(), sdk.AccAddress(valAddr).String()) + + // Delegate some coins + delTokens := staking.TokensFromTendermintPower(60) + resultTx := doDelegate(t, port, name1, pw, addr, valAddr, delTokens, fees) + tests.WaitForHeight(resultTx.Height+1, port) + require.Equal(t, uint32(0), resultTx.Code) + + // send some coins + _, resultTx = doTransfer(t, port, seed, name1, memo, pw, addr, fees) + tests.WaitForHeight(resultTx.Height+5, port) + require.Equal(t, uint32(0), resultTx.Code) + + // Query outstanding rewards changed + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/outstanding_rewards"), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + // Query validator distribution info + res, body = Request(t, port, "GET", "/distribution/validators/"+valAddr.String(), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &valDistInfo)) + + // Query validator's rewards + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/validators/%s/rewards", valAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + // Query self-delegation + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/delegators/%s/rewards/%s", operAddr, valAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + // Query delegation + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/delegators/%s/rewards/%s", addr, valAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + // Query delegator's rewards total + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/delegators/%s/rewards", operAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &rewards)) + + // Query delegator's withdrawal address + var withdrawAddr string + res, body = Request(t, port, "GET", fmt.Sprintf("/distribution/delegators/%s/withdraw_address", operAddr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.NoError(t, cdc.UnmarshalJSON([]byte(body), &withdrawAddr)) + require.Equal(t, operAddr.String(), withdrawAddr) + + // Withdraw delegator's rewards + resultTx = doWithdrawDelegatorAllRewards(t, port, seed, name1, pw, addr, fees) + require.Equal(t, uint32(0), resultTx.Code) +} diff --git a/client/lcd/root.go b/client/lcd/root.go index 07aade96b..0f6fb120a 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -16,7 +16,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/codec" keybase "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/server" @@ -52,25 +51,9 @@ func NewRestServer(cdc *codec.Codec) *RestServer { } } -func (rs *RestServer) setKeybase(kb keybase.Keybase) { - // If a keybase is passed in, set it and return - if kb != nil { - rs.KeyBase = kb - return - } - - // Otherwise get the keybase and set it - kb, err := keys.GetKeyBase() //XXX - if err != nil { - fmt.Printf("Failed to open Keybase: %s, exiting...", err) - os.Exit(1) - } - rs.KeyBase = kb -} - // Start starts the rest server func (rs *RestServer) Start(listenAddr string, sslHosts string, - certFile string, keyFile string, maxOpen int, insecure bool) (err error) { + certFile string, keyFile string, maxOpen int, secure bool) (err error) { server.TrapSignal(func() { err := rs.listener.Close() @@ -84,10 +67,11 @@ func (rs *RestServer) Start(listenAddr string, sslHosts string, if err != nil { return } - rs.log.Info("Starting Gaia Lite REST service...") + rs.log.Info(fmt.Sprintf("Starting Gaia Lite REST service (chain-id: %q)...", + viper.GetString(client.FlagChainID))) // launch rest-server in insecure mode - if insecure { + if !secure { return rpcserver.StartHTTPServer(rs.listener, rs.Mux, rs.log) } @@ -135,7 +119,6 @@ func ServeCommand(cdc *codec.Codec, registerRoutesFn func(*RestServer)) *cobra.C RunE: func(cmd *cobra.Command, args []string) (err error) { rs := NewRestServer(cdc) - rs.setKeybase(nil) registerRoutesFn(rs) // Start the rest server and return error if one exists @@ -145,15 +128,13 @@ func ServeCommand(cdc *codec.Codec, registerRoutesFn func(*RestServer)) *cobra.C viper.GetString(client.FlagSSLCertFile), viper.GetString(client.FlagSSLKeyFile), viper.GetInt(client.FlagMaxOpenConnections), - viper.GetBool(client.FlagInsecure)) + viper.GetBool(client.FlagTLS)) return err }, } - client.RegisterRestServerFlags(cmd) - - return cmd + return client.RegisterRestServerFlags(cmd) } func (rs *RestServer) registerSwaggerUI() { diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index eed9d7e27..82a6af083 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -1,28 +1,28 @@ --- -swagger: '2.0' +swagger: "2.0" info: version: "3.0" title: Gaia-Lite for Cosmos description: A REST interface for state queries, transaction generation, signing, and broadcast. tags: -- name: ICS0 - description: Tendermint APIs, such as query blocks, transactions and validatorset -- name: ICS1 - description: Key management APIs -- name: ICS20 - description: Create, sign and broadcast transactions -- name: ICS21 - description: Stake module APIs -- name: ICS22 - description: Governance module APIs -- name: ICS23 - description: Slashing module APIs -- name: ICS24 - description: WIP - Fee distribution module APIs -- name: version - description: Query app version + - name: ICS0 + description: Tendermint APIs, such as query blocks, transactions and validatorset + - name: ICS1 + description: Key management APIs + - name: ICS20 + description: Create, sign and broadcast transactions + - name: ICS21 + description: Stake module APIs + - name: ICS22 + description: Governance module APIs + - name: ICS23 + description: Slashing module APIs + - name: ICS24 + description: Fee distribution module APIs + - name: version + description: Query app version schemes: -- https + - https host: fabo.interblock.io:1317 securityDefinitions: kms: @@ -32,7 +32,7 @@ paths: get: summary: Version of Gaia-lite tags: - - version + - version description: Get the version of gaia-lite running locally to compare against expected responses: 200: @@ -41,7 +41,7 @@ paths: get: summary: Version of the connected node tags: - - version + - version description: Get the version of the SDK running on the connected node to compare against expected responses: 200: @@ -53,9 +53,9 @@ paths: description: Information about the connected node summary: The properties of the connected node tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json responses: 200: description: Node status @@ -90,7 +90,7 @@ paths: get: summary: Syncing state of node tags: - - ICS0 + - ICS0 description: Get if the node is currently syning with other nodes responses: 200: @@ -101,9 +101,9 @@ paths: get: summary: Get the latest block tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json responses: 200: description: The latest block @@ -115,15 +115,15 @@ paths: get: summary: Get a block at a certain height tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json parameters: - - in: path - name: height - description: Block height - required: true - type: number + - in: path + name: height + description: Block height + required: true + type: number responses: 200: description: The block at a specific height @@ -139,9 +139,9 @@ paths: get: summary: Get the latest validator set tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json responses: 200: description: The validator set at the latest block height @@ -160,15 +160,15 @@ paths: get: summary: Get a validator set a certain height tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json parameters: - - in: path - name: height - description: Block height - required: true - type: number + - in: path + name: height + description: Block height + required: true + type: number responses: 200: description: The validator set at a specific block height @@ -191,15 +191,15 @@ paths: get: summary: Get a Tx by hash tags: - - ICS0 + - ICS0 produces: - - application/json + - application/json parameters: - - in: path - name: hash - description: Tx hash - required: true - type: string + - in: path + name: hash + description: Tx hash + required: true + type: string responses: 200: description: Tx with the provided hash @@ -210,25 +210,25 @@ paths: /txs: get: tags: - - ICS0 + - ICS0 summary: Search transactions description: Search transactions by tag(s). produces: - - application/json + - application/json parameters: - - in: query - name: tag - type: string - description: "transaction tags such as 'action=submit-proposal' and 'proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc' which results in the following endpoint: 'GET /txs?action=submit-proposal&proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc'" - required: true - - in: query - name: page - description: Pagination page - type: integer - - in: query - name: size - description: Pagination size - type: integer + - in: query + name: tag + type: string + description: "transaction tags such as 'action=submit-proposal' and 'proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc' which results in the following endpoint: 'GET /txs?action=submit-proposal&proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc'" + required: true + - in: query + name: page + description: Pagination page + type: integer + - in: query + name: size + description: Pagination size + type: integer responses: 200: description: All txs matching the provided tags @@ -242,26 +242,26 @@ paths: description: Internal Server Error post: tags: - - ICS0 + - ICS0 summary: broadcast Tx description: broadcast tx with tendermint rpc consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: body - name: txBroadcast - description: Build a StdTx transaction and serilize it to a byte array with amino, then the `"tx"` field in the post body will be the base64 encoding of the byte array. The supported return types includes `"block"`(return after tx commit), `"sync"`(return afer CheckTx) and `"async"`(return right away). - required: true - schema: - type: object - properties: - tx: - type: string - return: - type: string - example: block + - in: body + name: txBroadcast + description: Build a StdTx transaction and serilize it to a byte array with amino, then the `"tx"` field in the post body will be the base64 encoding of the byte array. The supported return types includes `"block"`(return after tx commit), `"sync"`(return afer CheckTx) and `"async"`(return right away). + required: true + schema: + type: object + properties: + tx: + type: string + return: + type: string + example: block responses: 200: description: Broadcast tx result @@ -272,28 +272,28 @@ paths: /tx/sign: post: tags: - - ICS20 + - ICS20 summary: Sign a Tx description: Sign a Tx providing locally stored account and according password consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: body - name: sendToken - description: sign tx - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - tx: - $ref: "#/definitions/StdTx" - append_sig: - type: boolean - example: true + - in: body + name: sendToken + description: sign tx + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + tx: + $ref: "#/definitions/StdTx" + append_sig: + type: boolean + example: true responses: 200: description: The signed Tx @@ -308,23 +308,23 @@ paths: /tx/broadcast: post: tags: - - ICS20 + - ICS20 summary: Send a signed Tx description: Send a signed Tx to a Gaiad full node consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: body - name: txBroadcast - description: broadcast tx - required: true - schema: - type: object - properties: - tx: - $ref: "#/definitions/StdTx" + - in: body + name: txBroadcast + description: broadcast tx + required: true + schema: + type: object + properties: + tx: + $ref: "#/definitions/StdTx" responses: 202: description: Tx was send and will probably be added to the next block @@ -334,19 +334,52 @@ paths: description: The Tx was malformated 500: description: Server internal error + /tx/encode: + post: + tags: + - ICS20 + summary: Encode a transaction to wire format + description: Encode a transaction (signed or not) from JSON to base64-encoded Amino serialized bytes + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: tx + description: The transaction to encode + required: true + schema: + type: object + properties: + tx: + $ref: "#/definitions/StdTx" + responses: + 200: + description: Transaction was successfully decoded and re-encoded + schema: + type: object + properties: + tx: + type: string + example: The base64-encoded Amino-serialized bytes for the transaction + 400: + description: The Tx was malformated + 500: + description: Server internal error /bank/balances/{address}: get: summary: Get the account balances tags: - - ICS20 + - ICS20 produces: - - application/json + - application/json parameters: - - in: path - name: address - description: Account address in bech32 format - required: true - type: string + - in: path + name: address + description: Account address in bech32 format + required: true + type: string responses: 200: description: Account balances @@ -363,30 +396,30 @@ paths: summary: Send coins (build -> sign -> send) description: Send coins (build -> sign -> send) tags: - - ICS20 + - ICS20 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: path - name: address - description: Account address in bech32 format - required: true - type: string - - in: body - name: account - description: The password of the account to remove from the KMS - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - amount: - type: array - items: - $ref: "#/definitions/Coin" + - in: path + name: address + description: Account address in bech32 format + required: true + type: string + - in: body + name: account + description: The password of the account to remove from the KMS + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + amount: + type: array + items: + $ref: "#/definitions/Coin" responses: 202: description: Tx was send and will probably be added to the next block @@ -402,43 +435,43 @@ paths: get: summary: List of accounts stored locally tags: - - ICS1 + - ICS1 produces: - - application/json + - application/json responses: 200: description: Array of accounts schema: type: array items: - $ref: '#/definitions/KeyOutput' + $ref: "#/definitions/KeyOutput" 500: description: Server internal error post: summary: Create a new account locally tags: - - ICS1 + - ICS1 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: body - name: account - description: The account to create - schema: - type: object - required: - - name - - password - - seed - properties: - name: - type: string - password: - type: string - seed: - type: string + - in: body + name: account + description: The account to create + schema: + type: object + required: + - name + - password + - seed + properties: + name: + type: string + password: + type: string + seed: + type: string responses: 200: description: Returns account information of the created key @@ -454,7 +487,7 @@ paths: get: summary: Create a new seed to create a new account with tags: - - ICS1 + - ICS1 responses: 200: description: 24 word Seed @@ -465,30 +498,30 @@ paths: post: summary: Recover a account from a seed tags: - - ICS1 + - ICS1 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: path - name: name - description: Account name - required: true - type: string - - in: body - name: pwdAndSeed - description: Provide password and seed to recover a key - schema: - type: object - required: - - password - - seed - properties: - password: - type: string - seed: - type: string + - in: path + name: name + description: Account name + required: true + type: string + - in: body + name: pwdAndSeed + description: Provide password and seed to recover a key + schema: + type: object + required: + - password + - seed + properties: + password: + type: string + seed: + type: string responses: 200: description: Returns account information of the recovered key @@ -502,17 +535,17 @@ paths: description: Server internal error /keys/{name}: parameters: - - in: path - name: name - description: Account name - required: true - type: string + - in: path + name: name + description: Account name + required: true + type: string get: summary: Get a certain locally stored account tags: - - ICS1 + - ICS1 produces: - - application/json + - application/json responses: 200: description: Locally stored account @@ -523,23 +556,23 @@ paths: put: summary: Update the password for this account in the KMS tags: - - ICS1 + - ICS1 consumes: - - application/json + - application/json parameters: - - in: body - name: account - description: The new and old password - schema: - type: object - required: - - new_password - - old_password - properties: - new_password: - type: string - old_password: - type: string + - in: body + name: account + description: The new and old password + schema: + type: object + required: + - new_password + - old_password + properties: + new_password: + type: string + old_password: + type: string responses: 200: description: Updated password @@ -550,20 +583,20 @@ paths: delete: summary: Remove an account tags: - - ICS1 + - ICS1 consumes: - - application/json + - application/json parameters: - - in: body - name: account - description: The password of the account to remove from the KMS - schema: - type: object - required: - - password - properties: - password: - type: string + - in: body + name: account + description: The password of the account to remove from the KMS + schema: + type: object + required: + - password + properties: + password: + type: string responses: 200: description: Removed account @@ -575,15 +608,15 @@ paths: get: summary: Get the account information on blockchain tags: - - ICS1 + - ICS1 produces: - - application/json + - application/json parameters: - - in: path - name: address - description: Account address - required: true - type: string + - in: path + name: address + description: Account address + required: true + type: string responses: 200: description: Account information on the blockchain @@ -613,17 +646,17 @@ paths: description: Server internel error /staking/delegators/{delegatorAddr}/delegations: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Get all delegations from a delegator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -638,26 +671,26 @@ paths: post: summary: Submit delegation parameters: - - in: body - name: delegation - description: The password of the account to remove from the KMS - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_addr: - $ref: "#/definitions/Address" - validator_addr: - $ref: "#/definitions/ValidatorAddress" - delegation: - $ref: "#/definitions/Coin" + - in: body + name: delegation + description: The password of the account to remove from the KMS + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + delegator_addr: + $ref: "#/definitions/Address" + validator_addr: + $ref: "#/definitions/ValidatorAddress" + delegation: + $ref: "#/definitions/Coin" tags: - - ICS21 + - ICS21 consumes: - - application/json + - application/json produces: - - application/json + - application/json responses: 200: description: OK @@ -671,22 +704,22 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/delegations/{validatorAddr}: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Query the current delegation between a delegator and a validator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -698,17 +731,17 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/unbonding_delegations: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Get all unbonding delegations from a delegator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -723,37 +756,27 @@ paths: post: summary: Submit an unbonding delegation parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: delegation - description: The password of the account to remove from the KMS - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_addr: - $ref: "#/definitions/Address" - validator_addr: - $ref: "#/definitions/ValidatorAddress" - shares: - type: string - example: "100" + - in: body + name: delegation + description: The password of the account to remove from the KMS + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + delegator_addr: + $ref: "#/definitions/Address" + validator_addr: + $ref: "#/definitions/ValidatorAddress" + shares: + type: string + example: "100" tags: - - ICS21 + - ICS21 consumes: - - application/json + - application/json produces: - - application/json + - application/json responses: 200: description: OK @@ -767,22 +790,22 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr}: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Query all unbonding delegations between a delegator and a validator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -796,27 +819,27 @@ paths: description: Internal Server Error /staking/redelegations: parameters: - - in: query - name: delegator - description: Bech32 AccAddress of Delegator - required: false - type: string - - in: query - name: validator_from - description: Bech32 ValAddress of SrcValidator - required: false - type: string - - in: query - name: validator_to - description: Bech32 ValAddress of DstValidator - required: false - type: string + - in: query + name: delegator + description: Bech32 AccAddress of Delegator + required: false + type: string + - in: query + name: validator_from + description: Bech32 ValAddress of SrcValidator + required: false + type: string + - in: query + name: validator_to + description: Bech32 ValAddress of DstValidator + required: false + type: string get: summary: Get all redelegations (filter by query params) tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -828,41 +851,31 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/redelegations: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string post: summary: Submit a redelegation parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: delegation - description: The password of the account to remove from the KMS - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_addr: - $ref: "#/definitions/Address" - validator_src_addr: - $ref: "#/definitions/ValidatorAddress" - validator_dst_addr: - $ref: "#/definitions/ValidatorAddress" - shares: - type: string - example: "100" + - in: body + name: delegation + description: The password of the account to remove from the KMS + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + delegator_addr: + $ref: "#/definitions/Address" + validator_src_addr: + $ref: "#/definitions/ValidatorAddress" + validator_dst_addr: + $ref: "#/definitions/ValidatorAddress" + shares: + type: string + example: "100" tags: - ICS21 consumes: @@ -882,17 +895,17 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/validators: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Query all validators that a delegator is bonded to tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -906,22 +919,22 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/validators/{validatorAddr}: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string - - in: path - name: validatorAddr - description: Bech32 ValAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string + - in: path + name: validatorAddr + description: Bech32 ValAddress of Delegator + required: true + type: string get: summary: Query a validator that a delegator is bonded to tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -933,17 +946,17 @@ paths: description: Internal Server Error /staking/delegators/{delegatorAddr}/txs: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Get all staking txs (i.e msgs) from a delegator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -961,9 +974,9 @@ paths: get: summary: Get all validator candidates tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -975,17 +988,17 @@ paths: description: Internal Server Error /staking/validators/{validatorAddr}: parameters: - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Query the information from a single validator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -997,17 +1010,17 @@ paths: description: Internal Server Error /staking/validators/{validatorAddr}/delegations: parameters: - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Get all delegations from a validator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -1021,17 +1034,17 @@ paths: description: Internal Server Error /staking/validators/{validatorAddr}/unbonding_delegations: parameters: - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Get all unbonding delegations from a validator tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -1047,9 +1060,9 @@ paths: get: summary: Get the current state of the staking pool tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -1074,9 +1087,9 @@ paths: get: summary: Get the current staking parameter values tags: - - ICS21 + - ICS21 produces: - - application/json + - application/json responses: 200: description: OK @@ -1104,15 +1117,15 @@ paths: summary: Get sign info of given validator description: Get sign info of given validator produces: - - application/json + - application/json tags: - - ICS23 + - ICS23 parameters: - - type: string - description: Bech32 validator public key - name: validatorPubKey - required: true - in: path + - type: string + description: Bech32 validator public key + name: validatorPubKey + required: true + in: path responses: 200: description: OK @@ -1138,26 +1151,26 @@ paths: summary: Unjail a jailed validator description: Send transaction to unjail a jailed validator consumes: - - application/json + - application/json produces: - - application/json + - application/json tags: - - ICS23 + - ICS23 parameters: - - type: string - description: Bech32 validator address - name: validatorAddr - required: true - in: path - - description: '' - name: UnjailBody - in: body - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" + - type: string + description: Bech32 validator address + name: validatorAddr + required: true + in: path + - description: "" + name: UnjailBody + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" responses: 200: description: OK @@ -1173,9 +1186,9 @@ paths: get: summary: Get the current slashing parameters tags: - - ICS23 + - ICS23 produces: - - application/json + - application/json responses: 200: description: OK @@ -1203,34 +1216,34 @@ paths: summary: Submit a proposal description: Send transaction to submit a proposal consumes: - - application/json + - application/json produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - description: valid value of `"proposal_type"` can be `"text"`, `"parameter_change"`, `"software_upgrade"` - name: post_proposal_body - in: body - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - title: - type: string - description: - type: string - proposal_type: - type: string - example: "text" - proposer: - $ref: "#/definitions/Address" - initial_deposit: - type: array - items: - $ref: "#/definitions/Coin" + - description: valid value of `"proposal_type"` can be `"text"`, `"parameter_change"`, `"software_upgrade"` + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + description: + type: string + proposal_type: + type: string + example: "text" + proposer: + $ref: "#/definitions/Address" + initial_deposit: + type: array + items: + $ref: "#/definitions/Coin" responses: 200: description: OK @@ -1246,25 +1259,25 @@ paths: summary: Query proposals description: Query proposals information with parameters produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - in: query - name: voter - description: voter address - required: false - type: string - - in: query - name: depositor - description: depositor address - required: false - type: string - - in: query - name: status - description: proposal status, valid values can be `"deposit_period"`, `"voting_period"`, `"passed"`, `"rejected"` - required: false - type: string + - in: query + name: voter + description: voter address + required: false + type: string + - in: query + name: depositor + description: depositor address + required: false + type: string + - in: query + name: status + description: proposal status, valid values can be `"deposit_period"`, `"voting_period"`, `"passed"`, `"rejected"` + required: false + type: string responses: 200: description: OK @@ -1281,14 +1294,14 @@ paths: summary: Query a proposal description: Query a proposal by id produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - name: proposalId - required: true - in: path + - type: string + name: proposalId + required: true + in: path responses: 200: description: OK @@ -1303,14 +1316,14 @@ paths: summary: Query proposer description: Query for the proposer for a proposal produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - name: proposalId - required: true - in: path + - type: string + name: proposalId + required: true + in: path responses: 200: description: OK @@ -1325,14 +1338,14 @@ paths: summary: Query deposits description: Query deposits by proposalId produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - name: proposalId - required: true - in: path + - type: string + name: proposalId + required: true + in: path responses: 200: description: OK @@ -1348,32 +1361,32 @@ paths: summary: Deposit tokens to a proposal description: Send transaction to deposit tokens to a proposal consumes: - - application/json + - application/json produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path - - description: '' - name: post_deposit_body - in: body - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - depositor: - $ref: "#/definitions/Address" - amount: - type: array - items: - $ref: "#/definitions/Coin" + - type: string + description: proposal id + name: proposalId + required: true + in: path + - description: "" + name: post_deposit_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + depositor: + $ref: "#/definitions/Address" + amount: + type: array + items: + $ref: "#/definitions/Coin" responses: 200: description: OK @@ -1390,20 +1403,20 @@ paths: summary: Query deposit description: Query deposit by proposalId and depositor address produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path - - type: string - description: Bech32 depositor address - name: depositor - required: true - in: path + - type: string + description: proposal id + name: proposalId + required: true + in: path + - type: string + description: Bech32 depositor address + name: depositor + required: true + in: path responses: 200: description: OK @@ -1420,15 +1433,15 @@ paths: summary: Query voters description: Query voters information by proposalId produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path + - type: string + description: proposal id + name: proposalId + required: true + in: path responses: 200: description: OK @@ -1444,31 +1457,31 @@ paths: summary: Vote a proposal description: Send transaction to vote a proposal consumes: - - application/json + - application/json produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path - - description: valid value of `"option"` field can be `"yes"`, `"no"`, `"no_with_veto"` and `"abstain"` - name: post_vote_body - in: body - required: true - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - voter: - $ref: "#/definitions/Address" - option: - type: string - example: "yes" + - type: string + description: proposal id + name: proposalId + required: true + in: path + - description: valid value of `"option"` field can be `"yes"`, `"no"`, `"no_with_veto"` and `"abstain"` + name: post_vote_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + voter: + $ref: "#/definitions/Address" + option: + type: string + example: "yes" responses: 200: description: OK @@ -1485,20 +1498,20 @@ paths: summary: Query vote description: Query vote information by proposal Id and voter address produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path - - type: string - description: Bech32 voter address - name: voter - required: true - in: path + - type: string + description: proposal id + name: proposalId + required: true + in: path + - type: string + description: Bech32 voter address + name: voter + required: true + in: path responses: 200: description: OK @@ -1515,15 +1528,15 @@ paths: summary: Get a proposal's tally result at the current time description: Gets a proposal's tally result at the current time. If the proposal is pending deposits (i.e status 'DepositPeriod') it returns an empty tally result. produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 parameters: - - type: string - description: proposal id - name: proposalId - required: true - in: path + - type: string + description: proposal id + name: proposalId + required: true + in: path responses: 200: description: OK @@ -1538,9 +1551,9 @@ paths: summary: Query governance deposit parameters description: Query governance deposit parameters. The max_deposit_period units are in nanoseconds. produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 responses: 200: description: OK @@ -1565,9 +1578,9 @@ paths: summary: Query governance tally parameters description: Query governance tally parameters produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 responses: 200: description: OK @@ -1593,9 +1606,9 @@ paths: summary: Query governance voting parameters description: Query governance voting parameters. The voting_period units are in nanoseconds. produces: - - application/json + - application/json tags: - - ICS22 + - ICS22 responses: 200: description: OK @@ -1612,18 +1625,18 @@ paths: description: Internal Server Error /distribution/delegators/{delegatorAddr}/rewards: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Get the total rewards balance from all delegations description: Get the sum of all the rewards earned by delegations by a single delegator produces: - - application/json + - application/json tags: - - ICS24 + - ICS24 responses: 200: description: OK @@ -1639,28 +1652,18 @@ paths: summary: Withdraw all the delegator's delegation rewards description: Withdraw all the delegator's delegation rewards tags: - - ICS24 + - ICS24 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: Withdraw request body - schema: - properties: - base_req: - $ref: "#/definitions/BaseReq" + - in: body + name: Withdraw request body + schema: + properties: + base_req: + $ref: "#/definitions/BaseReq" responses: 200: description: OK @@ -1674,23 +1677,23 @@ paths: description: Internal Server Error /distribution/delegators/{delegatorAddr}/rewards/{validatorAddr}: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string - - in: path - name: validatorAddr - description: Bech32 OperatorAddress of validator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string + - in: path + name: validatorAddr + description: Bech32 OperatorAddress of validator + required: true + type: string get: summary: Query a delegation reward description: Query a single delegation reward by a delegator tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK @@ -1706,28 +1709,18 @@ paths: summary: Withdraw a delegation reward description: Withdraw a delegator's delegation reward from a single validator tags: - - ICS24 + - ICS24 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: Withdraw request body - schema: - properties: - base_req: - $ref: "#/definitions/BaseReq" + - in: body + name: Withdraw request body + schema: + properties: + base_req: + $ref: "#/definitions/BaseReq" responses: 200: description: OK @@ -1741,18 +1734,18 @@ paths: description: Internal Server Error /distribution/delegators/{delegatorAddr}/withdraw_address: parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string + - in: path + name: delegatorAddr + description: Bech32 AccAddress of Delegator + required: true + type: string get: summary: Get the rewards withdrawal address description: Get the delegations' rewards withdrawal address. This is the address in which the user will receive the reward funds tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK @@ -1766,30 +1759,20 @@ paths: summary: Replace the rewards withdrawal address description: Replace the delegations' rewards withdrawal address for a new one. tags: - - ICS24 + - ICS24 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: Withdraw request body - schema: - properties: - base_req: - $ref: "#/definitions/BaseReq" - withdraw_address: - $ref: "#/definitions/Address" + - in: body + name: Withdraw request body + schema: + properties: + base_req: + $ref: "#/definitions/BaseReq" + withdraw_address: + $ref: "#/definitions/Address" responses: 200: description: OK @@ -1812,9 +1795,9 @@ paths: summary: Validator distribution information description: Query the distribution information of a single validator tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK @@ -1835,9 +1818,9 @@ paths: summary: Commission and self-delegation rewards of a single a validator description: Query the commission and self-delegation rewards of a validator. tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK @@ -1853,28 +1836,18 @@ paths: summary: Withdraw the validator's rewards description: Withdraw the validator's self-delegation and commissions rewards tags: - - ICS24 + - ICS24 consumes: - - application/json + - application/json produces: - - application/json + - application/json parameters: - - in: query - name: simulate - description: if true, ignore the gas field and perform a simulation of a transaction, but don't broadcast it - required: false - type: boolean - - in: query - name: generate_only - description: if true, build an unsigned transaction and write it back - required: false - type: boolean - - in: body - name: Withdraw request body - schema: - properties: - base_req: - $ref: "#/definitions/BaseReq" + - in: body + name: Withdraw request body + schema: + properties: + base_req: + $ref: "#/definitions/BaseReq" responses: 200: description: OK @@ -1890,9 +1863,9 @@ paths: get: summary: Fee distribution parameters tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK @@ -1906,18 +1879,20 @@ paths: type: string 500: description: Internal Server Error - /distribution/pool: + /distribution/outstanding_rewards: get: - summary: Fee distribution pool + summary: Fee distribution outstanding rewards tags: - - ICS24 + - ICS24 produces: - - application/json + - application/json responses: 200: description: OK schema: - $ref: "#/definitions/FeePool" + type: array + items: + $ref: "#/definitions/Coin" 500: description: Internal Server Error definitions: @@ -1948,8 +1923,8 @@ definitions: gas_wanted: 10000 info: info tags: - - '' - - '' + - "" + - "" DeliverTxResult: type: object properties: @@ -1977,8 +1952,8 @@ definitions: gas_wanted: 10000 info: info tags: - - '' - - '' + - "" + - "" BroadcastTxCommitResult: type: object properties: @@ -2123,7 +2098,7 @@ definitions: example: 1 time: type: string - example: '2017-12-30T05:53:09.287+01:00' + example: "2017-12-30T05:53:09.287+01:00" num_txs: type: number example: 0 @@ -2186,7 +2161,7 @@ definitions: example: "0" timestamp: type: string - example: '2017-12-30T05:53:09.287+01:00' + example: "2017-12-30T05:53:09.287+01:00" type: type: number example: 2 @@ -2194,7 +2169,7 @@ definitions: $ref: "#/definitions/BlockID" signature: type: string - example: '7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ==' + example: "7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ==" BlockQuery: type: object properties: @@ -2210,9 +2185,10 @@ definitions: BaseReq: type: object properties: - name: + from: type: string - example: "my_name" + example: "cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc" + description: Sender address or Keybase name to generate a transaction password: type: string example: "12345678" @@ -2238,28 +2214,26 @@ definitions: type: array items: $ref: "#/definitions/Coin" - gas_prices: - type: array - items: - $ref: "#/definitions/DecCoin" generate_only: type: boolean example: false + description: Create a JSON transaction that can be signed client side instead of actually signing and broadcasting simulate: type: boolean - example: true + example: false + description: Estimate gas for a transaction (cannot be used in conjunction with generate_only) TendermintValidator: type: object properties: address: - $ref: '#/definitions/ValidatorAddress' + $ref: "#/definitions/ValidatorAddress" pub_key: type: string example: cosmosvalconspub1zcjduepq7sjfglw7ra4mjxpw4ph7dtdhdheh7nz8dfgl6t8u2n5szuuql9mqsrwquu power: type: string example: "1000" - accum: + proposer_priority: type: string example: "1000" TextProposal: @@ -2276,7 +2250,7 @@ definitions: proposal_status: type: string final_tally_result: - $ref: "#/definitions/TallyResult" + $ref: "#/definitions/TallyResult" submit_time: type: string total_deposit: @@ -2331,7 +2305,7 @@ definitions: type: object properties: operator_address: - $ref: '#/definitions/ValidatorAddress' + $ref: "#/definitions/ValidatorAddress" consensus_pubkey: type: string example: cosmosvalconspub1zcjduepq7sjfglw7ra4mjxpw4ph7dtdhdheh7nz8dfgl6t8u2n5szuuql9mqsrwquu @@ -2356,31 +2330,31 @@ definitions: type: string bond_height: type: string - example: '0' + example: "0" bond_intra_tx_counter: type: integer example: 0 unbonding_height: type: string - example: '0' + example: "0" unbonding_time: type: string - example: '1970-01-01T00:00:00Z' + example: "1970-01-01T00:00:00Z" commission: type: object properties: rate: type: string - example: '0' + example: "0" max_rate: type: string - example: '0' + example: "0" max_change_rate: type: string - example: '0' + example: "0" update_time: type: string - example: '1970-01-01T00:00:00Z' + example: "1970-01-01T00:00:00Z" Delegation: type: object properties: @@ -2428,40 +2402,16 @@ definitions: type: string shares_dst: type: string - FeePool: - type: object - properties: - community_pool: - type: array - items: - $ref: '#/definitions/Coin' - val_accum: - $ref: '#/definitions/TotalAccum' - val_pool: - type: array - items: - $ref: '#/definitions/Coin' - TotalAccum: - type: object - properties: - update_height: - type: integer - accum: - type: string ValidatorDistInfo: type: object properties: operator_addr: - $ref: '#/definitions/ValidatorAddress' - fee_pool_withdrawal_height: - type: integer - del_accum: - $ref: '#/definitions/TotalAccum' - del_pool: + $ref: "#/definitions/ValidatorAddress" + self_bond_rewards: type: array items: - $ref: '#/definitions/Coin' + $ref: "#/definitions/Coin" val_commission: type: array items: - $ref: '#/definitions/Coin' + $ref: "#/definitions/Coin" diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index da41e196b..9e6f08fe9 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -15,20 +15,14 @@ import ( "strings" "testing" - "github.com/tendermint/tendermint/crypto/secp256k1" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" - authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/slashing" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/rest" "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/cosmos/cosmos-sdk/client/utils" gapp "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/codec" crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" @@ -36,16 +30,24 @@ import ( "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + txbuilder "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + bankrest "github.com/cosmos/cosmos-sdk/x/bank/client/rest" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + distrrest "github.com/cosmos/cosmos-sdk/x/distribution/client/rest" + "github.com/cosmos/cosmos-sdk/x/gov" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingrest "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" "github.com/cosmos/cosmos-sdk/x/staking" - - "github.com/spf13/viper" - "github.com/stretchr/testify/require" + stakingrest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" abci "github.com/tendermint/tendermint/abci/types" tmcfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/libs/cli" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -53,16 +55,9 @@ import ( "github.com/tendermint/tendermint/p2p" pvm "github.com/tendermint/tendermint/privval" "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" - - txbuilder "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" - - authRest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" - bankRest "github.com/cosmos/cosmos-sdk/x/bank/client/rest" - govRest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" - slashingRest "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" - stakingRest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" ) // makePathname creates a unique pathname for each test. It will panic if it @@ -104,30 +99,6 @@ func GetConfig() *tmcfg.Config { return config } -// GetKeyBase returns the LCD test keybase. It also requires that a directory -// could be made and a keybase could be fetched. -// -// NOTE: memDB cannot be used because the request is expecting to interact with -// the default location. -func GetKeyBase(t *testing.T) crkeys.Keybase { - dir, err := ioutil.TempDir("", "lcd_test") - require.NoError(t, err) - - viper.Set(cli.HomeFlag, dir) - - keybase, err := keys.GetKeyBaseWithWritePerm() - require.NoError(t, err) - - return keybase -} - -// GetTestKeyBase fetches the current testing keybase -func GetTestKeyBase(t *testing.T) crkeys.Keybase { - keybase, err := keys.GetKeyBaseWithWritePerm() - require.NoError(t, err) - return keybase -} - // CreateAddr adds an address to the key store and returns an address and seed. // It also requires that the key could be created. func CreateAddr(t *testing.T, name, password string, kb crkeys.Keybase) (sdk.AccAddress, string) { @@ -143,14 +114,6 @@ func CreateAddr(t *testing.T, name, password string, kb crkeys.Keybase) (sdk.Acc return sdk.AccAddress(info.GetPubKey().Address()), seed } -// Type that combines an Address with the pnemonic of the private key to that address -type AddrSeed struct { - Address sdk.AccAddress - Seed string - Name string - Password string -} - // CreateAddr adds multiple address to the key store and returns the addresses and associated seeds in lexographical order by address. // It also requires that the keys could be created. func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.AccAddress, seeds, names, passwords []string) { @@ -167,7 +130,7 @@ func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.Acc password := "1234567890" info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) require.NoError(t, err) - addrSeeds = append(addrSeeds, AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) + addrSeeds = append(addrSeeds, rest.AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) } sort.Sort(addrSeeds) @@ -182,14 +145,14 @@ func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.Acc return addrs, seeds, names, passwords } -// implement `Interface` in sort package. -type AddrSeedSlice []AddrSeed +// AddrSeedSlice implements `Interface` in sort package. +type AddrSeedSlice []rest.AddrSeed func (b AddrSeedSlice) Len() int { return len(b) } -// Sorts lexographically by Address +// Less sorts lexicographically by Address func (b AddrSeedSlice) Less(i, j int) bool { // bytes package already implements Comparable for []byte. switch bytes.Compare(b[i].Address.Bytes(), b[j].Address.Bytes()) { @@ -206,14 +169,27 @@ func (b AddrSeedSlice) Swap(i, j int) { b[j], b[i] = b[i], b[j] } +// InitClientHome initialises client home dir. +func InitClientHome(t *testing.T, dir string) string { + var err error + if dir == "" { + dir, err = ioutil.TempDir("", "lcd_test") + require.NoError(t, err) + } + // TODO: this should be set in NewRestServer + // and pass down the CLIContext to achieve + // parallelism. + viper.Set(cli.HomeFlag, dir) + return dir +} + // TODO: Make InitializeTestLCD safe to call in multiple tests at the same time // InitializeTestLCD starts Tendermint and the LCD in process, listening on // their respective sockets where nValidators is the total number of validators // and initAddrs are the accounts to initialize with some steak tokens. It // returns a cleanup function, a set of validator public keys, and a port. -func InitializeTestLCD( - t *testing.T, nValidators int, initAddrs []sdk.AccAddress, -) (cleanup func(), valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, port string) { +func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress, minting bool) ( + cleanup func(), valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, port string) { if nValidators < 1 { panic("InitializeTestLCD must use at least one validator") @@ -248,17 +224,21 @@ func InitializeTestLCD( operPrivKey := secp256k1.GenPrivKey() operAddr := operPrivKey.PubKey().Address() pubKey := privVal.GetPubKey() - delegation := 100 + + power := int64(100) if i > 0 { pubKey = ed25519.GenPrivKey().PubKey() - delegation = 1 + power = 1 } + startTokens := staking.TokensFromTendermintPower(power) + msg := staking.NewMsgCreateValidator( sdk.ValAddress(operAddr), pubKey, - sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(int64(delegation))), - staking.Description{Moniker: fmt.Sprintf("validator-%d", i+1)}, + sdk.NewCoin(staking.DefaultBondDenom, startTokens), + staking.NewDescription(fmt.Sprintf("validator-%d", i+1), "", "", ""), staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), ) stdSignMsg := txbuilder.StdSignMsg{ ChainID: genDoc.ChainID, @@ -269,12 +249,14 @@ func InitializeTestLCD( tx := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{{Signature: sig, PubKey: operPrivKey.PubKey()}}, "") txBytes, err := cdc.MarshalJSON(tx) require.Nil(t, err) + genTxs = append(genTxs, txBytes) valConsPubKeys = append(valConsPubKeys, pubKey) valOperAddrs = append(valOperAddrs, sdk.ValAddress(operAddr)) accAuth := auth.NewBaseAccountWithAddress(sdk.AccAddress(operAddr)) - accAuth.Coins = sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 150)} + accTokens := staking.TokensFromTendermintPower(150) + accAuth.Coins = sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, accTokens)} accs = append(accs, gapp.NewGenesisAccount(&accAuth)) } @@ -288,10 +270,34 @@ func InitializeTestLCD( // add some tokens to init accounts for _, addr := range initAddrs { accAuth := auth.NewBaseAccountWithAddress(addr) - accAuth.Coins = sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 100)} + accTokens := staking.TokensFromTendermintPower(100) + accAuth.Coins = sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, accTokens)} acc := gapp.NewGenesisAccount(&accAuth) genesisState.Accounts = append(genesisState.Accounts, acc) - genesisState.StakingData.Pool.NotBondedTokens = genesisState.StakingData.Pool.NotBondedTokens.Add(sdk.NewInt(100)) + genesisState.StakingData.Pool.NotBondedTokens = genesisState.StakingData.Pool.NotBondedTokens.Add(accTokens) + } + + inflationMin := sdk.ZeroDec() + if minting { + inflationMin = sdk.MustNewDecFromStr("10000.0") + genesisState.MintData.Params.InflationMax = sdk.MustNewDecFromStr("15000.0") + } else { + genesisState.MintData.Params.InflationMax = inflationMin + } + genesisState.MintData.Minter.Inflation = inflationMin + genesisState.MintData.Params.InflationMin = inflationMin + + // double check inflation is set according to the minting boolean flag + if minting { + require.Equal(t, sdk.MustNewDecFromStr("15000.0"), + genesisState.MintData.Params.InflationMax) + require.Equal(t, sdk.MustNewDecFromStr("10000.0"), genesisState.MintData.Minter.Inflation) + require.Equal(t, sdk.MustNewDecFromStr("10000.0"), + genesisState.MintData.Params.InflationMin) + } else { + require.Equal(t, sdk.ZeroDec(), genesisState.MintData.Params.InflationMax) + require.Equal(t, sdk.ZeroDec(), genesisState.MintData.Minter.Inflation) + require.Equal(t, sdk.ZeroDec(), genesisState.MintData.Params.InflationMin) } appState, err := codec.MarshalJSONIndent(cdc, genesisState) @@ -306,9 +312,6 @@ func InitializeTestLCD( viper.Set(client.FlagChainID, genDoc.ChainID) // TODO Set to false once the upstream Tendermint proof verification issue is fixed. viper.Set(client.FlagTrustNode, true) - dir, err := ioutil.TempDir("", "lcd_test") - require.NoError(t, err) - viper.Set(cli.HomeFlag, dir) node, err := startTM(config, logger, genDoc, privVal, app) require.NoError(t, err) @@ -339,6 +342,7 @@ func startTM( tmcfg *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 } nodeKey, err := p2p.LoadOrGenNodeKey(tmcfg.NodeKeyFile()) @@ -373,7 +377,6 @@ func startTM( // startLCD starts the LCD. func startLCD(logger log.Logger, listenAddr string, cdc *codec.Codec, t *testing.T) (net.Listener, error) { rs := NewRestServer(cdc) - rs.setKeybase(GetTestKeyBase(t)) registerRoutes(rs) listener, err := tmrpc.Listen(listenAddr, tmrpc.Config{}) if err != nil { @@ -388,11 +391,12 @@ func registerRoutes(rs *RestServer) { keys.RegisterRoutes(rs.Mux, rs.CliCtx.Indent) rpc.RegisterRoutes(rs.CliCtx, rs.Mux) tx.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) - authRest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, auth.StoreKey) - bankRest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) - stakingRest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) - slashingRest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) - govRest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) + authrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, auth.StoreKey) + bankrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) + distrrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, distr.StoreKey) + stakingrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) + slashingrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) + govrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) } // Request makes a test LCD test request. It returns a response object and a @@ -498,8 +502,8 @@ func getValidatorSets(t *testing.T, port string, height int, expectFail bool) rp } // GET /txs/{hash} get tx by hash -func getTransaction(t *testing.T, port string, hash string) tx.Info { - var tx tx.Info +func getTransaction(t *testing.T, port string, hash string) sdk.TxResponse { + var tx sdk.TxResponse res, body := Request(t, port, "GET", fmt.Sprintf("/txs/%s", hash), nil) require.Equal(t, http.StatusOK, res.StatusCode, body) @@ -511,8 +515,8 @@ func getTransaction(t *testing.T, port string, hash string) tx.Info { // POST /txs broadcast txs // GET /txs search transactions -func getTransactions(t *testing.T, port string, tags ...string) []tx.Info { - var txs []tx.Info +func getTransactions(t *testing.T, port string, tags ...string) []sdk.TxResponse { + var txs []sdk.TxResponse if len(tags) == 0 { return txs } @@ -539,10 +543,11 @@ func getKeys(t *testing.T, port string) []keys.KeyOutput { } // POST /keys Create a new account locally -func doKeysPost(t *testing.T, port, name, password, seed string) keys.KeyOutput { - pk := postKeys{name, password, seed} +func doKeysPost(t *testing.T, port, name, password, mnemonic string, account int, index int) keys.KeyOutput { + pk := keys.AddNewKey{name, password, mnemonic, account, index} req, err := cdc.MarshalJSON(pk) require.NoError(t, err) + res, body := Request(t, port, "POST", "/keys", req) require.Equal(t, http.StatusOK, res.StatusCode, body) @@ -552,12 +557,6 @@ func doKeysPost(t *testing.T, port, name, password, seed string) keys.KeyOutput return resp } -type postKeys struct { - Name string `json:"name"` - Password string `json:"password"` - Seed string `json:"seed"` -} - // GET /keys/seed Create a new seed to create a new account defaultValidFor func getKeysSeed(t *testing.T, port string) string { res, body := Request(t, port, "GET", "/keys/seed", nil) @@ -569,14 +568,17 @@ func getKeysSeed(t *testing.T, port string) string { return body } -// POST /keys/{name}/recover Recover a account from a seed -func doRecoverKey(t *testing.T, port, recoverName, recoverPassword, seed string) { - jsonStr := []byte(fmt.Sprintf(`{"password":"%s", "seed":"%s"}`, recoverPassword, seed)) - res, body := Request(t, port, "POST", fmt.Sprintf("/keys/%s/recover", recoverName), jsonStr) +// POST /keys/{name}/recove Recover a account from a seed +func doRecoverKey(t *testing.T, port, recoverName, recoverPassword, mnemonic string, account uint32, index uint32) { + pk := keys.RecoverKey{recoverPassword, mnemonic, int(account), int(index)} + req, err := cdc.MarshalJSON(pk) + require.NoError(t, err) + + res, body := Request(t, port, "POST", fmt.Sprintf("/keys/%s/recover", recoverName), req) require.Equal(t, http.StatusOK, res.StatusCode, body) var resp keys.KeyOutput - err := codec.Cdc.UnmarshalJSON([]byte(body), &resp) + err = codec.Cdc.UnmarshalJSON([]byte(body), &resp) require.Nil(t, err, body) addr1Bech32 := resp.Address @@ -596,7 +598,7 @@ func getKey(t *testing.T, port, name string) keys.KeyOutput { // PUT /keys/{name} Update the password for this account in the KMS func updateKey(t *testing.T, port, name, oldPassword, newPassword string, fail bool) { - kr := updateKeyReq{oldPassword, newPassword} + kr := keys.UpdateKeyReq{oldPassword, newPassword} req, err := cdc.MarshalJSON(kr) require.NoError(t, err) keyEndpoint := fmt.Sprintf("/keys/%s", name) @@ -608,14 +610,9 @@ func updateKey(t *testing.T, port, name, oldPassword, newPassword string, fail b require.Equal(t, http.StatusOK, res.StatusCode, body) } -type updateKeyReq struct { - OldPassword string `json:"old_password"` - NewPassword string `json:"new_password"` -} - // DELETE /keys/{name} Remove an account func deleteKey(t *testing.T, port, name, password string) { - dk := deleteKeyReq{password} + dk := keys.DeleteKeyReq{password} req, err := cdc.MarshalJSON(dk) require.NoError(t, err) keyEndpoint := fmt.Sprintf("/keys/%s", name) @@ -623,10 +620,6 @@ func deleteKey(t *testing.T, port, name, password string) { require.Equal(t, http.StatusOK, res.StatusCode, body) } -type deleteKeyReq struct { - Password string `json:"password"` -} - // GET /auth/accounts/{address} Get the account information on blockchain func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr.String()), nil) @@ -646,7 +639,7 @@ func doSign(t *testing.T, port, name, password, chainID string, accnum, sequence var signedMsg auth.StdTx payload := authrest.SignBody{ Tx: msg, - BaseReq: utils.NewBaseReq( + BaseReq: rest.NewBaseReq( name, password, "", chainID, "", "", accnum, sequence, nil, nil, false, false, ), } @@ -659,26 +652,21 @@ func doSign(t *testing.T, port, name, password, chainID string, accnum, sequence } // POST /tx/broadcast Send a signed Tx -func doBroadcast(t *testing.T, port string, msg auth.StdTx) ctypes.ResultBroadcastTxCommit { - tx := broadcastReq{Tx: msg, Return: "block"} +func doBroadcast(t *testing.T, port string, msg auth.StdTx) sdk.TxResponse { + tx := rest.BroadcastReq{Tx: msg, Return: "block"} req, err := cdc.MarshalJSON(tx) require.Nil(t, err) res, body := Request(t, port, "POST", "/tx/broadcast", req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var resultTx ctypes.ResultBroadcastTxCommit + var resultTx sdk.TxResponse require.Nil(t, cdc.UnmarshalJSON([]byte(body), &resultTx)) return resultTx } -type broadcastReq struct { - Tx auth.StdTx `json:"tx"` - Return string `json:"return"` -} - // GET /bank/balances/{address} Get the account balances // POST /bank/accounts/{address}/transfers Send coins (build -> sign -> send) -func doTransfer(t *testing.T, port, seed, name, memo, password string, addr sdk.AccAddress, fees sdk.Coins) (receiveAddr sdk.AccAddress, resultTx ctypes.ResultBroadcastTxCommit) { +func doTransfer(t *testing.T, port, seed, name, memo, password string, addr sdk.AccAddress, fees sdk.Coins) (receiveAddr sdk.AccAddress, resultTx sdk.TxResponse) { res, body, receiveAddr := doTransferWithGas(t, port, seed, name, memo, password, addr, "", 1.0, false, false, fees) require.Equal(t, http.StatusOK, res.StatusCode, body) @@ -688,29 +676,71 @@ func doTransfer(t *testing.T, port, seed, name, memo, password string, addr sdk. return receiveAddr, resultTx } -func doTransferWithGas(t *testing.T, port, seed, name, memo, password string, addr sdk.AccAddress, gas string, - gasAdjustment float64, simulate, generateOnly bool, fees sdk.Coins) ( - res *http.Response, body string, receiveAddr sdk.AccAddress) { +func doTransferWithGas( + t *testing.T, port, seed, from, memo, password string, addr sdk.AccAddress, + gas string, gasAdjustment float64, simulate, generateOnly bool, fees sdk.Coins, +) (res *http.Response, body string, receiveAddr sdk.AccAddress) { // create receive address kb := client.MockKeyBase() - receiveInfo, _, err := kb.CreateMnemonic("receive_address", cryptoKeys.English, gapp.DefaultKeyPass, cryptoKeys.SigningAlgo("secp256k1")) - require.Nil(t, err) - receiveAddr = sdk.AccAddress(receiveInfo.GetPubKey().Address()) + receiveInfo, _, err := kb.CreateMnemonic( + "receive_address", crkeys.English, gapp.DefaultKeyPass, crkeys.SigningAlgo("secp256k1"), + ) + require.Nil(t, err) + + receiveAddr = sdk.AccAddress(receiveInfo.GetPubKey().Address()) acc := getAccount(t, port, addr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq( - name, password, memo, chainID, gas, + if generateOnly { + // generate only txs do not use a Keybase so the address must be used + from = addr.String() + } + + baseReq := rest.NewBaseReq( + from, password, memo, chainID, gas, fmt.Sprintf("%f", gasAdjustment), accnum, sequence, fees, nil, generateOnly, simulate, ) - sr := sendReq{ - Amount: sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 1)}, + sr := rest.SendReq{ + Amount: sdk.Coins{sdk.NewInt64Coin(staking.DefaultBondDenom, 1)}, + BaseReq: baseReq, + } + + req, err := cdc.MarshalJSON(sr) + require.NoError(t, err) + + res, body = Request(t, port, "POST", fmt.Sprintf("/bank/accounts/%s/transfers", receiveAddr), req) + return +} + +func doTransferWithGasAccAuto( + t *testing.T, port, seed, from, memo, password string, gas string, + gasAdjustment float64, simulate, generateOnly bool, fees sdk.Coins, +) (res *http.Response, body string, receiveAddr sdk.AccAddress) { + + // create receive address + kb := client.MockKeyBase() + + receiveInfo, _, err := kb.CreateMnemonic( + "receive_address", crkeys.English, gapp.DefaultKeyPass, crkeys.SigningAlgo("secp256k1"), + ) + require.Nil(t, err) + + receiveAddr = sdk.AccAddress(receiveInfo.GetPubKey().Address()) + chainID := viper.GetString(client.FlagChainID) + + baseReq := rest.NewBaseReq( + from, password, memo, chainID, gas, + fmt.Sprintf("%f", gasAdjustment), 0, 0, fees, nil, generateOnly, simulate, + ) + + sr := rest.SendReq{ + Amount: sdk.Coins{sdk.NewInt64Coin(staking.DefaultBondDenom, 1)}, BaseReq: baseReq, } @@ -722,8 +752,8 @@ func doTransferWithGas(t *testing.T, port, seed, name, memo, password string, ad } type sendReq struct { - Amount sdk.Coins `json:"amount"` - BaseReq utils.BaseReq `json:"base_req"` + Amount sdk.Coins `json:"amount"` + BaseReq rest.BaseReq `json:"base_req"` } // ---------------------------------------------------------------------- @@ -732,24 +762,25 @@ type sendReq struct { // POST /staking/delegators/{delegatorAddr}/delegations Submit delegation func doDelegate(t *testing.T, port, name, password string, - delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount int64, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { + delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Int, fees sdk.Coins) (resultTx sdk.TxResponse) { + acc := getAccount(t, port, delAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) msg := msgDelegationsInput{ BaseReq: baseReq, DelegatorAddr: delAddr, ValidatorAddr: valAddr, - Delegation: sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, amount), + Delegation: sdk.NewCoin(staking.DefaultBondDenom, amount), } req, err := cdc.MarshalJSON(msg) require.NoError(t, err) res, body := Request(t, port, "POST", fmt.Sprintf("/staking/delegators/%s/delegations", delAddr.String()), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var result ctypes.ResultBroadcastTxCommit + var result sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &result) require.Nil(t, err) @@ -757,7 +788,7 @@ func doDelegate(t *testing.T, port, name, password string, } type msgDelegationsInput struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // in bech32 ValidatorAddr sdk.ValAddress `json:"validator_addr"` // in bech32 Delegation sdk.Coin `json:"delegation"` @@ -765,18 +796,18 @@ type msgDelegationsInput struct { // POST /staking/delegators/{delegatorAddr}/delegations Submit delegation func doUndelegate(t *testing.T, port, name, password string, - delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount int64, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { + delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Int, fees sdk.Coins) (resultTx sdk.TxResponse) { acc := getAccount(t, port, delAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) msg := msgUndelegateInput{ BaseReq: baseReq, DelegatorAddr: delAddr, ValidatorAddr: valAddr, - SharesAmount: sdk.NewDec(amount), + SharesAmount: sdk.NewDecFromInt(amount), } req, err := cdc.MarshalJSON(msg) require.NoError(t, err) @@ -784,7 +815,7 @@ func doUndelegate(t *testing.T, port, name, password string, res, body := Request(t, port, "POST", fmt.Sprintf("/staking/delegators/%s/unbonding_delegations", delAddr), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var result ctypes.ResultBroadcastTxCommit + var result sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &result) require.Nil(t, err) @@ -792,7 +823,7 @@ func doUndelegate(t *testing.T, port, name, password string, } type msgUndelegateInput struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // in bech32 ValidatorAddr sdk.ValAddress `json:"validator_addr"` // in bech32 SharesAmount sdk.Dec `json:"shares"` @@ -800,21 +831,22 @@ type msgUndelegateInput struct { // POST /staking/delegators/{delegatorAddr}/delegations Submit delegation func doBeginRedelegation(t *testing.T, port, name, password string, - delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress, amount int64, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { + delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress, amount sdk.Int, + fees sdk.Coins) (resultTx sdk.TxResponse) { acc := getAccount(t, port, delAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - msg := msgBeginRedelegateInput{ + msg := rest.MsgBeginRedelegateInput{ BaseReq: baseReq, DelegatorAddr: delAddr, ValidatorSrcAddr: valSrcAddr, ValidatorDstAddr: valDstAddr, - SharesAmount: sdk.NewDec(amount), + SharesAmount: sdk.NewDecFromInt(amount), } req, err := cdc.MarshalJSON(msg) require.NoError(t, err) @@ -822,7 +854,7 @@ func doBeginRedelegation(t *testing.T, port, name, password string, res, body := Request(t, port, "POST", fmt.Sprintf("/staking/delegators/%s/redelegations", delAddr), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var result ctypes.ResultBroadcastTxCommit + var result sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &result) require.Nil(t, err) @@ -830,7 +862,7 @@ func doBeginRedelegation(t *testing.T, port, name, password string, } type msgBeginRedelegateInput struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // in bech32 ValidatorSrcAddr sdk.ValAddress `json:"validator_src_addr"` // in bech32 ValidatorDstAddr sdk.ValAddress `json:"validator_dst_addr"` // in bech32 @@ -911,7 +943,7 @@ func getDelegatorValidator(t *testing.T, port string, delegatorAddr sdk.AccAddre } // GET /staking/delegators/{delegatorAddr}/txs Get all staking txs (i.e msgs) from a delegator -func getBondingTxs(t *testing.T, port string, delegatorAddr sdk.AccAddress, query string) []tx.Info { +func getBondingTxs(t *testing.T, port string, delegatorAddr sdk.AccAddress, query string) []sdk.TxResponse { var res *http.Response var body string @@ -922,7 +954,7 @@ func getBondingTxs(t *testing.T, port string, delegatorAddr sdk.AccAddress, quer } require.Equal(t, http.StatusOK, res.StatusCode, body) - var txs []tx.Info + var txs []sdk.TxResponse err := cdc.UnmarshalJSON([]byte(body), &txs) require.Nil(t, err) @@ -1033,19 +1065,21 @@ func getStakingParams(t *testing.T, port string) staking.Params { // ICS 22 - Gov // ---------------------------------------------------------------------- // POST /gov/proposals Submit a proposal -func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, amount int64, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { +func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, + amount sdk.Int, fees sdk.Coins) (resultTx sdk.TxResponse) { + acc := getAccount(t, port, proposerAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - pr := postProposalReq{ + pr := rest.PostProposalReq{ Title: "Test", Description: "test", ProposalType: "Text", Proposer: proposerAddr, - InitialDeposit: sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(amount))}, + InitialDeposit: sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, amount)}, BaseReq: baseReq, } @@ -1056,7 +1090,7 @@ func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerA res, body := Request(t, port, "POST", "/gov/proposals", req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var results ctypes.ResultBroadcastTxCommit + var results sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &results) require.Nil(t, err) @@ -1064,7 +1098,7 @@ func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerA } type postProposalReq struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` Title string `json:"title"` // Title of the proposal Description string `json:"description"` // Description of the proposal ProposalType string `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} @@ -1128,17 +1162,18 @@ func getProposalsFilterStatus(t *testing.T, port string, status gov.ProposalStat } // POST /gov/proposals/{proposalId}/deposits Deposit tokens to a proposal -func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, proposalID uint64, amount int64, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { +func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, proposalID uint64, + amount sdk.Int, fees sdk.Coins) (resultTx sdk.TxResponse) { acc := getAccount(t, port, proposerAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - dr := depositReq{ + dr := rest.DepositReq{ Depositor: proposerAddr, - Amount: sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(amount))}, + Amount: sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, amount)}, BaseReq: baseReq, } @@ -1148,7 +1183,7 @@ func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk res, body := Request(t, port, "POST", fmt.Sprintf("/gov/proposals/%d/deposits", proposalID), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var results ctypes.ResultBroadcastTxCommit + var results sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &results) require.Nil(t, err) @@ -1156,7 +1191,7 @@ func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk } type depositReq struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` Depositor sdk.AccAddress `json:"depositor"` // Address of the depositor Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit } @@ -1182,15 +1217,15 @@ func getTally(t *testing.T, port string, proposalID uint64) gov.TallyResult { } // POST /gov/proposals/{proposalId}/votes Vote a proposal -func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, proposalID uint64, option string, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { +func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.AccAddress, proposalID uint64, option string, fees sdk.Coins) (resultTx sdk.TxResponse) { // get the account to get the sequence acc := getAccount(t, port, proposerAddr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - vr := voteReq{ + vr := rest.VoteReq{ Voter: proposerAddr, Option: option, BaseReq: baseReq, @@ -1202,7 +1237,7 @@ func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.Ac res, body := Request(t, port, "POST", fmt.Sprintf("/gov/proposals/%d/votes", proposalID), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var results ctypes.ResultBroadcastTxCommit + var results sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &results) require.Nil(t, err) @@ -1210,7 +1245,7 @@ func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.Ac } type voteReq struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` Voter sdk.AccAddress `json:"voter"` // address of the voter Option string `json:"option"` // option from OptionSet chosen by the voter } @@ -1318,11 +1353,11 @@ func getSigningInfo(t *testing.T, port string, validatorPubKey string) slashing. // TODO: Test this functionality, it is not currently in any of the tests // POST /slashing/validators/{validatorAddr}/unjail Unjail a jailed validator func doUnjail(t *testing.T, port, seed, name, password string, - valAddr sdk.ValAddress, fees sdk.Coins) (resultTx ctypes.ResultBroadcastTxCommit) { + valAddr sdk.ValAddress, fees sdk.Coins) (resultTx sdk.TxResponse) { chainID := viper.GetString(client.FlagChainID) - baseReq := utils.NewBaseReq(name, password, "", chainID, "", "", 1, 1, fees, nil, false, false) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", 1, 1, fees, nil, false, false) - ur := unjailReq{ + ur := rest.UnjailReq{ BaseReq: baseReq, } req, err := cdc.MarshalJSON(ur) @@ -1330,13 +1365,47 @@ func doUnjail(t *testing.T, port, seed, name, password string, res, body := Request(t, port, "POST", fmt.Sprintf("/slashing/validators/%s/unjail", valAddr.String()), req) require.Equal(t, http.StatusOK, res.StatusCode, body) - var results []ctypes.ResultBroadcastTxCommit + var results sdk.TxResponse err = cdc.UnmarshalJSON([]byte(body), &results) require.Nil(t, err) - return results[0] + return results } type unjailReq struct { - BaseReq utils.BaseReq `json:"base_req"` + BaseReq rest.BaseReq `json:"base_req"` +} + +// ICS24 - fee distribution + +// POST /distribution/delegators/{delgatorAddr}/rewards Withdraw delegator rewards +func doWithdrawDelegatorAllRewards(t *testing.T, port, seed, name, password string, + delegatorAddr sdk.AccAddress, fees sdk.Coins) (resultTx sdk.TxResponse) { + // get the account to get the sequence + acc := getAccount(t, port, delegatorAddr) + accnum := acc.GetAccountNumber() + sequence := acc.GetSequence() + chainID := viper.GetString(client.FlagChainID) + baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) + + wr := struct { + BaseReq rest.BaseReq `json:"base_req"` + }{BaseReq: baseReq} + + req := cdc.MustMarshalJSON(wr) + res, body := Request(t, port, "POST", fmt.Sprintf("/distribution/delegators/%s/rewards", delegatorAddr), req) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var results sdk.TxResponse + cdc.MustUnmarshalJSON([]byte(body), &results) + + return results +} + +func mustParseDecCoins(dcstring string) sdk.DecCoins { + dcoins, err := sdk.ParseDecCoins(dcstring) + if err != nil { + panic(err) + } + return dcoins } diff --git a/client/rest/rest.go b/client/rest/rest.go new file mode 100644 index 000000000..80f48ff29 --- /dev/null +++ b/client/rest/rest.go @@ -0,0 +1,255 @@ +package rest + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" +) + +//----------------------------------------------------------------------------- +// Basic HTTP utilities + +// ErrorResponse defines the attributes of a JSON error response. +type ErrorResponse struct { + Code int `json:"code,omitempty"` + Message string `json:"message"` +} + +// NewErrorResponse creates a new ErrorResponse instance. +func NewErrorResponse(code int, msg string) ErrorResponse { + return ErrorResponse{Code: code, Message: msg} +} + +// WriteErrorResponse prepares and writes a HTTP error +// given a status code and an error message. +func WriteErrorResponse(w http.ResponseWriter, status int, err string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(codec.Cdc.MustMarshalJSON(NewErrorResponse(0, err))) +} + +// WriteSimulationResponse prepares and writes an HTTP +// response for transactions simulations. +func WriteSimulationResponse(w http.ResponseWriter, cdc *codec.Codec, gas uint64) { + gasEst := GasEstimateResponse{GasEstimate: gas} + resp, err := cdc.MarshalJSON(gasEst) + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(resp) +} + +// ParseInt64OrReturnBadRequest converts s to a int64 value. +func ParseInt64OrReturnBadRequest(w http.ResponseWriter, s string) (n int64, ok bool) { + var err error + + n, err = strconv.ParseInt(s, 10, 64) + if err != nil { + err := fmt.Errorf("'%s' is not a valid int64", s) + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return n, false + } + + return n, true +} + +// ParseUint64OrReturnBadRequest converts s to a uint64 value. +func ParseUint64OrReturnBadRequest(w http.ResponseWriter, s string) (n uint64, ok bool) { + var err error + + n, err = strconv.ParseUint(s, 10, 64) + if err != nil { + err := fmt.Errorf("'%s' is not a valid uint64", s) + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return n, false + } + + return n, true +} + +// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a +// default value, defaultIfEmpty, if the string is empty. +func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) { + if len(s) == 0 { + return defaultIfEmpty, true + } + + n, err := strconv.ParseFloat(s, 64) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return n, false + } + + return n, true +} + +//----------------------------------------------------------------------------- +// Building / Sending utilities + +// CompleteAndBroadcastTxREST implements a utility function that facilitates +// sending a series of messages in a signed tx. In addition, it will handle +// tx gas simulation and estimation. +// +// NOTE: Also see CompleteAndBroadcastTxCLI. +func CompleteAndBroadcastTxREST( + w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, + baseReq BaseReq, msgs []sdk.Msg, cdc *codec.Codec, +) { + + gasAdj, ok := ParseFloat64OrReturnBadRequest(w, baseReq.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + + simAndExec, gas, err := client.ParseGas(baseReq.Gas) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // derive the from account address and name from the Keybase + fromAddress, fromName, err := context.GetFromFields(baseReq.From) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + cliCtx = cliCtx.WithFromName(fromName).WithFromAddress(fromAddress) + txBldr := authtxb.NewTxBuilder( + utils.GetTxEncoder(cdc), baseReq.AccountNumber, + baseReq.Sequence, gas, gasAdj, baseReq.Simulate, + baseReq.ChainID, baseReq.Memo, baseReq.Fees, baseReq.GasPrices, + ) + + txBldr, err = utils.PrepareTxBuilder(txBldr, cliCtx) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if baseReq.Simulate || simAndExec { + if gasAdj < 0 { + WriteErrorResponse(w, http.StatusBadRequest, client.ErrInvalidGasAdjustment.Error()) + return + } + + txBldr, err = utils.EnrichWithGas(txBldr, cliCtx, msgs) + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + if baseReq.Simulate { + WriteSimulationResponse(w, cdc, txBldr.Gas()) + return + } + } + + txBytes, err := txBldr.BuildAndSign(cliCtx.GetFromName(), baseReq.Password, msgs) + if keyerror.IsErrKeyNotFound(err) { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } else if keyerror.IsErrWrongPassword(err) { + WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) + return + } else if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + res, err := cliCtx.BroadcastTx(txBytes) + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + PostProcessResponse(w, cdc, res, cliCtx.Indent) +} + +// PostProcessResponse performs post processing for a REST response. +func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response interface{}, indent bool) { + var output []byte + + switch response.(type) { + default: + var err error + if indent { + output, err = cdc.MarshalJSONIndent(response, "", " ") + } else { + output, err = cdc.MarshalJSON(response) + } + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + case []byte: + output = response.([]byte) + } + + w.Header().Set("Content-Type", "application/json") + w.Write(output) +} + +// WriteGenerateStdTxResponse writes response for the generate only mode. +func WriteGenerateStdTxResponse( + w http.ResponseWriter, cdc *codec.Codec, cliCtx context.CLIContext, br BaseReq, msgs []sdk.Msg, +) { + + gasAdj, ok := ParseFloat64OrReturnBadRequest(w, br.GasAdjustment, client.DefaultGasAdjustment) + if !ok { + return + } + + simAndExec, gas, err := client.ParseGas(br.Gas) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + txBldr := authtxb.NewTxBuilder( + utils.GetTxEncoder(cdc), br.AccountNumber, br.Sequence, gas, gasAdj, + br.Simulate, br.ChainID, br.Memo, br.Fees, br.GasPrices, + ) + + if simAndExec { + if gasAdj < 0 { + WriteErrorResponse(w, http.StatusBadRequest, client.ErrInvalidGasAdjustment.Error()) + return + } + + txBldr, err = utils.EnrichWithGas(txBldr, cliCtx, msgs) + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + } + + stdMsg, err := txBldr.BuildSignMsg(msgs) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + output, err := cdc.MarshalJSON(auth.NewStdTx(stdMsg.Msgs, stdMsg.Fee, nil, stdMsg.Memo)) + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(output) + return +} diff --git a/client/rest/types.go b/client/rest/types.go new file mode 100644 index 000000000..0595042ba --- /dev/null +++ b/client/rest/types.go @@ -0,0 +1,187 @@ +package rest + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +// GasEstimateResponse defines a response definition for tx gas estimation. +type GasEstimateResponse struct { + GasEstimate uint64 `json:"gas_estimate"` +} + +// BaseReq defines a structure that can be embedded in other request structures +// that all share common "base" fields. +type BaseReq struct { + From string `json:"from"` + Password string `json:"password"` + Memo string `json:"memo"` + ChainID string `json:"chain_id"` + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` + Fees sdk.Coins `json:"fees"` + GasPrices sdk.DecCoins `json:"gas_prices"` + Gas string `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` + GenerateOnly bool `json:"generate_only"` + Simulate bool `json:"simulate"` +} + +// NewBaseReq creates a new basic request instance and sanitizes its values +func NewBaseReq( + from, password, memo, chainID string, gas, gasAdjustment string, + accNumber, seq uint64, fees sdk.Coins, gasPrices sdk.DecCoins, genOnly, simulate bool, +) BaseReq { + + return BaseReq{ + From: strings.TrimSpace(from), + Password: password, + Memo: strings.TrimSpace(memo), + ChainID: strings.TrimSpace(chainID), + Fees: fees, + GasPrices: gasPrices, + Gas: strings.TrimSpace(gas), + GasAdjustment: strings.TrimSpace(gasAdjustment), + AccountNumber: accNumber, + Sequence: seq, + GenerateOnly: genOnly, + Simulate: simulate, + } +} + +// Sanitize performs basic sanitization on a BaseReq object. +func (br BaseReq) Sanitize() BaseReq { + return NewBaseReq( + br.From, br.Password, br.Memo, br.ChainID, br.Gas, br.GasAdjustment, + br.AccountNumber, br.Sequence, br.Fees, br.GasPrices, br.GenerateOnly, br.Simulate, + ) +} + +// ValidateBasic performs basic validation of a BaseReq. If custom validation +// logic is needed, the implementing request handler should perform those +// checks manually. +func (br BaseReq) ValidateBasic(w http.ResponseWriter) bool { + if !br.GenerateOnly && !br.Simulate { + switch { + case len(br.Password) == 0: + WriteErrorResponse(w, http.StatusUnauthorized, "password required but not specified") + return false + + case len(br.ChainID) == 0: + WriteErrorResponse(w, http.StatusUnauthorized, "chain-id required but not specified") + return false + + case !br.Fees.IsZero() && !br.GasPrices.IsZero(): + // both fees and gas prices were provided + WriteErrorResponse(w, http.StatusBadRequest, "cannot provide both fees and gas prices") + return false + + case !br.Fees.IsValid() && !br.GasPrices.IsValid(): + // neither fees or gas prices were provided + WriteErrorResponse(w, http.StatusPaymentRequired, "invalid fees or gas prices provided") + return false + } + } + + if len(br.From) == 0 { + WriteErrorResponse(w, http.StatusUnauthorized, "name or address required but not specified") + return false + } + + return true +} + +/* +ReadRESTReq is a simple convenience wrapper that reads the body and +unmarshals to the req interface. Returns false if errors occurred. + + Usage: + type SomeReq struct { + BaseReq `json:"base_req"` + CustomField string `json:"custom_field"` + } + + req := new(SomeReq) + if ok := ReadRESTReq(w, r, cdc, req); !ok { + return + } +*/ +func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.Codec, req interface{}) bool { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return false + } + + err = cdc.UnmarshalJSON(body, req) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to decode JSON payload: %s", err)) + return false + } + + return true +} + +// AddrSeed combines an Address with the mnemonic of the private key to that address +type AddrSeed struct { + Address sdk.AccAddress + Seed string + Name string + Password string +} + +// SendReq requests sending an amount of coins +type SendReq struct { + Amount sdk.Coins `json:"amount"` + BaseReq BaseReq `json:"base_req"` +} + +// MsgBeginRedelegateInput request to begin a redelegation +type MsgBeginRedelegateInput struct { + BaseReq BaseReq `json:"base_req"` + DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // in bech32 + ValidatorSrcAddr sdk.ValAddress `json:"validator_src_addr"` // in bech32 + ValidatorDstAddr sdk.ValAddress `json:"validator_dst_addr"` // in bech32 + SharesAmount sdk.Dec `json:"shares"` +} + +// PostProposalReq requests a proposals +type PostProposalReq struct { + BaseReq BaseReq `json:"base_req"` + Title string `json:"title"` // Title of the proposal + Description string `json:"description"` // Description of the proposal + ProposalType string `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + Proposer sdk.AccAddress `json:"proposer"` // Address of the proposer + InitialDeposit sdk.Coins `json:"initial_deposit"` // Coins to add to the proposal's deposit +} + +// BroadcastReq requests broadcasting a transaction +type BroadcastReq struct { + Tx auth.StdTx `json:"tx"` + Return string `json:"return"` +} + +// DepositReq requests a deposit of an amount of coins +type DepositReq struct { + BaseReq BaseReq `json:"base_req"` + Depositor sdk.AccAddress `json:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit +} + +// VoteReq requests sending a vote +type VoteReq struct { + BaseReq BaseReq `json:"base_req"` + Voter sdk.AccAddress `json:"voter"` // address of the voter + Option string `json:"option"` // option from OptionSet chosen by the voter +} + +// UnjailReq request unjailing +type UnjailReq struct { + BaseReq BaseReq `json:"base_req"` +} diff --git a/client/rpc/block.go b/client/rpc/block.go index 95f5acb99..d5aaecc1f 100644 --- a/client/rpc/block.go +++ b/client/rpc/block.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/cosmos/cosmos-sdk/client/rest" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" @@ -12,8 +14,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" tmliteProxy "github.com/tendermint/tendermint/lite/proxy" - - "github.com/cosmos/cosmos-sdk/client/utils" ) //BlockCommand returns the verified block data for a given heights @@ -131,7 +131,7 @@ func BlockRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { w.Write([]byte(err.Error())) return } - utils.PostProcessResponse(w, cdc, output, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, output, cliCtx.Indent) } } @@ -150,6 +150,6 @@ func LatestBlockRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { w.Write([]byte(err.Error())) return } - utils.PostProcessResponse(w, cdc, output, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, output, cliCtx.Indent) } } diff --git a/client/rpc/root.go b/client/rpc/root.go index cb25b47f3..6ea831cac 100644 --- a/client/rpc/root.go +++ b/client/rpc/root.go @@ -4,10 +4,11 @@ import ( "fmt" "net/http" + "github.com/cosmos/cosmos-sdk/client/rest" + "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/version" ) @@ -26,7 +27,7 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { // cli version REST handler endpoint func CLIVersionRequestHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - w.Write([]byte(fmt.Sprintf("{\"version\": \"%s\"}", version.GetVersion()))) + w.Write([]byte(fmt.Sprintf("{\"version\": \"%s\"}", version.Version))) } // connected node version REST handler endpoint @@ -34,7 +35,7 @@ func NodeVersionRequestHandler(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { version, err := cliCtx.Query("/app/version", nil) if err != nil { - utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } diff --git a/client/rpc/status.go b/client/rpc/status.go index 86bb1ef31..438346f16 100644 --- a/client/rpc/status.go +++ b/client/rpc/status.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/cosmos/cosmos-sdk/client/rest" + "github.com/spf13/cobra" "github.com/spf13/viper" @@ -12,7 +14,6 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" ) // StatusCommand returns the status of the network @@ -77,7 +78,7 @@ func NodeInfoRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } nodeInfo := status.NodeInfo - utils.PostProcessResponse(w, cdc, nodeInfo, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, nodeInfo, cliCtx.Indent) } } diff --git a/client/rpc/validators.go b/client/rpc/validators.go index 0992f8504..091ac97e8 100644 --- a/client/rpc/validators.go +++ b/client/rpc/validators.go @@ -5,6 +5,11 @@ import ( "fmt" "net/http" "strconv" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + + "github.com/cosmos/cosmos-sdk/client/rest" "github.com/gorilla/mux" "github.com/spf13/cobra" @@ -14,33 +19,59 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" sdk "github.com/cosmos/cosmos-sdk/types" ) // TODO these next two functions feel kinda hacky based on their placement //ValidatorCommand returns the validator set for a given height -func ValidatorCommand() *cobra.Command { +func ValidatorCommand(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "tendermint-validator-set [height]", Short: "Get the full tendermint validator set at given height", Args: cobra.MaximumNArgs(1), - RunE: printValidators, + RunE: func(cmd *cobra.Command, args []string) error { + var height *int64 + + // optional height + if len(args) > 0 { + h, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if h > 0 { + tmp := int64(h) + height = &tmp + } + } + + cliCtx := context.NewCLIContext().WithCodec(cdc) + + result, err := getValidators(cliCtx, height) + if err != nil { + return err + } + + return cliCtx.PrintOutput(result) + }, } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:26657", "Node to connect to") viper.BindPFlag(client.FlagNode, cmd.Flags().Lookup(client.FlagNode)) cmd.Flags().Bool(client.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") viper.BindPFlag(client.FlagTrustNode, cmd.Flags().Lookup(client.FlagTrustNode)) + cmd.Flags().Bool(client.FlagIndentResponse, false, "indent JSON response") + viper.BindPFlag(client.FlagIndentResponse, cmd.Flags().Lookup(client.FlagIndentResponse)) + return cmd } // Validator output in bech32 format type ValidatorOutput struct { - Address sdk.ValAddress `json:"address"` // in bech32 - PubKey string `json:"pub_key"` // in bech32 - ProposerPriority int64 `json:"proposer_priority"` - VotingPower int64 `json:"voting_power"` + Address sdk.ConsAddress `json:"address"` + PubKey string `json:"pub_key"` + ProposerPriority int64 `json:"proposer_priority"` + VotingPower int64 `json:"voting_power"` } // Validators at a certain height output in bech32 format @@ -49,6 +80,27 @@ type ResultValidatorsOutput struct { Validators []ValidatorOutput `json:"validators"` } +func (rvo ResultValidatorsOutput) String() string { + var b strings.Builder + + b.WriteString(fmt.Sprintf("block height: %d\n", rvo.BlockHeight)) + + for _, val := range rvo.Validators { + b.WriteString( + fmt.Sprintf(` + Address: %s + Pubkey: %s + ProposerPriority: %d + VotingPower: %d + `, + val.Address, val.PubKey, val.ProposerPriority, val.VotingPower, + ), + ) + } + + return b.String() +} + func bech32ValidatorOutput(validator *tmtypes.Validator) (ValidatorOutput, error) { bechValPubkey, err := sdk.Bech32ifyConsPub(validator.PubKey) if err != nil { @@ -56,33 +108,33 @@ func bech32ValidatorOutput(validator *tmtypes.Validator) (ValidatorOutput, error } return ValidatorOutput{ - Address: sdk.ValAddress(validator.Address), + Address: sdk.ConsAddress(validator.Address), PubKey: bechValPubkey, ProposerPriority: validator.ProposerPriority, VotingPower: validator.VotingPower, }, nil } -func getValidators(cliCtx context.CLIContext, height *int64) ([]byte, error) { +func getValidators(cliCtx context.CLIContext, height *int64) (ResultValidatorsOutput, error) { // get the node node, err := cliCtx.GetNode() if err != nil { - return nil, err + return ResultValidatorsOutput{}, err } validatorsRes, err := node.Validators(height) if err != nil { - return nil, err + return ResultValidatorsOutput{}, err } if !cliCtx.TrustNode { check, err := cliCtx.Verify(validatorsRes.BlockHeight) if err != nil { - return nil, err + return ResultValidatorsOutput{}, err } if !bytes.Equal(check.ValidatorsHash, tmtypes.NewValidatorSet(validatorsRes.Validators).Hash()) { - return nil, fmt.Errorf("got invalid validatorset") + return ResultValidatorsOutput{}, fmt.Errorf("received invalid validatorset") } } @@ -94,40 +146,11 @@ func getValidators(cliCtx context.CLIContext, height *int64) ([]byte, error) { for i := 0; i < len(validatorsRes.Validators); i++ { outputValidatorsRes.Validators[i], err = bech32ValidatorOutput(validatorsRes.Validators[i]) if err != nil { - return nil, err + return ResultValidatorsOutput{}, err } } - if cliCtx.Indent { - return cdc.MarshalJSONIndent(outputValidatorsRes, "", " ") - } - return cdc.MarshalJSON(outputValidatorsRes) - -} - -// CMD - -func printValidators(cmd *cobra.Command, args []string) error { - var height *int64 - // optional height - if len(args) > 0 { - h, err := strconv.Atoi(args[0]) - if err != nil { - return err - } - if h > 0 { - tmp := int64(h) - height = &tmp - } - } - - output, err := getValidators(context.NewCLIContext(), height) - if err != nil { - return err - } - - fmt.Println(string(output)) - return nil + return outputValidatorsRes, nil } // REST @@ -157,7 +180,7 @@ func ValidatorSetRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { w.Write([]byte(err.Error())) return } - utils.PostProcessResponse(w, cdc, output, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, output, cliCtx.Indent) } } @@ -177,6 +200,6 @@ func LatestValidatorSetRequestHandlerFn(cliCtx context.CLIContext) http.HandlerF w.Write([]byte(err.Error())) return } - utils.PostProcessResponse(w, cdc, output, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, output, cliCtx.Indent) } } diff --git a/client/tx/broadcast.go b/client/tx/broadcast.go index 4080b68ec..9dd7b3abd 100644 --- a/client/tx/broadcast.go +++ b/client/tx/broadcast.go @@ -3,10 +3,11 @@ package tx import ( "net/http" + "github.com/cosmos/cosmos-sdk/client/rest" + "io/ioutil" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" ) @@ -32,12 +33,12 @@ func BroadcastTxRequest(cliCtx context.CLIContext, cdc *codec.Codec) http.Handle var m BroadcastBody body, err := ioutil.ReadAll(r.Body) if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } err = cdc.UnmarshalJSON(body, &m) if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } var res interface{} @@ -49,13 +50,13 @@ func BroadcastTxRequest(cliCtx context.CLIContext, cdc *codec.Codec) http.Handle case flagAsync: res, err = cliCtx.BroadcastTxAsync(m.TxBytes) default: - utils.WriteErrorResponse(w, http.StatusInternalServerError, "unsupported return type. supported types: block, sync, async") + rest.WriteErrorResponse(w, http.StatusInternalServerError, "unsupported return type. supported types: block, sync, async") return } if err != nil { - utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - utils.PostProcessResponse(w, cdc, res, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, res, cliCtx.Indent) } } diff --git a/client/tx/query.go b/client/tx/query.go index e169286e3..cbf64f34e 100644 --- a/client/tx/query.go +++ b/client/tx/query.go @@ -5,18 +5,15 @@ import ( "fmt" "net/http" - "github.com/tendermint/tendermint/libs/common" - "github.com/gorilla/mux" "github.com/spf13/cobra" - abci "github.com/tendermint/tendermint/abci/types" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/spf13/viper" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/client/rest" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" @@ -99,26 +96,13 @@ func ValidateTxResult(cliCtx context.CLIContext, res *ctypes.ResultTx) error { return nil } -func formatTxResult(cdc *codec.Codec, res *ctypes.ResultTx) (Info, error) { +func formatTxResult(cdc *codec.Codec, res *ctypes.ResultTx) (sdk.TxResponse, error) { tx, err := parseTx(cdc, res.Tx) if err != nil { - return Info{}, err + return sdk.TxResponse{}, err } - return Info{ - Hash: res.Hash, - Height: res.Height, - Tx: tx, - Result: res.TxResult, - }, nil -} - -// Info is used to prepare info to display -type Info struct { - Hash common.HexBytes `json:"hash"` - Height int64 `json:"height"` - Tx sdk.Tx `json:"tx"` - Result abci.ResponseDeliverTx `json:"result"` + return sdk.NewResponseResultTx(res, tx), nil } func parseTx(cdc *codec.Codec, txBytes []byte) (sdk.Tx, error) { @@ -142,9 +126,9 @@ func QueryTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.H output, err := queryTx(cdc, cliCtx, hashHexStr) if err != nil { - utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - utils.PostProcessResponse(w, cdc, output, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, output, cliCtx.Indent) } } diff --git a/client/tx/search.go b/client/tx/search.go index 29537e00b..91c3b32c4 100644 --- a/client/tx/search.go +++ b/client/tx/search.go @@ -8,9 +8,10 @@ import ( "strconv" "strings" + "github.com/cosmos/cosmos-sdk/client/rest" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -105,7 +106,7 @@ $ gaiacli query txs --tags ':&:' --page 1 --limit 30 // SearchTxs performs a search for transactions for a given set of tags via // Tendermint RPC. It returns a slice of Info object containing txs and metadata. // An error is returned if the query fails. -func SearchTxs(cliCtx context.CLIContext, cdc *codec.Codec, tags []string, page, limit int) ([]Info, error) { +func SearchTxs(cliCtx context.CLIContext, cdc *codec.Codec, tags []string, page, limit int) ([]sdk.TxResponse, error) { if len(tags) == 0 { return nil, errors.New("must declare at least one tag to search") } @@ -152,9 +153,9 @@ func SearchTxs(cliCtx context.CLIContext, cdc *codec.Codec, tags []string, page, } // parse the indexed txs into an array of Info -func FormatTxResults(cdc *codec.Codec, res []*ctypes.ResultTx) ([]Info, error) { +func FormatTxResults(cdc *codec.Codec, res []*ctypes.ResultTx) ([]sdk.TxResponse, error) { var err error - out := make([]Info, len(res)) + out := make([]sdk.TxResponse, len(res)) for i := range res { out[i], err = formatTxResult(cdc, res[i]) if err != nil { @@ -172,31 +173,31 @@ func SearchTxRequestHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) http. return func(w http.ResponseWriter, r *http.Request) { var tags []string var page, limit int - var txs []Info + var txs []sdk.TxResponse err := r.ParseForm() if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, sdk.AppendMsgToErr("could not parse query parameters", err.Error())) + rest.WriteErrorResponse(w, http.StatusBadRequest, sdk.AppendMsgToErr("could not parse query parameters", err.Error())) return } if len(r.Form) == 0 { - utils.PostProcessResponse(w, cdc, txs, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, txs, cliCtx.Indent) return } tags, page, limit, err = parseHTTPArgs(r) if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return } txs, err = SearchTxs(cliCtx, cdc, tags, page, limit) if err != nil { - utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - utils.PostProcessResponse(w, cdc, txs, cliCtx.Indent) + rest.PostProcessResponse(w, cdc, txs, cliCtx.Indent) } } diff --git a/client/utils/rest.go b/client/utils/rest.go deleted file mode 100644 index f6bd18a4f..000000000 --- a/client/utils/rest.go +++ /dev/null @@ -1,305 +0,0 @@ -package utils - -import ( - "fmt" - "io/ioutil" - "net/http" - "strconv" - "strings" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" -) - -//---------------------------------------- -// Basic HTTP utilities - -// WriteErrorResponse prepares and writes a HTTP error -// given a status code and an error message. -func WriteErrorResponse(w http.ResponseWriter, status int, err string) { - w.WriteHeader(status) - w.Write([]byte(err)) -} - -// WriteSimulationResponse prepares and writes an HTTP -// response for transactions simulations. -func WriteSimulationResponse(w http.ResponseWriter, gas uint64) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf(`{"gas_estimate":%v}`, gas))) -} - -// ParseInt64OrReturnBadRequest converts s to a int64 value. -func ParseInt64OrReturnBadRequest(w http.ResponseWriter, s string) (n int64, ok bool) { - var err error - - n, err = strconv.ParseInt(s, 10, 64) - if err != nil { - err := fmt.Errorf("'%s' is not a valid int64", s) - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return n, false - } - - return n, true -} - -// ParseUint64OrReturnBadRequest converts s to a uint64 value. -func ParseUint64OrReturnBadRequest(w http.ResponseWriter, s string) (n uint64, ok bool) { - var err error - - n, err = strconv.ParseUint(s, 10, 64) - if err != nil { - err := fmt.Errorf("'%s' is not a valid uint64", s) - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return n, false - } - - return n, true -} - -// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a -// default value, defaultIfEmpty, if the string is empty. -func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) { - if len(s) == 0 { - return defaultIfEmpty, true - } - - n, err := strconv.ParseFloat(s, 64) - if err != nil { - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return n, false - } - - return n, true -} - -// WriteGenerateStdTxResponse writes response for the generate_only mode. -func WriteGenerateStdTxResponse(w http.ResponseWriter, cdc *codec.Codec, txBldr authtxb.TxBuilder, msgs []sdk.Msg) { - stdMsg, err := txBldr.Build(msgs) - if err != nil { - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - output, err := cdc.MarshalJSON(auth.NewStdTx(stdMsg.Msgs, stdMsg.Fee, nil, stdMsg.Memo)) - if err != nil { - WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Write(output) - return -} - -//---------------------------------------- -// Building / Sending utilities - -// BaseReq defines a structure that can be embedded in other request structures -// that all share common "base" fields. -type BaseReq struct { - Name string `json:"name"` - Password string `json:"password"` - Memo string `json:"memo"` - ChainID string `json:"chain_id"` - AccountNumber uint64 `json:"account_number"` - Sequence uint64 `json:"sequence"` - Fees sdk.Coins `json:"fees"` - GasPrices sdk.DecCoins `json:"gas_prices"` - Gas string `json:"gas"` - GasAdjustment string `json:"gas_adjustment"` - GenerateOnly bool `json:"generate_only"` - Simulate bool `json:"simulate"` -} - -// NewBaseReq creates a new basic request instance and sanitizes its values -func NewBaseReq( - name, password, memo, chainID string, gas, gasAdjustment string, - accNumber, seq uint64, fees sdk.Coins, gasPrices sdk.DecCoins, genOnly, simulate bool, -) BaseReq { - - return BaseReq{ - Name: strings.TrimSpace(name), - Password: password, - Memo: strings.TrimSpace(memo), - ChainID: strings.TrimSpace(chainID), - Fees: fees, - GasPrices: gasPrices, - Gas: strings.TrimSpace(gas), - GasAdjustment: strings.TrimSpace(gasAdjustment), - AccountNumber: accNumber, - Sequence: seq, - GenerateOnly: genOnly, - Simulate: simulate, - } -} - -// Sanitize performs basic sanitization on a BaseReq object. -func (br BaseReq) Sanitize() BaseReq { - return NewBaseReq( - br.Name, br.Password, br.Memo, br.ChainID, br.Gas, br.GasAdjustment, - br.AccountNumber, br.Sequence, br.Fees, br.GasPrices, br.GenerateOnly, br.Simulate, - ) -} - -// ValidateBasic performs basic validation of a BaseReq. If custom validation -// logic is needed, the implementing request handler should perform those -// checks manually. -func (br BaseReq) ValidateBasic(w http.ResponseWriter) bool { - if !br.GenerateOnly && !br.Simulate { - switch { - case len(br.Password) == 0: - WriteErrorResponse(w, http.StatusUnauthorized, "password required but not specified") - return false - - case len(br.ChainID) == 0: - WriteErrorResponse(w, http.StatusUnauthorized, "chain-id required but not specified") - return false - - case !br.Fees.IsZero() && !br.GasPrices.IsZero(): - // both fees and gas prices were provided - WriteErrorResponse(w, http.StatusBadRequest, "cannot provide both fees and gas prices") - return false - - case !br.Fees.IsValid() && !br.GasPrices.IsValid(): - // neither fees or gas prices were provided - WriteErrorResponse(w, http.StatusPaymentRequired, "invalid fees or gas prices provided") - return false - } - } - - if len(br.Name) == 0 { - WriteErrorResponse(w, http.StatusUnauthorized, "name required but not specified") - return false - } - - return true -} - -/* -ReadRESTReq is a simple convenience wrapper that reads the body and -unmarshals to the req interface. - - Usage: - type SomeReq struct { - BaseReq `json:"base_req"` - CustomField string `json:"custom_field"` - } - - req := new(SomeReq) - err := ReadRESTReq(w, r, cdc, req) -*/ -func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.Codec, req interface{}) error { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return err - } - - err = cdc.UnmarshalJSON(body, req) - if err != nil { - WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to decode JSON payload: %s", err)) - return err - } - - return nil -} - -// CompleteAndBroadcastTxREST implements a utility function that facilitates -// sending a series of messages in a signed transaction given a TxBuilder and a -// QueryContext. It ensures that the account exists, has a proper number and -// sequence set. In addition, it builds and signs a transaction with the -// supplied messages. Finally, it broadcasts the signed transaction to a node. -// -// NOTE: Also see CompleteAndBroadcastTxCli. -// NOTE: Also see x/stake/client/rest/tx.go delegationsRequestHandlerFn. -func CompleteAndBroadcastTxREST( - w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, - baseReq BaseReq, msgs []sdk.Msg, cdc *codec.Codec, -) { - - gasAdjustment, ok := ParseFloat64OrReturnBadRequest(w, baseReq.GasAdjustment, client.DefaultGasAdjustment) - if !ok { - return - } - - simulateAndExecute, gas, err := client.ParseGas(baseReq.Gas) - if err != nil { - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - txBldr := authtxb.NewTxBuilder( - GetTxEncoder(cdc), baseReq.AccountNumber, - baseReq.Sequence, gas, gasAdjustment, baseReq.Simulate, - baseReq.ChainID, baseReq.Memo, baseReq.Fees, baseReq.GasPrices, - ) - - if baseReq.Simulate || simulateAndExecute { - if gasAdjustment < 0 { - WriteErrorResponse(w, http.StatusBadRequest, "gas adjustment must be a positive float") - return - } - - txBldr, err = EnrichCtxWithGas(txBldr, cliCtx, baseReq.Name, msgs) - if err != nil { - WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - if baseReq.Simulate { - WriteSimulationResponse(w, txBldr.GetGas()) - return - } - } - - if baseReq.GenerateOnly { - WriteGenerateStdTxResponse(w, cdc, txBldr, msgs) - return - } - - txBytes, err := txBldr.BuildAndSign(baseReq.Name, baseReq.Password, msgs) - if keyerror.IsErrKeyNotFound(err) { - WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } else if keyerror.IsErrWrongPassword(err) { - WriteErrorResponse(w, http.StatusUnauthorized, err.Error()) - return - } else if err != nil { - WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - res, err := cliCtx.BroadcastTx(txBytes) - if err != nil { - WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - PostProcessResponse(w, cdc, res, cliCtx.Indent) -} - -// PostProcessResponse performs post process for rest response -func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response interface{}, indent bool) { - var output []byte - switch response.(type) { - default: - var err error - if indent { - output, err = cdc.MarshalJSONIndent(response, "", " ") - } else { - output, err = cdc.MarshalJSON(response) - } - if err != nil { - WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - case []byte: - output = response.([]byte) - } - w.Header().Set("Content-Type", "application/json") - w.Write(output) -} diff --git a/client/utils/utils.go b/client/utils/utils.go index 92ce82b99..8f4283088 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -3,9 +3,9 @@ package utils import ( "bytes" "fmt" - "io" "os" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/tendermint/go-amino" @@ -18,55 +18,73 @@ import ( authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" ) -// CompleteAndBroadcastTxCli implements a utility function that facilitates +// GasEstimateResponse defines a response definition for tx gas estimation. +type GasEstimateResponse struct { + GasEstimate uint64 `json:"gas_estimate"` +} + +func (gr GasEstimateResponse) String() string { + return fmt.Sprintf("gas estimate: %d", gr.GasEstimate) +} + +// GenerateOrBroadcastMsgs respects CLI flags and outputs a message +func GenerateOrBroadcastMsgs(cliCtx context.CLIContext, txBldr authtxb.TxBuilder, msgs []sdk.Msg, offline bool) error { + if cliCtx.GenerateOnly { + return PrintUnsignedStdTx(txBldr, cliCtx, msgs, offline) + } + return CompleteAndBroadcastTxCLI(txBldr, cliCtx, msgs) +} + +// CompleteAndBroadcastTxCLI implements a utility function that facilitates // sending a series of messages in a signed transaction given a TxBuilder and a // QueryContext. It ensures that the account exists, has a proper number and // sequence set. In addition, it builds and signs a transaction with the // supplied messages. Finally, it broadcasts the signed transaction to a node. // // NOTE: Also see CompleteAndBroadcastTxREST. -func CompleteAndBroadcastTxCli(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) error { - txBldr, err := prepareTxBuilder(txBldr, cliCtx) +func CompleteAndBroadcastTxCLI(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) error { + txBldr, err := PrepareTxBuilder(txBldr, cliCtx) if err != nil { return err } - name, err := cliCtx.GetFromName() - if err != nil { - return err - } + fromName := cliCtx.GetFromName() - if txBldr.GetSimulateAndExecute() || cliCtx.Simulate { - txBldr, err = EnrichCtxWithGas(txBldr, cliCtx, name, msgs) + if txBldr.SimulateAndExecute() || cliCtx.Simulate { + txBldr, err = EnrichWithGas(txBldr, cliCtx, msgs) if err != nil { return err } - fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txBldr.GetGas()) + + gasEst := GasEstimateResponse{GasEstimate: txBldr.Gas()} + fmt.Fprintf(os.Stderr, "%s\n", gasEst.String()) } + if cliCtx.Simulate { return nil } - passphrase, err := keys.GetPassphrase(name) + passphrase, err := keys.GetPassphrase(fromName) if err != nil { return err } // build and sign the transaction - txBytes, err := txBldr.BuildAndSign(name, passphrase, msgs) + txBytes, err := txBldr.BuildAndSign(fromName, passphrase, msgs) if err != nil { return err } // broadcast to a Tendermint node - _, err = cliCtx.BroadcastTx(txBytes) + res, err := cliCtx.BroadcastTx(txBytes) + cliCtx.PrintOutput(res) return err } -// EnrichCtxWithGas calculates the gas estimate that would be consumed by the +// EnrichWithGas calculates the gas estimate that would be consumed by the // transaction and set the transaction's respective value accordingly. -func EnrichCtxWithGas(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string, msgs []sdk.Msg) (authtxb.TxBuilder, error) { - _, adjusted, err := simulateMsgs(txBldr, cliCtx, name, msgs) +func EnrichWithGas(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (authtxb.TxBuilder, error) { + _, adjusted, err := simulateMsgs(txBldr, cliCtx, msgs) if err != nil { return txBldr, err } @@ -92,7 +110,7 @@ func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc * // PrintUnsignedStdTx builds an unsigned StdTx and prints it to os.Stdout. // Don't perform online validation or lookups if offline is true. -func PrintUnsignedStdTx(w io.Writer, txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg, offline bool) (err error) { +func PrintUnsignedStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg, offline bool) (err error) { var stdTx auth.StdTx if offline { stdTx, err = buildUnsignedStdTxOffline(txBldr, cliCtx, msgs) @@ -104,7 +122,7 @@ func PrintUnsignedStdTx(w io.Writer, txBldr authtxb.TxBuilder, cliCtx context.CL } json, err := cliCtx.Codec.MarshalJSON(stdTx) if err == nil { - fmt.Fprintf(w, "%s\n", json) + fmt.Fprintf(cliCtx.Output, "%s\n", json) } return } @@ -115,10 +133,7 @@ func PrintUnsignedStdTx(w io.Writer, txBldr authtxb.TxBuilder, cliCtx context.CL func SignStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string, stdTx auth.StdTx, appendSig bool, offline bool) (auth.StdTx, error) { var signedStdTx auth.StdTx - keybase, err := keys.GetKeyBase() - if err != nil { - return signedStdTx, err - } + keybase := txBldr.Keybase() info, err := keybase.Get(name) if err != nil { @@ -129,8 +144,7 @@ func SignStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string, // check whether the address is a signer if !isTxSigner(sdk.AccAddress(addr), stdTx.GetSigners()) { - return signedStdTx, fmt.Errorf( - "The generated transaction's intended signer does not match the given signer: %q", name) + return signedStdTx, fmt.Errorf("%s: %s", client.ErrInvalidSigner, name) } if !offline { @@ -158,8 +172,7 @@ func SignStdTxWithSignerAddress(txBldr authtxb.TxBuilder, cliCtx context.CLICont // check whether the address is a signer if !isTxSigner(addr, stdTx.GetSigners()) { - return signedStdTx, fmt.Errorf( - "The generated transaction's intended signer does not match the given signer: %q", name) + return signedStdTx, fmt.Errorf("%s: %s", client.ErrInvalidSigner, name) } if !offline { @@ -179,7 +192,7 @@ func SignStdTxWithSignerAddress(txBldr authtxb.TxBuilder, cliCtx context.CLICont func populateAccountFromState(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, addr sdk.AccAddress) (authtxb.TxBuilder, error) { - if txBldr.GetAccountNumber() == 0 { + if txBldr.AccountNumber() == 0 { accNum, err := cliCtx.GetAccountNumber(addr) if err != nil { return txBldr, err @@ -187,7 +200,7 @@ func populateAccountFromState(txBldr authtxb.TxBuilder, cliCtx context.CLIContex txBldr = txBldr.WithAccountNumber(accNum) } - if txBldr.GetSequence() == 0 { + if txBldr.Sequence() == 0 { accSeq, err := cliCtx.GetAccountSequence(addr) if err != nil { return txBldr, err @@ -210,12 +223,12 @@ func GetTxEncoder(cdc *codec.Codec) (encoder sdk.TxEncoder) { // nolint // SimulateMsgs simulates the transaction and returns the gas estimate and the adjusted value. -func simulateMsgs(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, name string, msgs []sdk.Msg) (estimated, adjusted uint64, err error) { - txBytes, err := txBldr.BuildWithPubKey(name, msgs) +func simulateMsgs(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (estimated, adjusted uint64, err error) { + txBytes, err := txBldr.BuildTxForSim(msgs) if err != nil { return } - estimated, adjusted, err = CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, txBldr.GetGasAdjustment()) + estimated, adjusted, err = CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, txBldr.GasAdjustment()) return } @@ -231,19 +244,17 @@ func parseQueryResponse(cdc *amino.Codec, rawRes []byte) (uint64, error) { return simulationResult.GasUsed, nil } -func prepareTxBuilder(txBldr authtxb.TxBuilder, cliCtx context.CLIContext) (authtxb.TxBuilder, error) { +// PrepareTxBuilder populates a TxBuilder in preparation for the build of a Tx. +func PrepareTxBuilder(txBldr authtxb.TxBuilder, cliCtx context.CLIContext) (authtxb.TxBuilder, error) { if err := cliCtx.EnsureAccountExists(); err != nil { return txBldr, err } - from, err := cliCtx.GetFromAddress() - if err != nil { - return txBldr, err - } + from := cliCtx.GetFromAddress() // TODO: (ref #1903) Allow for user supplied account number without // automatically doing a manual lookup. - if txBldr.GetAccountNumber() == 0 { + if txBldr.AccountNumber() == 0 { accNum, err := cliCtx.GetAccountNumber(from) if err != nil { return txBldr, err @@ -253,7 +264,7 @@ func prepareTxBuilder(txBldr authtxb.TxBuilder, cliCtx context.CLIContext) (auth // TODO: (ref #1903) Allow for user supplied account sequence without // automatically doing a manual lookup. - if txBldr.GetSequence() == 0 { + if txBldr.Sequence() == 0 { accSeq, err := cliCtx.GetAccountSequence(from) if err != nil { return txBldr, err @@ -266,7 +277,7 @@ func prepareTxBuilder(txBldr authtxb.TxBuilder, cliCtx context.CLIContext) (auth // buildUnsignedStdTx builds a StdTx as per the parameters passed in the // contexts. Gas is automatically estimated if gas wanted is set to 0. func buildUnsignedStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (stdTx auth.StdTx, err error) { - txBldr, err = prepareTxBuilder(txBldr, cliCtx) + txBldr, err = PrepareTxBuilder(txBldr, cliCtx) if err != nil { return } @@ -274,23 +285,20 @@ func buildUnsignedStdTx(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msg } func buildUnsignedStdTxOffline(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (stdTx auth.StdTx, err error) { - if txBldr.GetSimulateAndExecute() { - var name string - name, err = cliCtx.GetFromName() + if txBldr.SimulateAndExecute() { + txBldr, err = EnrichWithGas(txBldr, cliCtx, msgs) if err != nil { return } - txBldr, err = EnrichCtxWithGas(txBldr, cliCtx, name, msgs) - if err != nil { - return - } - fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txBldr.GetGas()) + fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txBldr.Gas()) } - stdSignMsg, err := txBldr.Build(msgs) + + stdSignMsg, err := txBldr.BuildSignMsg(msgs) if err != nil { return } + return auth.NewStdTx(stdSignMsg.Msgs, stdSignMsg.Fee, nil, stdSignMsg.Memo), nil } @@ -300,5 +308,6 @@ func isTxSigner(user sdk.AccAddress, signers []sdk.AccAddress) bool { return true } } + return false } diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 8b5ce4e73..a84f179a4 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -102,7 +102,11 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b ) // add handlers - app.bankKeeper = bank.NewBaseKeeper(app.accountKeeper) + app.bankKeeper = bank.NewBaseKeeper( + app.accountKeeper, + app.paramsKeeper.Subspace(bank.DefaultParamspace), + bank.DefaultCodespace, + ) app.feeCollectionKeeper = auth.NewFeeCollectionKeeper( app.cdc, app.keyFeeCollection, @@ -160,11 +164,12 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b // initialize BaseApp app.MountStores(app.keyMain, app.keyAccount, app.keyStaking, app.keyMint, app.keyDistr, - app.keySlashing, app.keyGov, app.keyFeeCollection, app.keyParams) + app.keySlashing, app.keyGov, app.keyFeeCollection, app.keyParams, + app.tkeyParams, app.tkeyStaking, app.tkeyDistr, + ) app.SetInitChainer(app.initChainer) app.SetBeginBlocker(app.BeginBlocker) app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.feeCollectionKeeper)) - app.MountStoresTransient(app.tkeyParams, app.tkeyStaking, app.tkeyDistr) app.SetEndBlocker(app.EndBlocker) if loadLatest { @@ -228,10 +233,7 @@ func (app *GaiaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.R // initialize store from a genesis state func (app *GaiaApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisState) []abci.ValidatorUpdate { - // sort by account number to maintain consistency - sort.Slice(genesisState.Accounts, func(i, j int) bool { - return genesisState.Accounts[i].AccountNumber < genesisState.Accounts[j].AccountNumber - }) + genesisState.Sanitize() // load the accounts for _, gacc := range genesisState.Accounts { @@ -251,13 +253,13 @@ func (app *GaiaApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisSt // initialize module-specific stores auth.InitGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper, genesisState.AuthData) + bank.InitGenesis(ctx, app.bankKeeper, genesisState.BankData) slashing.InitGenesis(ctx, app.slashingKeeper, genesisState.SlashingData, genesisState.StakingData) gov.InitGenesis(ctx, app.govKeeper, genesisState.GovData) mint.InitGenesis(ctx, app.mintKeeper, genesisState.MintData) // validate genesis state - err = GaiaValidateGenesisState(genesisState) - if err != nil { + if err := GaiaValidateGenesisState(genesisState); err != nil { panic(err) // TODO find a way to do this w/o panics } @@ -354,10 +356,6 @@ func (h StakingHooks) AfterValidatorBonded(ctx sdk.Context, consAddr sdk.ConsAdd h.dh.AfterValidatorBonded(ctx, consAddr, valAddr) h.sh.AfterValidatorBonded(ctx, consAddr, valAddr) } -func (h StakingHooks) AfterValidatorPowerDidChange(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) { - h.dh.AfterValidatorPowerDidChange(ctx, consAddr, valAddr) - h.sh.AfterValidatorPowerDidChange(ctx, consAddr, valAddr) -} func (h StakingHooks) AfterValidatorBeginUnbonding(ctx sdk.Context, consAddr sdk.ConsAddress, valAddr sdk.ValAddress) { h.dh.AfterValidatorBeginUnbonding(ctx, consAddr, valAddr) h.sh.AfterValidatorBeginUnbonding(ctx, consAddr, valAddr) diff --git a/cmd/gaia/app/app_test.go b/cmd/gaia/app/app_test.go index 5f2b5b968..0ee1596ea 100644 --- a/cmd/gaia/app/app_test.go +++ b/cmd/gaia/app/app_test.go @@ -4,6 +4,8 @@ import ( "os" "testing" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -28,6 +30,7 @@ func setGenesis(gapp *GaiaApp, accs ...*auth.BaseAccount) error { genesisState := NewGenesisState( genaccs, auth.DefaultGenesisState(), + bank.DefaultGenesisState(), staking.DefaultGenesisState(), mint.DefaultGenesisState(), distr.DefaultGenesisState(), @@ -55,6 +58,6 @@ func TestGaiadExport(t *testing.T) { // Making a new app object with the db, so that initchain hasn't been called newGapp := NewGaiaApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true) - _, _, err := newGapp.ExportAppStateAndValidators(false) + _, _, err := newGapp.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") } diff --git a/cmd/gaia/app/benchmarks/txsize_test.go b/cmd/gaia/app/benchmarks/txsize_test.go index 2726dcd1b..af381a3fd 100644 --- a/cmd/gaia/app/benchmarks/txsize_test.go +++ b/cmd/gaia/app/benchmarks/txsize_test.go @@ -23,7 +23,7 @@ func ExampleTxSendSize() { priv2 := secp256k1.GenPrivKeySecp256k1([]byte{1}) addr2 := sdk.AccAddress(priv2.PubKey().Address()) coins := sdk.Coins{sdk.NewCoin("denom", sdk.NewInt(10))} - msg1 := bank.MsgSend{ + msg1 := bank.MsgMultiSend{ Inputs: []bank.Input{bank.NewInput(addr1, coins)}, Outputs: []bank.Output{bank.NewOutput(addr2, coins)}, } diff --git a/cmd/gaia/app/export.go b/cmd/gaia/app/export.go index f535d1373..3c3df5e14 100644 --- a/cmd/gaia/app/export.go +++ b/cmd/gaia/app/export.go @@ -2,6 +2,7 @@ package app import ( "encoding/json" + "log" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -9,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" @@ -17,14 +19,14 @@ import ( ) // export the state of gaia for a genesis file -func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool) ( +func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string) ( appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { // as if they could withdraw from the start of the next block ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) if forZeroHeight { - app.prepForZeroHeightGenesis(ctx) + app.prepForZeroHeightGenesis(ctx, jailWhiteList) } // iterate to get the accounts @@ -39,6 +41,7 @@ func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool) ( genState := NewGenesisState( accounts, auth.ExportGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper), + bank.ExportGenesis(ctx, app.bankKeeper), staking.ExportGenesis(ctx, app.stakingKeeper), mint.ExportGenesis(ctx, app.mintKeeper), distr.ExportGenesis(ctx, app.distrKeeper), @@ -54,7 +57,23 @@ func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool) ( } // prepare for fresh start at zero height -func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { +func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) { + applyWhiteList := false + + //Check if there is a whitelist + if len(jailWhiteList) > 0 { + applyWhiteList = true + } + + whiteListMap := make(map[string]bool) + + for _, addr := range jailWhiteList { + _, err := sdk.ValAddressFromBech32(addr) + if err != nil { + log.Fatal(err) + } + whiteListMap[addr] = true + } /* Just to be safe, assert the invariants on current state. */ app.assertRuntimeInvariantsOnContext(ctx) @@ -131,9 +150,11 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { panic("expected validator, not found") } - validator.BondHeight = 0 validator.UnbondingHeight = 0 valConsAddrs = append(valConsAddrs, validator.ConsAddress()) + if applyWhiteList && !whiteListMap[addr.String()] { + validator.Jailed = true + } app.stakingKeeper.SetValidator(ctx, validator) counter++ @@ -141,6 +162,8 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { iter.Close() + _ = app.stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) + /* Handle slashing state. */ // reset start height on signing infos diff --git a/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index cfda1c6c4..da71e750a 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -9,6 +9,9 @@ import ( "path/filepath" "sort" "strings" + "time" + + "github.com/cosmos/cosmos-sdk/x/bank" tmtypes "github.com/tendermint/tendermint/types" @@ -20,20 +23,19 @@ import ( "github.com/cosmos/cosmos-sdk/x/mint" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/staking" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( // bonded tokens given to genesis validators/accounts - freeFermionVal = int64(100) - freeFermionsAcc = sdk.NewInt(150) - bondDenom = stakingTypes.DefaultBondDenom + freeFermionsAcc = staking.TokensFromTendermintPower(150) + bondDenom = staking.DefaultBondDenom ) // State to Unmarshal type GenesisState struct { Accounts []GenesisAccount `json:"accounts"` AuthData auth.GenesisState `json:"auth"` + BankData bank.GenesisState `json:"bank"` StakingData staking.GenesisState `json:"staking"` MintData mint.GenesisState `json:"mint"` DistrData distr.GenesisState `json:"distr"` @@ -43,6 +45,7 @@ type GenesisState struct { } func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, + bankData bank.GenesisState, stakingData staking.GenesisState, mintData mint.GenesisState, distrData distr.GenesisState, govData gov.GenesisState, slashingData slashing.GenesisState) GenesisState { @@ -50,6 +53,7 @@ func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, return GenesisState{ Accounts: accounts, AuthData: authData, + BankData: bankData, StakingData: stakingData, MintData: mintData, DistrData: distrData, @@ -58,6 +62,17 @@ func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, } } +// Sanitize sorts accounts and coin sets. +func (gs GenesisState) Sanitize() { + sort.Slice(gs.Accounts, func(i, j int) bool { + return gs.Accounts[i].AccountNumber < gs.Accounts[j].AccountNumber + }) + + for _, acc := range gs.Accounts { + acc.Coins = acc.Coins.Sort() + } +} + // GenesisAccount defines an account initialized at genesis. type GenesisAccount struct { Address sdk.AccAddress `json:"address"` @@ -69,8 +84,8 @@ type GenesisAccount struct { OriginalVesting sdk.Coins `json:"original_vesting"` // total vesting coins upon initialization DelegatedFree sdk.Coins `json:"delegated_free"` // delegated vested coins at time of delegation DelegatedVesting sdk.Coins `json:"delegated_vesting"` // delegated vesting coins at time of delegation - StartTime int64 `json:"start_time"` // vesting start time - EndTime int64 `json:"end_time"` // vesting end time + StartTime int64 `json:"start_time"` // vesting start time (UNIX Epoch time) + EndTime int64 `json:"end_time"` // vesting end time (UNIX Epoch time) } func NewGenesisAccount(acc *auth.BaseAccount) GenesisAccount { @@ -190,6 +205,7 @@ func NewDefaultGenesisState() GenesisState { return GenesisState{ Accounts: nil, AuthData: auth.DefaultGenesisState(), + BankData: bank.DefaultGenesisState(), StakingData: staking.DefaultGenesisState(), MintData: mint.DefaultGenesisState(), DistrData: distr.DefaultGenesisState(), @@ -216,6 +232,9 @@ func GaiaValidateGenesisState(genesisState GenesisState) error { if err := auth.ValidateGenesis(genesisState.AuthData); err != nil { return err } + if err := bank.ValidateGenesis(genesisState.BankData); err != nil { + return err + } if err := staking.ValidateGenesis(genesisState.StakingData); err != nil { return err } @@ -232,17 +251,38 @@ func GaiaValidateGenesisState(genesisState GenesisState) error { return slashing.ValidateGenesis(genesisState.SlashingData) } -// Ensures that there are no duplicate accounts in the genesis state, +// validateGenesisStateAccounts performs validation of genesis accounts. It +// ensures that there are no duplicate accounts in the genesis state and any +// provided vesting accounts are valid. func validateGenesisStateAccounts(accs []GenesisAccount) error { addrMap := make(map[string]bool, len(accs)) - for i := 0; i < len(accs); i++ { - acc := accs[i] - strAddr := string(acc.Address) - if _, ok := addrMap[strAddr]; ok { - return fmt.Errorf("Duplicate account in genesis state: Address %v", acc.Address) + for _, acc := range accs { + addrStr := acc.Address.String() + + // disallow any duplicate accounts + if _, ok := addrMap[addrStr]; ok { + return fmt.Errorf("duplicate account found in genesis state; address: %s", addrStr) } - addrMap[strAddr] = true + + // validate any vesting fields + if !acc.OriginalVesting.IsZero() { + if acc.EndTime == 0 { + return fmt.Errorf("missing end time for vesting account; address: %s", addrStr) + } + + if acc.StartTime >= acc.EndTime { + return fmt.Errorf( + "vesting start time must before end time; address: %s, start: %s, end: %s", + addrStr, + time.Unix(acc.StartTime, 0).UTC().Format(time.RFC3339), + time.Unix(acc.EndTime, 0).UTC().Format(time.RFC3339), + ) + } + } + + addrMap[addrStr] = true } + return nil } diff --git a/cmd/gaia/app/genesis_test.go b/cmd/gaia/app/genesis_test.go index 1383794a3..30765c6ae 100644 --- a/cmd/gaia/app/genesis_test.go +++ b/cmd/gaia/app/genesis_test.go @@ -5,17 +5,16 @@ import ( "testing" "time" - "github.com/tendermint/tendermint/crypto/secp256k1" - tmtypes "github.com/tendermint/tendermint/types" - "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" + tmtypes "github.com/tendermint/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/staking" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( @@ -104,41 +103,86 @@ func TestGaiaAppGenState(t *testing.T) { func makeMsg(name string, pk crypto.PubKey) auth.StdTx { desc := staking.NewDescription(name, "", "", "") - comm := stakingTypes.CommissionMsg{} + comm := staking.CommissionMsg{} msg := staking.NewMsgCreateValidator(sdk.ValAddress(pk.Address()), pk, sdk.NewInt64Coin(bondDenom, - 50), desc, comm) + 50), desc, comm, sdk.OneInt()) return auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, nil, "") } func TestGaiaGenesisValidation(t *testing.T) { - genTxs := make([]auth.StdTx, 2) - // Test duplicate accounts fails - genTxs[0] = makeMsg("test-0", pk1) - genTxs[1] = makeMsg("test-1", pk1) - genesisState := makeGenesisState(t, genTxs) + genTxs := []auth.StdTx{makeMsg("test-0", pk1), makeMsg("test-1", pk2)} + dupGenTxs := []auth.StdTx{makeMsg("test-0", pk1), makeMsg("test-1", pk1)} + + // require duplicate accounts fails validation + genesisState := makeGenesisState(t, dupGenTxs) err := GaiaValidateGenesisState(genesisState) - require.NotNil(t, err) - // Test bonded + jailed validator fails + require.Error(t, err) + + // require invalid vesting account fails validation (invalid end time) genesisState = makeGenesisState(t, genTxs) - val1 := stakingTypes.NewValidator(addr1, pk1, stakingTypes.Description{Moniker: "test #2"}) + genesisState.Accounts[0].OriginalVesting = genesisState.Accounts[0].Coins + err = GaiaValidateGenesisState(genesisState) + require.Error(t, err) + genesisState.Accounts[0].StartTime = 1548888000 + genesisState.Accounts[0].EndTime = 1548775410 + err = GaiaValidateGenesisState(genesisState) + require.Error(t, err) + + // require bonded + jailed validator fails validation + genesisState = makeGenesisState(t, genTxs) + val1 := staking.NewValidator(addr1, pk1, staking.NewDescription("test #2", "", "", "")) val1.Jailed = true val1.Status = sdk.Bonded genesisState.StakingData.Validators = append(genesisState.StakingData.Validators, val1) err = GaiaValidateGenesisState(genesisState) - require.NotNil(t, err) - // Test duplicate validator fails + require.Error(t, err) + + // require duplicate validator fails validation val1.Jailed = false genesisState = makeGenesisState(t, genTxs) - val2 := stakingTypes.NewValidator(addr1, pk1, stakingTypes.Description{Moniker: "test #3"}) + val2 := staking.NewValidator(addr1, pk1, staking.NewDescription("test #3", "", "", "")) genesisState.StakingData.Validators = append(genesisState.StakingData.Validators, val1) genesisState.StakingData.Validators = append(genesisState.StakingData.Validators, val2) err = GaiaValidateGenesisState(genesisState) - require.NotNil(t, err) + require.Error(t, err) } func TestNewDefaultGenesisAccount(t *testing.T) { addr := secp256k1.GenPrivKeySecp256k1([]byte("")).PubKey().Address() acc := NewDefaultGenesisAccount(sdk.AccAddress(addr)) require.Equal(t, sdk.NewInt(1000), acc.Coins.AmountOf("footoken")) - require.Equal(t, sdk.NewInt(150), acc.Coins.AmountOf(bondDenom)) + require.Equal(t, staking.TokensFromTendermintPower(150), acc.Coins.AmountOf(bondDenom)) +} + +func TestGenesisStateSanitize(t *testing.T) { + genesisState := makeGenesisState(t, nil) + require.Nil(t, GaiaValidateGenesisState(genesisState)) + + addr1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + authAcc1 := auth.NewBaseAccountWithAddress(addr1) + authAcc1.SetCoins(sdk.Coins{ + sdk.NewInt64Coin("bcoin", 150), + sdk.NewInt64Coin("acoin", 150), + }) + authAcc1.SetAccountNumber(1) + genAcc1 := NewGenesisAccount(&authAcc1) + + addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + authAcc2 := auth.NewBaseAccountWithAddress(addr2) + authAcc2.SetCoins(sdk.Coins{ + sdk.NewInt64Coin("acoin", 150), + sdk.NewInt64Coin("bcoin", 150), + }) + genAcc2 := NewGenesisAccount(&authAcc2) + + genesisState.Accounts = []GenesisAccount{genAcc1, genAcc2} + require.True(t, genesisState.Accounts[0].AccountNumber > genesisState.Accounts[1].AccountNumber) + require.Equal(t, genesisState.Accounts[0].Coins[0].Denom, "bcoin") + require.Equal(t, genesisState.Accounts[0].Coins[1].Denom, "acoin") + require.Equal(t, genesisState.Accounts[1].Address, addr2) + genesisState.Sanitize() + require.False(t, genesisState.Accounts[0].AccountNumber > genesisState.Accounts[1].AccountNumber) + require.Equal(t, genesisState.Accounts[1].Address, addr1) + require.Equal(t, genesisState.Accounts[1].Coins[0].Denom, "acoin") + require.Equal(t, genesisState.Accounts[1].Coins[1].Denom, "bcoin") } diff --git a/cmd/gaia/app/invariants.go b/cmd/gaia/app/invariants.go index cb53f1f9c..6b98edb6a 100644 --- a/cmd/gaia/app/invariants.go +++ b/cmd/gaia/app/invariants.go @@ -38,5 +38,5 @@ func (app *GaiaApp) assertRuntimeInvariantsOnContext(ctx sdk.Context) { } end := time.Now() diff := end.Sub(start) - app.BaseApp.Logger.With("module", "invariants").Info("Asserted all invariants", "duration", diff) + app.BaseApp.Logger().With("module", "invariants").Info("Asserted all invariants", "duration", diff) } diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index b88e809eb..90b1785b1 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -13,13 +13,16 @@ import ( "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/secp256k1" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" authsim "github.com/cosmos/cosmos-sdk/x/auth/simulation" + "github.com/cosmos/cosmos-sdk/x/bank" banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" distr "github.com/cosmos/cosmos-sdk/x/distribution" distrsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation" @@ -31,20 +34,21 @@ import ( slashingsim "github.com/cosmos/cosmos-sdk/x/slashing/simulation" "github.com/cosmos/cosmos-sdk/x/staking" stakingsim "github.com/cosmos/cosmos-sdk/x/staking/simulation" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) var ( - seed int64 - numBlocks int - blockSize int - enabled bool - verbose bool - commit bool - period int + genesisFile string + seed int64 + numBlocks int + blockSize int + enabled bool + verbose bool + commit bool + period int ) func init() { + flag.StringVar(&genesisFile, "SimulationGenesis", "", "Custom simulation genesis file") flag.Int64Var(&seed, "SimulationSeed", 42, "Simulation random seed") flag.IntVar(&numBlocks, "SimulationNumBlocks", 500, "Number of blocks") flag.IntVar(&blockSize, "SimulationBlockSize", 200, "Operations per block") @@ -54,10 +58,34 @@ func init() { flag.IntVar(&period, "SimulationPeriod", 1, "Run slow invariants only once every period assertions") } -func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.Time) json.RawMessage { +func appStateFromGenesisFileFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.Time) (json.RawMessage, []simulation.Account, string) { + var genesis tmtypes.GenesisDoc + cdc := MakeCodec() + bytes, err := ioutil.ReadFile(genesisFile) + if err != nil { + panic(err) + } + cdc.MustUnmarshalJSON(bytes, &genesis) + var appState GenesisState + cdc.MustUnmarshalJSON(genesis.AppState, &appState) + var newAccs []simulation.Account + for _, acc := range appState.Accounts { + // Pick a random private key, since we don't know the actual key + // This should be fine as it's only used for mock Tendermint validators + // and these keys are never actually used to sign by mock Tendermint. + privkeySeed := make([]byte, 15) + r.Read(privkeySeed) + privKey := secp256k1.GenPrivKeySecp256k1(privkeySeed) + newAccs = append(newAccs, simulation.Account{privKey, privKey.PubKey(), acc.Address}) + } + return genesis.AppState, newAccs, genesis.ChainID +} + +func appStateRandomizedFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.Time) (json.RawMessage, []simulation.Account, string) { + var genesisAccounts []GenesisAccount - amount := int64(r.Intn(1e6)) + amount := int64(r.Intn(1e12)) numInitiallyBonded := int64(r.Intn(250)) numAccs := int64(len(accs)) if numInitiallyBonded > numAccs { @@ -69,7 +97,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T // randomly generate some genesis accounts for i, acc := range accs { - coins := sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(amount))} + coins := sdk.Coins{sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(amount))} bacc := auth.NewBaseAccountWithAddress(acc.Address) bacc.SetCoins(coins) @@ -109,21 +137,24 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T authGenesis := auth.GenesisState{ Params: auth.Params{ - MemoCostPerByte: uint64(r.Intn(10) + 1), - MaxMemoCharacters: uint64(r.Intn(200-100) + 100), + MaxMemoCharacters: uint64(randIntBetween(r, 100, 200)), TxSigLimit: uint64(r.Intn(7) + 1), - SigVerifyCostED25519: uint64(r.Intn(1000-500) + 500), - SigVerifyCostSecp256k1: uint64(r.Intn(1000-500) + 500), + TxSizeCostPerByte: uint64(randIntBetween(r, 5, 15)), + SigVerifyCostED25519: uint64(randIntBetween(r, 500, 1000)), + SigVerifyCostSecp256k1: uint64(randIntBetween(r, 500, 1000)), }, } fmt.Printf("Selected randomly generated auth parameters:\n\t%+v\n", authGenesis) + bankGenesis := bank.NewGenesisState(r.Int63n(2) == 0) + fmt.Printf("Selected randomly generated bank parameters:\n\t%+v\n", bankGenesis) + // Random genesis states vp := time.Duration(r.Intn(2*172800)) * time.Second govGenesis := gov.GenesisState{ StartingProposalID: uint64(r.Intn(100)), DepositParams: gov.DepositParams{ - MinDeposit: sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, int64(r.Intn(1e3)))}, + MinDeposit: sdk.Coins{sdk.NewInt64Coin(staking.DefaultBondDenom, int64(r.Intn(1e3)))}, MaxDepositPeriod: vp, }, VotingParams: gov.VotingParams{ @@ -142,7 +173,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T Params: staking.Params{ UnbondingTime: time.Duration(randIntBetween(r, 60, 60*60*24*3*2)) * time.Second, MaxValidators: uint16(r.Intn(250)), - BondDenom: stakingTypes.DefaultBondDenom, + BondDenom: staking.DefaultBondDenom, }, } fmt.Printf("Selected randomly generated staking parameters:\n\t%+v\n", stakingGenesis) @@ -163,7 +194,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T Minter: mint.InitialMinter( sdk.NewDecWithPrec(int64(r.Intn(99)), 2)), Params: mint.NewParams( - stakingTypes.DefaultBondDenom, + staking.DefaultBondDenom, sdk.NewDecWithPrec(int64(r.Intn(99)), 2), sdk.NewDecWithPrec(20, 2), sdk.NewDecWithPrec(7, 2), @@ -203,6 +234,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T genesis := GenesisState{ Accounts: genesisAccounts, AuthData: authGenesis, + BankData: bankGenesis, StakingData: stakingGenesis, MintData: mintGenesis, DistrData: distrGenesis, @@ -216,7 +248,14 @@ func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.T panic(err) } - return appState + return appState, accs, "simulation" +} + +func appStateFn(r *rand.Rand, accs []simulation.Account, genesisTimestamp time.Time) (json.RawMessage, []simulation.Account, string) { + if genesisFile != "" { + return appStateFromGenesisFileFn(r, accs, genesisTimestamp) + } + return appStateRandomizedFn(r, accs, genesisTimestamp) } func randIntBetween(r *rand.Rand, min, max int) int { @@ -226,7 +265,8 @@ func randIntBetween(r *rand.Rand, min, max int) int { func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { return []simulation.WeightedOperation{ {5, authsim.SimulateDeductFee(app.accountKeeper, app.feeCollectionKeeper)}, - {100, banksim.SingleInputSendMsg(app.accountKeeper, app.bankKeeper)}, + {100, banksim.SendMsg(app.accountKeeper, app.bankKeeper)}, + {10, banksim.SingleInputMsgMultiSend(app.accountKeeper, app.bankKeeper)}, {50, distrsim.SimulateMsgSetWithdrawAddress(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawDelegatorReward(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper)}, @@ -261,8 +301,8 @@ func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { // /usr/local/go/bin/go test -benchmem -run=^$ github.com/cosmos/cosmos-sdk/cmd/gaia/app -bench ^BenchmarkFullGaiaSimulation$ -SimulationCommit=true -cpuprofile cpu.out func BenchmarkFullGaiaSimulation(b *testing.B) { // Setup Gaia application - var logger log.Logger - logger = log.NewNopLogger() + logger := log.NewNopLogger() + var db dbm.DB dir, _ := ioutil.TempDir("", "goleveldb-gaia-sim") db, _ = dbm.NewGoLevelDB("Simulation", dir) @@ -376,11 +416,8 @@ func TestGaiaImportExport(t *testing.T) { fmt.Printf("Exporting genesis...\n") - appState, _, err := app.ExportAppStateAndValidators(false) - if err != nil { - panic(err) - } - + appState, _, err := app.ExportAppStateAndValidators(false, []string{}) + require.NoError(t, err) fmt.Printf("Importing genesis...\n") newDir, _ := ioutil.TempDir("", "goleveldb-gaia-sim-2") @@ -480,7 +517,7 @@ func TestGaiaSimulationAfterImport(t *testing.T) { fmt.Printf("Exporting genesis...\n") - appState, _, err := app.ExportAppStateAndValidators(true) + appState, _, err := app.ExportAppStateAndValidators(true, []string{}) if err != nil { panic(err) } diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 2d6864ec9..bff6f34e1 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -1,12 +1,14 @@ package clitest import ( + "encoding/base64" "errors" "fmt" "io/ioutil" "os" "path" "path/filepath" + "strings" "testing" "time" @@ -14,15 +16,13 @@ import ( "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/staking" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) func TestGaiaCLIKeysAddMultisig(t *testing.T) { @@ -45,6 +45,31 @@ func TestGaiaCLIKeysAddMultisig(t *testing.T) { require.NotEqual(t, f.KeysShow("msig3").Address, f.KeysShow("msig4").Address) } +func TestGaiaCLIKeysAddRecover(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + f.KeysAddRecover("test-recover", "dentist task convince chimney quality leave banana trade firm crawl eternal easily") + require.Equal(t, "cosmos1qcfdf69js922qrdr4yaww3ax7gjml6pdds46f4", f.KeyAddress("test-recover").String()) +} + +func TestGaiaCLIKeysAddRecoverHDPath(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + f.KeysAddRecoverHDPath("test-recoverHD1", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 0, 0) + require.Equal(t, "cosmos1qcfdf69js922qrdr4yaww3ax7gjml6pdds46f4", f.KeyAddress("test-recoverHD1").String()) + + f.KeysAddRecoverHDPath("test-recoverH2", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 1, 5) + require.Equal(t, "cosmos1pdfav2cjhry9k79nu6r8kgknnjtq6a7rykmafy", f.KeyAddress("test-recoverH2").String()) + + f.KeysAddRecoverHDPath("test-recoverH3", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 1, 17) + require.Equal(t, "cosmos1909k354n6wl8ujzu6kmh49w4d02ax7qvlkv4sn", f.KeyAddress("test-recoverH3").String()) + + f.KeysAddRecoverHDPath("test-recoverH4", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 2, 17) + require.Equal(t, "cosmos1v9plmhvyhgxk3th9ydacm7j4z357s3nhtwsjat", f.KeyAddress("test-recoverH4").String()) +} + func TestGaiaCLIMinimumFees(t *testing.T) { t.Parallel() f := InitFixtures(t) @@ -67,13 +92,13 @@ func TestGaiaCLIMinimumFees(t *testing.T) { tests.WaitForNextNBlocksTM(1, f.Port) // Ensure tx w/ correct fees pass - txFees := fmt.Sprintf("--fees=%s,%s", sdk.NewInt64Coin(feeDenom, 2), sdk.NewInt64Coin(fee2Denom, 2)) + txFees := fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2)) success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10), txFees) require.True(f.T, success) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure tx w/ improper fees fails - txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 5)) + txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 1)) success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10), txFees) require.False(f.T, success) @@ -144,8 +169,9 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { require.Equal(t, fooAmt.Int64(), fooAcc.GetCoins().AmountOf(fooDenom).Int64()) // insufficient funds (coins + fees) tx fails + largeCoins := staking.TokensFromTendermintPower(10000000) success, _, _ = f.TxSend( - keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10000000), + keyFoo, barAddr, sdk.NewCoin(fooDenom, largeCoins), fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2))) require.False(t, success) @@ -178,45 +204,47 @@ func TestGaiaCLISend(t *testing.T) { barAddr := f.KeyAddress(keyBar) fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + startTokens := staking.TokensFromTendermintPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Send some tokens from one account to the other - f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10)) + sendTokens := staking.TokensFromTendermintPower(10) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected barAcc := f.QueryAccount(barAddr) - require.Equal(t, int64(10), barAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, sendTokens, barAcc.GetCoins().AmountOf(denom)) fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(sendTokens), fooAcc.GetCoins().AmountOf(denom)) // Test --dry-run - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--dry-run") + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--dry-run") require.True(t, success) // Check state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(sendTokens), fooAcc.GetCoins().AmountOf(denom)) // test autosequencing - f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10)) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected barAcc = f.QueryAccount(barAddr) - require.Equal(t, int64(20), barAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, sendTokens.MulRaw(2), barAcc.GetCoins().AmountOf(denom)) fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(30), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(sendTokens.MulRaw(2)), fooAcc.GetCoins().AmountOf(denom)) // test memo - f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--memo='testmemo'") + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--memo='testmemo'") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected barAcc = f.QueryAccount(barAddr) - require.Equal(t, int64(30), barAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, sendTokens.MulRaw(3), barAcc.GetCoins().AmountOf(denom)) fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(20), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(sendTokens.MulRaw(3)), fooAcc.GetCoins().AmountOf(denom)) f.Cleanup() } @@ -233,50 +261,48 @@ func TestGaiaCLIGasAuto(t *testing.T) { barAddr := f.KeyAddress(keyBar) fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + startTokens := staking.TokensFromTendermintPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test failure with auto gas disabled and very little gas set by hand - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=10") + sendTokens := staking.TokensFromTendermintPower(10) + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=10") require.False(t, success) // Check state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test failure with negative gas - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=-100") + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=-100") require.False(t, success) // Check state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test failure with 0 gas - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=0") + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=0") require.False(t, success) // Check state didn't change fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Enable auto gas - success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=auto") + success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=auto") require.NotEmpty(t, stderr) require.True(t, success) cdc := app.MakeCodec() - sendResp := struct { - Height int64 - TxHash string - Response abci.ResponseDeliverTx - }{} + sendResp := sdk.TxResponse{} err := cdc.UnmarshalJSON([]byte(stdout), &sendResp) require.Nil(t, err) - require.Equal(t, sendResp.Response.GasWanted, sendResp.Response.GasUsed) + require.True(t, sendResp.GasWanted >= sendResp.GasUsed) tests.WaitForNextNBlocksTM(1, f.Port) // Check state has changed accordingly fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(sendTokens), fooAcc.GetCoins().AmountOf(denom)) f.Cleanup() } @@ -289,23 +315,17 @@ func TestGaiaCLICreateValidator(t *testing.T) { proc := f.GDStart() defer proc.Stop(false) - fooAddr := f.KeyAddress(keyFoo) barAddr := f.KeyAddress(keyBar) barVal := sdk.ValAddress(barAddr) consPubKey := sdk.MustBech32ifyConsPub(ed25519.GenPrivKey().PubKey()) - f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10)) + sendTokens := staking.TokensFromTendermintPower(10) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) tests.WaitForNextNBlocksTM(1, f.Port) barAcc := f.QueryAccount(barAddr) - require.Equal(t, int64(10), barAcc.GetCoins().AmountOf(denom).Int64()) - fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(denom).Int64()) - - defaultParams := staking.DefaultParams() - initialPool := staking.InitialPool() - initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewInt(101)) // Delegate tx on GaiaAppGenState + require.Equal(t, sendTokens, barAcc.GetCoins().AmountOf(denom)) // Generate a create validator transaction and ensure correctness success, stdout, stderr := f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewInt64Coin(denom, 2), "--generate-only") @@ -318,21 +338,22 @@ func TestGaiaCLICreateValidator(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Test --dry-run - success, _, _ = f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewInt64Coin(denom, 2), "--dry-run") + newValTokens := staking.TokensFromTendermintPower(2) + success, _, _ = f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewCoin(denom, newValTokens), "--dry-run") require.True(t, success) // Create the validator - f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewInt64Coin(denom, 2)) + f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewCoin(denom, newValTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure funds were deducted properly barAcc = f.QueryAccount(barAddr) - require.Equal(t, int64(8), barAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, sendTokens.Sub(newValTokens), barAcc.GetCoins().AmountOf(denom)) // Ensure that validator state is as expected validator := f.QueryStakingValidator(barVal) require.Equal(t, validator.OperatorAddr, barVal) - require.True(sdk.IntEq(t, sdk.NewInt(2), validator.Tokens)) + require.True(sdk.IntEq(t, newValTokens, validator.Tokens)) // Query delegations to the validator validatorDelegations := f.QueryStakingDelegationsTo(barVal) @@ -340,27 +361,21 @@ func TestGaiaCLICreateValidator(t *testing.T) { require.NotZero(t, validatorDelegations[0].Shares) // unbond a single share - success = f.TxStakingUnbond(keyBar, "1", barVal) + unbondTokens := staking.TokensFromTendermintPower(1) + success = f.TxStakingUnbond(keyBar, unbondTokens.String(), barVal) require.True(t, success) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure bonded staking is correct + remainingTokens := newValTokens.Sub(unbondTokens) validator = f.QueryStakingValidator(barVal) - require.Equal(t, "1", validator.Tokens.String()) + require.Equal(t, remainingTokens, validator.Tokens) // Get unbonding delegations from the validator validatorUbds := f.QueryStakingUnbondingDelegationsFrom(barVal) require.Len(t, validatorUbds, 1) require.Len(t, validatorUbds[0].Entries, 1) - require.Equal(t, "1", validatorUbds[0].Entries[0].Balance.Amount.String()) - - // Query staking parameters - params := f.QueryStakingParameters() - require.True(t, defaultParams.Equal(params)) - - // Query staking pool - pool := f.QueryStakingPool() - require.Equal(t, initialPool.BondedTokens, pool.BondedTokens) + require.Equal(t, remainingTokens.String(), validatorUbds[0].Entries[0].Balance.String()) f.Cleanup() } @@ -380,14 +395,16 @@ func TestGaiaCLISubmitProposal(t *testing.T) { fooAddr := f.KeyAddress(keyFoo) fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(stakingTypes.DefaultBondDenom).Int64()) + startTokens := staking.TokensFromTendermintPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(staking.DefaultBondDenom)) proposalsQuery := f.QueryGovProposals() require.Empty(t, proposalsQuery) // Test submit generate only for submit proposal + proposalTokens := staking.TokensFromTendermintPower(5) success, stdout, stderr := f.TxGovSubmitProposal( - keyFoo, "Text", "Test", "test", sdk.NewInt64Coin(denom, 5), "--generate-only") + keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--generate-only") require.True(t, success) require.Empty(t, stderr) msg := unmarshalStdTx(t, stdout) @@ -396,11 +413,11 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Test --dry-run - success, _, _ = f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewInt64Coin(denom, 5), "--dry-run") + success, _, _ = f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--dry-run") require.True(t, success) // Create the proposal - f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewInt64Coin(denom, 5)) + f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure transaction tags can be queried @@ -409,7 +426,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { // Ensure deposit was deducted fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(45), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(proposalTokens), fooAcc.GetCoins().AmountOf(denom)) // Ensure propsal is directly queryable proposal1 := f.QueryGovProposal(1) @@ -422,10 +439,11 @@ func TestGaiaCLISubmitProposal(t *testing.T) { // Query the deposits on the proposal deposit := f.QueryGovDeposit(1, fooAddr) - require.Equal(t, int64(5), deposit.Amount.AmountOf(denom).Int64()) + require.Equal(t, proposalTokens, deposit.Amount.AmountOf(denom)) // Test deposit generate only - success, stdout, stderr = f.TxGovDeposit(1, keyFoo, sdk.NewInt64Coin(denom, 10), "--generate-only") + depositTokens := staking.TokensFromTendermintPower(10) + success, stdout, stderr = f.TxGovDeposit(1, keyFoo, sdk.NewCoin(denom, depositTokens), "--generate-only") require.True(t, success) require.Empty(t, stderr) msg = unmarshalStdTx(t, stdout) @@ -434,17 +452,17 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Run the deposit transaction - f.TxGovDeposit(1, keyFoo, sdk.NewInt64Coin(denom, 10)) + f.TxGovDeposit(1, keyFoo, sdk.NewCoin(denom, depositTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // test query deposit deposits := f.QueryGovDeposits(1) require.Len(t, deposits, 1) - require.Equal(t, int64(15), deposits[0].Amount.AmountOf(denom).Int64()) + require.Equal(t, proposalTokens.Add(depositTokens), deposits[0].Amount.AmountOf(denom)) // Ensure querying the deposit returns the proper amount deposit = f.QueryGovDeposit(1, fooAddr) - require.Equal(t, int64(15), deposit.Amount.AmountOf(denom).Int64()) + require.Equal(t, proposalTokens.Add(depositTokens), deposit.Amount.AmountOf(denom)) // Ensure tags are set on the transaction txs = f.QueryTxs(1, 50, "action:deposit", fmt.Sprintf("depositor:%s", fooAddr)) @@ -452,7 +470,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { // Ensure account has expected amount of funds fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(35), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, startTokens.Sub(proposalTokens.Add(depositTokens)), fooAcc.GetCoins().AmountOf(denom)) // Fetch the proposal and ensure it is now in the voting period proposal1 = f.QueryGovProposal(1) @@ -496,7 +514,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, uint64(1), proposalsQuery[0].GetProposalID()) // submit a second test proposal - f.TxGovSubmitProposal(keyFoo, "Text", "Apples", "test", sdk.NewInt64Coin(denom, 5)) + f.TxGovSubmitProposal(keyFoo, "Text", "Apples", "test", sdk.NewCoin(denom, proposalTokens)) tests.WaitForNextNBlocksTM(1, f.Port) // Test limit on proposals query @@ -517,12 +535,13 @@ func TestGaiaCLIQueryTxPagination(t *testing.T) { fooAddr := f.KeyAddress(keyFoo) barAddr := f.KeyAddress(keyBar) + accFoo := f.QueryAccount(fooAddr) + seq := accFoo.GetSequence() + for i := 1; i <= 30; i++ { - success := executeWrite(t, fmt.Sprintf( - "gaiacli tx send %s --amount=%dfootoken --to=%s --from=foo", - f.Flags(), i, barAddr), app.DefaultKeyPass) + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, int64(i)), fmt.Sprintf("--sequence=%d", seq)) require.True(t, success) - tests.WaitForNextNBlocksTM(1, f.Port) + seq++ } // perPage = 15, 2 pages @@ -571,7 +590,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { require.Empty(t, stderr) // write unsigned tx to file - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // validate we can successfully sign @@ -584,7 +603,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { require.Equal(t, fooAddr.String(), stdTx.GetSigners()[0].String()) // write signed tx to file - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // validate signatures @@ -594,7 +613,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { // modify the transaction stdTx.Memo = "MODIFIED-ORIGINAL-TX-BAD" bz := marshalStdTx(t, stdTx) - modSignedTxFile := writeToNewTempFile(t, string(bz)) + modSignedTxFile := WriteToNewTempFile(t, string(bz)) defer os.Remove(modSignedTxFile.Name()) // validate signature validation failure due to different transaction sig bytes @@ -616,7 +635,8 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { barAddr := f.KeyAddress(keyBar) // Test generate sendTx with default gas - success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--generate-only") + sendTokens := staking.TokensFromTendermintPower(10) + success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--generate-only") require.True(t, success) require.Empty(t, stderr) msg := unmarshalStdTx(t, stdout) @@ -625,7 +645,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Test generate sendTx with --gas=$amount - success, stdout, stderr = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=100", "--generate-only") + success, stdout, stderr = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=100", "--generate-only") require.True(t, success) require.Empty(t, stderr) msg = unmarshalStdTx(t, stdout) @@ -634,7 +654,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Test generate sendTx, estimate gas - success, stdout, stderr = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(denom, 10), "--gas=auto", "--generate-only") + success, stdout, stderr = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=auto", "--generate-only") require.True(t, success) require.NotEmpty(t, stderr) msg = unmarshalStdTx(t, stdout) @@ -642,7 +662,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, len(msg.Msgs), 1) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Test sign --validate-signatures @@ -659,7 +679,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, fooAddr.String(), msg.GetSigners()[0].String()) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Test sign --validate-signatures @@ -670,27 +690,26 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { // Ensure foo has right amount of funds fooAcc := f.QueryAccount(fooAddr) - require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf(denom).Int64()) + startTokens := staking.TokensFromTendermintPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test broadcast success, stdout, _ = f.TxBroadcast(signedTxFile.Name()) require.True(t, success) - var result struct { - Response abci.ResponseDeliverTx - } + var result sdk.TxResponse // Unmarshal the response and ensure that gas was properly used require.Nil(t, app.MakeCodec().UnmarshalJSON([]byte(stdout), &result)) - require.Equal(t, msg.Fee.Gas, uint64(result.Response.GasUsed)) - require.Equal(t, msg.Fee.Gas, uint64(result.Response.GasWanted)) + require.Equal(t, msg.Fee.Gas, uint64(result.GasUsed)) + require.Equal(t, msg.Fee.Gas, uint64(result.GasWanted)) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account state barAcc := f.QueryAccount(barAddr) fooAcc = f.QueryAccount(fooAddr) - require.Equal(t, int64(10), barAcc.GetCoins().AmountOf(denom).Int64()) - require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(denom).Int64()) + require.Equal(t, sendTokens, barAcc.GetCoins().AmountOf(denom)) + require.Equal(t, startTokens.Sub(sendTokens), fooAcc.GetCoins().AmountOf(denom)) f.Cleanup() } @@ -716,7 +735,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -724,7 +743,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Multisign, not enough signatures @@ -732,7 +751,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature @@ -744,6 +763,42 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.False(t, success) } +func TestGaiaCLIEncode(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server + proc := f.GDStart() + defer proc.Stop(false) + + cdc := app.MakeCodec() + + // Build a testing transaction and write it to disk + barAddr := f.KeyAddress(keyBar) + sendTokens := staking.TokensFromTendermintPower(10) + success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--generate-only", "--memo", "deadbeef") + require.True(t, success) + require.Empty(t, stderr) + + // Write it to disk + jsonTxFile := WriteToNewTempFile(t, stdout) + defer os.Remove(jsonTxFile.Name()) + + // Run the encode command, and trim the extras from the stdout capture + success, base64Encoded, _ := f.TxEncode(jsonTxFile.Name()) + require.True(t, success) + trimmedBase64 := strings.Trim(base64Encoded, "\"\n") + + // Decode the base64 + decodedBytes, err := base64.StdEncoding.DecodeString(trimmedBase64) + require.Nil(t, err) + + // Check that the transaction decodes as epxceted + var decodedTx auth.StdTx + require.Nil(t, cdc.UnmarshalBinaryLengthPrefixed(decodedBytes, &decodedTx)) + require.Equal(t, "deadbeef", decodedTx.Memo) +} + func TestGaiaCLIMultisignSortSignatures(t *testing.T) { t.Parallel() f := InitFixtures(t) @@ -769,7 +824,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -777,7 +832,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Sign with baz's key @@ -785,7 +840,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - bazSignatureFile := writeToNewTempFile(t, stdout) + bazSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(bazSignatureFile.Name()) // Multisign, keys in different order @@ -794,7 +849,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature @@ -832,7 +887,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.Empty(t, stderr) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -840,7 +895,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Sign with bar's key @@ -848,7 +903,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - barSignatureFile := writeToNewTempFile(t, stdout) + barSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(barSignatureFile.Name()) // Multisign @@ -857,7 +912,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature @@ -909,10 +964,7 @@ func TestGaiadCollectGentxs(t *testing.T) { f.UnsafeResetAll() // Initialize keys - f.KeysDelete(keyFoo) - f.KeysDelete(keyBar) f.KeysAdd(keyFoo) - f.KeysAdd(keyBar) // Configure json output f.CLIConfig("output", "json") @@ -932,6 +984,42 @@ func TestGaiadCollectGentxs(t *testing.T) { f.Cleanup(gentxDir) } +func TestGaiadAddGenesisAccount(t *testing.T) { + t.Parallel() + f := NewFixtures(t) + + // Reset testing path + f.UnsafeResetAll() + + // Initialize keys + f.KeysDelete(keyFoo) + f.KeysDelete(keyBar) + f.KeysDelete(keyBaz) + f.KeysAdd(keyFoo) + f.KeysAdd(keyBar) + f.KeysAdd(keyBaz) + + // Configure json output + f.CLIConfig("output", "json") + + // Run init + f.GDInit(keyFoo) + + // Add account to genesis.json + bazCoins := sdk.Coins{ + sdk.NewInt64Coin("acoin", 1000000), + sdk.NewInt64Coin("bcoin", 1000000), + } + + f.AddGenesisAccount(f.KeyAddress(keyFoo), startCoins) + f.AddGenesisAccount(f.KeyAddress(keyBar), bazCoins) + genesisState := f.GenesisState() + require.Equal(t, genesisState.Accounts[0].Address, f.KeyAddress(keyFoo)) + require.Equal(t, genesisState.Accounts[1].Address, f.KeyAddress(keyBar)) + require.True(t, genesisState.Accounts[0].Coins.IsEqual(startCoins)) + require.True(t, genesisState.Accounts[1].Coins.IsEqual(bazCoins)) +} + func TestSlashingGetParams(t *testing.T) { t.Parallel() f := InitFixtures(t) @@ -944,4 +1032,19 @@ func TestSlashingGetParams(t *testing.T) { require.Equal(t, time.Duration(120000000000), params.MaxEvidenceAge) require.Equal(t, int64(100), params.SignedBlocksWindow) require.Equal(t, sdk.NewDecWithPrec(5, 1), params.MinSignedPerWindow) + + sinfo := f.QuerySigningInfo(f.GDTendermint("show-validator")) + require.Equal(t, int64(0), sinfo.StartHeight) + require.False(t, sinfo.Tombstoned) +} + +func TestValidateGenesis(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server + proc := f.GDStart() + defer proc.Stop(false) + + f.ValidateGenesis() } diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index 757198506..899a1db0e 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -8,14 +8,15 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/require" cmn "github.com/tendermint/tendermint/libs/common" "github.com/cosmos/cosmos-sdk/client/keys" - "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + appInit "github.com/cosmos/cosmos-sdk/cmd/gaia/init" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/tests" @@ -34,15 +35,22 @@ const ( feeDenom = "feetoken" fee2Denom = "fee2token" keyBaz = "baz" + keyVesting = "vesting" keyFooBarBaz = "foobarbaz" ) -var startCoins = sdk.Coins{ - sdk.NewInt64Coin(feeDenom, 1000000), - sdk.NewInt64Coin(fee2Denom, 1000000), - sdk.NewInt64Coin(fooDenom, 1000), - sdk.NewInt64Coin(denom, 150), -} +var ( + startCoins = sdk.Coins{ + sdk.NewCoin(feeDenom, staking.TokensFromTendermintPower(1000000)), + sdk.NewCoin(fee2Denom, staking.TokensFromTendermintPower(1000000)), + sdk.NewCoin(fooDenom, staking.TokensFromTendermintPower(1000)), + sdk.NewCoin(denom, staking.TokensFromTendermintPower(150)), + } + + vestingCoins = sdk.Coins{ + sdk.NewCoin(feeDenom, staking.TokensFromTendermintPower(500000)), + } +) //___________________________________________________________________________________ // Fixtures @@ -76,6 +84,22 @@ func NewFixtures(t *testing.T) *Fixtures { } } +// GenesisFile returns the path of the genesis file +func (f Fixtures) GenesisFile() string { + return filepath.Join(f.GDHome, "config", "genesis.json") +} + +// GenesisFile returns the application's genesis state +func (f Fixtures) GenesisState() app.GenesisState { + cdc := codec.New() + genDoc, err := appInit.LoadGenesisDoc(cdc, f.GenesisFile()) + require.NoError(f.T, err) + + var appState app.GenesisState + require.NoError(f.T, cdc.UnmarshalJSON(genDoc.AppState, &appState)) + return appState +} + // InitFixtures is called at the beginning of a test // and initializes a chain with 1 validator func InitFixtures(t *testing.T) (f *Fixtures) { @@ -92,6 +116,7 @@ func InitFixtures(t *testing.T) (f *Fixtures) { f.KeysAdd(keyFoo) f.KeysAdd(keyBar) f.KeysAdd(keyBaz) + f.KeysAdd(keyVesting) f.KeysAdd(keyFooBarBaz, "--multisig-threshold=2", fmt.Sprintf( "--multisig=%s,%s,%s", keyFoo, keyBar, keyBaz)) @@ -104,6 +129,12 @@ func InitFixtures(t *testing.T) (f *Fixtures) { // Start an account with tokens f.AddGenesisAccount(f.KeyAddress(keyFoo), startCoins) + f.AddGenesisAccount( + f.KeyAddress(keyVesting), startCoins, + fmt.Sprintf("--vesting-amount=%s", vestingCoins), + fmt.Sprintf("--vesting-start-time=%d", time.Now().UTC().UnixNano()), + fmt.Sprintf("--vesting-end-time=%d", time.Now().Add(60*time.Second).UTC().UnixNano()), + ) f.GenTx(keyFoo) f.CollectGenTxs() return @@ -137,7 +168,7 @@ func (f *Fixtures) UnsafeResetAll(flags ...string) { // GDInit is gaiad init // NOTE: GDInit sets the ChainID for the Fixtures instance func (f *Fixtures) GDInit(moniker string, flags ...string) { - cmd := fmt.Sprintf("gaiad init -o --moniker=%s --home=%s", moniker, f.GDHome) + cmd := fmt.Sprintf("gaiad init -o --home=%s %s", f.GDHome, moniker) _, stderr := tests.ExecuteT(f.T, addFlags(cmd, flags), app.DefaultKeyPass) var chainID string @@ -179,6 +210,21 @@ func (f *Fixtures) GDStart(flags ...string) *tests.Process { return proc } +// GDTendermint returns the results of gaiad tendermint [query] +func (f *Fixtures) GDTendermint(query string) string { + cmd := fmt.Sprintf("gaiad tendermint %s --home=%s", query, f.GDHome) + success, stdout, stderr := executeWriteRetStdStreams(f.T, cmd) + require.Empty(f.T, stderr) + require.True(f.T, success) + return strings.TrimSpace(stdout) +} + +// ValidateGenesis runs gaiad validate-genesis +func (f *Fixtures) ValidateGenesis() { + cmd := fmt.Sprintf("gaiad validate-genesis --home=%s", f.GDHome) + executeWriteCheckErr(f.T, cmd) +} + //___________________________________________________________________________________ // gaiacli keys @@ -194,6 +240,18 @@ func (f *Fixtures) KeysAdd(name string, flags ...string) { executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } +// KeysAddRecover prepares gaiacli keys add --recover +func (f *Fixtures) KeysAddRecover(name, mnemonic string, flags ...string) { + cmd := fmt.Sprintf("gaiacli keys add --home=%s --recover %s", f.GCLIHome, name) + executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass, mnemonic) +} + +// KeysAddRecoverHDPath prepares gaiacli keys add --recover --account --index +func (f *Fixtures) KeysAddRecoverHDPath(name, mnemonic string, account uint32, index uint32, flags ...string) { + cmd := fmt.Sprintf("gaiacli keys add --home=%s --recover %s --account %d --index %d", f.GCLIHome, name, account, index) + executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass, mnemonic) +} + // KeysShow is gaiacli keys show func (f *Fixtures) KeysShow(name string, flags ...string) keys.KeyOutput { cmd := fmt.Sprintf("gaiacli keys show --home=%s %s", f.GCLIHome, name) @@ -226,7 +284,7 @@ func (f *Fixtures) CLIConfig(key, value string, flags ...string) { // TxSend is gaiacli tx send func (f *Fixtures) TxSend(from string, to sdk.AccAddress, amount sdk.Coin, flags ...string) (bool, string, string) { - cmd := fmt.Sprintf("gaiacli tx send %v --amount=%s --to=%s --from=%s", f.Flags(), amount, to, from) + cmd := fmt.Sprintf("gaiacli tx send %s %s %v --from=%s", to, amount, f.Flags(), from) return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } @@ -236,12 +294,18 @@ func (f *Fixtures) TxSign(signer, fileName string, flags ...string) (bool, strin return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } -// TxBroadcast is gaiacli tx sign +// TxBroadcast is gaiacli tx broadcast func (f *Fixtures) TxBroadcast(fileName string, flags ...string) (bool, string, string) { cmd := fmt.Sprintf("gaiacli tx broadcast %v %v", f.Flags(), fileName) return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } +// TxEncode is gaiacli tx encode +func (f *Fixtures) TxEncode(fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("gaiacli tx encode %v %v", f.Flags(), fileName) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) +} + // TxMultisign is gaiacli tx multisign func (f *Fixtures) TxMultisign(fileName, name string, signaturesFiles []string, flags ...string) (bool, string, string) { @@ -260,12 +324,13 @@ func (f *Fixtures) TxStakingCreateValidator(from, consPubKey string, amount sdk. cmd := fmt.Sprintf("gaiacli tx staking create-validator %v --from=%s --pubkey=%s", f.Flags(), from, consPubKey) cmd += fmt.Sprintf(" --amount=%v --moniker=%v --commission-rate=%v", amount, from, "0.05") cmd += fmt.Sprintf(" --commission-max-rate=%v --commission-max-change-rate=%v", "0.20", "0.10") + cmd += fmt.Sprintf(" --min-self-delegation=%v", "1") return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } // TxStakingUnbond is gaiacli tx staking unbond func (f *Fixtures) TxStakingUnbond(from, shares string, validator sdk.ValAddress, flags ...string) bool { - cmd := fmt.Sprintf("gaiacli tx staking unbond %v --from=%s --validator=%s --shares-amount=%v", f.Flags(), from, validator, shares) + cmd := fmt.Sprintf("gaiacli tx staking unbond %s %v --from=%s %v", validator, shares, from, f.Flags()) return executeWrite(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } @@ -314,10 +379,10 @@ func (f *Fixtures) QueryAccount(address sdk.AccAddress, flags ...string) auth.Ba // gaiacli query txs // QueryTxs is gaiacli query txs -func (f *Fixtures) QueryTxs(page, limit int, tags ...string) []tx.Info { +func (f *Fixtures) QueryTxs(page, limit int, tags ...string) []sdk.TxResponse { cmd := fmt.Sprintf("gaiacli query txs --page=%d --limit=%d --tags='%s' %v", page, limit, queryTags(tags), f.Flags()) out, _ := tests.ExecuteT(f.T, cmd, "") - var txs []tx.Info + var txs []sdk.TxResponse cdc := app.MakeCodec() err := cdc.UnmarshalJSON([]byte(out), &txs) require.NoError(f.T, err, "out %v\n, err %v", out, err) @@ -498,6 +563,18 @@ func (f *Fixtures) QueryGovDeposits(propsalID int, flags ...string) []gov.Deposi //___________________________________________________________________________________ // query slashing +// QuerySigningInfo returns the signing info for a validator +func (f *Fixtures) QuerySigningInfo(val string) slashing.ValidatorSigningInfo { + cmd := fmt.Sprintf("gaiacli query slashing signing-info %s %s", val, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var sinfo slashing.ValidatorSigningInfo + err := cdc.UnmarshalJSON([]byte(res), &sinfo) + require.NoError(f.T, err) + return sinfo +} + // QuerySlashingParams is gaiacli query slashing params func (f *Fixtures) QuerySlashingParams() slashing.Params { cmd := fmt.Sprintf("gaiacli query slashing params %s", f.Flags()) @@ -569,7 +646,8 @@ func queryTags(tags []string) (out string) { return strings.TrimSuffix(out, "&") } -func writeToNewTempFile(t *testing.T, s string) *os.File { +// Write the given string to a new temporary file +func WriteToNewTempFile(t *testing.T, s string) *os.File { fp, err := ioutil.TempFile(os.TempDir(), "cosmos_cli_test_") require.Nil(t, err) _, err = fp.WriteString(s) diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index f390324aa..a5939116f 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -25,7 +25,7 @@ import ( at "github.com/cosmos/cosmos-sdk/x/auth" auth "github.com/cosmos/cosmos-sdk/x/auth/client/rest" bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest" - dist "github.com/cosmos/cosmos-sdk/x/distribution" + dist "github.com/cosmos/cosmos-sdk/x/distribution/client/rest" gv "github.com/cosmos/cosmos-sdk/x/gov" gov "github.com/cosmos/cosmos-sdk/x/gov/client/rest" sl "github.com/cosmos/cosmos-sdk/x/slashing" @@ -35,6 +35,7 @@ import ( authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + distcmd "github.com/cosmos/cosmos-sdk/x/distribution" distClient "github.com/cosmos/cosmos-sdk/x/distribution/client" govClient "github.com/cosmos/cosmos-sdk/x/gov/client" slashingClient "github.com/cosmos/cosmos-sdk/x/slashing/client" @@ -65,7 +66,7 @@ func main() { // TODO: Make the lcd command take a list of ModuleClient mc := []sdk.ModuleClients{ govClient.NewModuleClient(gv.StoreKey, cdc), - distClient.NewModuleClient(dist.StoreKey, cdc), + distClient.NewModuleClient(distcmd.StoreKey, cdc), stakingClient.NewModuleClient(st.StoreKey, cdc), slashingClient.NewModuleClient(sl.StoreKey, cdc), } @@ -84,7 +85,7 @@ func main() { // Construct Root Command rootCmd.AddCommand( rpc.StatusCommand(), - client.ConfigCmd(), + client.ConfigCmd(app.DefaultCLIHome), queryCmd(cdc, mc), txCmd(cdc, mc), client.LineBreak, @@ -114,7 +115,7 @@ func queryCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { } queryCmd.AddCommand( - rpc.ValidatorCommand(), + rpc.ValidatorCommand(cdc), rpc.BlockCommand(), tx.SearchTxCmd(cdc), tx.QueryTxCmd(cdc), @@ -140,7 +141,8 @@ func txCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { client.LineBreak, authcmd.GetSignCommand(cdc), authcmd.GetMultiSignCommand(cdc), - bankcmd.GetBroadcastCommand(cdc), + authcmd.GetBroadcastCommand(cdc), + authcmd.GetEncodeCommand(cdc), client.LineBreak, ) @@ -161,6 +163,7 @@ func registerRoutes(rs *lcd.RestServer) { tx.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) auth.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, at.StoreKey) bank.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) + dist.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, distcmd.StoreKey) staking.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) slashing.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) gov.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index 4014bef73..d6d88e0df 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -43,6 +43,7 @@ func main() { rootCmd.AddCommand(gaiaInit.TestnetFilesCmd(ctx, cdc)) rootCmd.AddCommand(gaiaInit.GenTxCmd(ctx, cdc)) rootCmd.AddCommand(gaiaInit.AddGenesisAccountCmd(ctx, cdc)) + rootCmd.AddCommand(gaiaInit.ValidateGenesisCmd(ctx, cdc)) rootCmd.AddCommand(client.NewCompletionCmd(rootCmd, true)) server.AddCommands(ctx, cdc, rootCmd, newApp, exportAppStateAndTMValidators) @@ -59,13 +60,13 @@ func main() { func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { return app.NewGaiaApp( logger, db, traceStore, true, - baseapp.SetPruning(store.NewPruningOptions(viper.GetString("pruning"))), + baseapp.SetPruning(store.NewPruningOptionsFromString(viper.GetString("pruning"))), baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), ) } func exportAppStateAndTMValidators( - logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, + logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailWhiteList []string, ) (json.RawMessage, []tmtypes.GenesisValidator, error) { if height != -1 { gApp := app.NewGaiaApp(logger, db, traceStore, false) @@ -73,8 +74,8 @@ func exportAppStateAndTMValidators( if err != nil { return nil, nil, err } - return gApp.ExportAppStateAndValidators(forZeroHeight) + return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } gApp := app.NewGaiaApp(logger, db, traceStore, true) - return gApp.ExportAppStateAndValidators(forZeroHeight) + return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } diff --git a/cmd/gaia/cmd/gaiadebug/hack.go b/cmd/gaia/cmd/gaiadebug/hack.go index 022192861..eb4c8749b 100644 --- a/cmd/gaia/cmd/gaiadebug/hack.go +++ b/cmd/gaia/cmd/gaiadebug/hack.go @@ -50,7 +50,7 @@ func runHackCmd(cmd *cobra.Command, args []string) error { fmt.Println(err) os.Exit(1) } - app := NewGaiaApp(logger, db, baseapp.SetPruning(store.NewPruningOptions(viper.GetString("pruning")))) + app := NewGaiaApp(logger, db, baseapp.SetPruning(store.NewPruningOptionsFromString(viper.GetString("pruning")))) // print some info id := app.LastCommitID() @@ -178,7 +178,7 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, baseAppOptions ...func(*bam.BaseAp ) // add handlers - app.bankKeeper = bank.NewBaseKeeper(app.accountKeeper) + app.bankKeeper = bank.NewBaseKeeper(app.accountKeeper, app.paramsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) app.stakingKeeper = staking.NewKeeper(app.cdc, app.keyStaking, app.tkeyStaking, app.bankKeeper, app.paramsKeeper.Subspace(staking.DefaultParamspace), staking.DefaultCodespace) app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakingKeeper, app.paramsKeeper.Subspace(slashing.DefaultParamspace), slashing.DefaultCodespace) diff --git a/cmd/gaia/init/collect.go b/cmd/gaia/init/collect.go index e630d494c..858639486 100644 --- a/cmd/gaia/init/collect.go +++ b/cmd/gaia/init/collect.go @@ -1,5 +1,7 @@ package init +// DONTCOVER + import ( "encoding/json" "path/filepath" @@ -44,7 +46,7 @@ func CollectGenTxsCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { return err } - genDoc, err := loadGenesisDoc(cdc, config.GenesisFile()) + genDoc, err := LoadGenesisDoc(cdc, config.GenesisFile()) if err != nil { return err } diff --git a/cmd/gaia/init/genesis_accts.go b/cmd/gaia/init/genesis_accts.go index bb9c4af0c..5aa73dae9 100644 --- a/cmd/gaia/init/genesis_accts.go +++ b/cmd/gaia/init/genesis_accts.go @@ -1,7 +1,6 @@ package init import ( - "encoding/json" "fmt" "github.com/spf13/cobra" @@ -17,7 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" ) -// AddGenesisAccountCmd returns add-genesis-account cobra Command +// AddGenesisAccountCmd returns add-genesis-account cobra Command. func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "add-genesis-account [address_or_key_name] [coin][,[coin]]", @@ -29,27 +28,37 @@ func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command addr, err := sdk.AccAddressFromBech32(args[0]) if err != nil { - kb, err := keys.GetKeyBaseFromDir(viper.GetString(flagClientHome)) + kb, err := keys.NewKeyBaseFromDir(viper.GetString(flagClientHome)) if err != nil { return err } + info, err := kb.Get(args[0]) if err != nil { return err } + addr = info.GetAddress() } + coins, err := sdk.ParseCoins(args[1]) if err != nil { return err } - coins.Sort() + + vestingStart := viper.GetInt64(flagVestingStart) + vestingEnd := viper.GetInt64(flagVestingEnd) + vestingAmt, err := sdk.ParseCoins(viper.GetString(flagVestingAmt)) + if err != nil { + return err + } genFile := config.GenesisFile() if !common.FileExists(genFile) { return fmt.Errorf("%s does not exist, run `gaiad init` first", genFile) } - genDoc, err := loadGenesisDoc(cdc, genFile) + + genDoc, err := LoadGenesisDoc(cdc, genFile) if err != nil { return err } @@ -59,7 +68,12 @@ func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command return err } - appStateJSON, err := addGenesisAccount(cdc, appState, addr, coins) + appState, err = addGenesisAccount(cdc, appState, addr, coins, vestingAmt, vestingStart, vestingEnd) + if err != nil { + return err + } + + appStateJSON, err := cdc.MarshalJSON(appState) if err != nil { return err } @@ -70,18 +84,58 @@ func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command cmd.Flags().String(cli.HomeFlag, app.DefaultNodeHome, "node's home directory") cmd.Flags().String(flagClientHome, app.DefaultCLIHome, "client's home directory") + cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") + cmd.Flags().Uint64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") + cmd.Flags().Uint64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") + return cmd } -func addGenesisAccount(cdc *codec.Codec, appState app.GenesisState, addr sdk.AccAddress, coins sdk.Coins) (json.RawMessage, error) { +func addGenesisAccount( + cdc *codec.Codec, appState app.GenesisState, addr sdk.AccAddress, + coins, vestingAmt sdk.Coins, vestingStart, vestingEnd int64, +) (app.GenesisState, error) { + for _, stateAcc := range appState.Accounts { if stateAcc.Address.Equals(addr) { - return nil, fmt.Errorf("the application state already contains account %v", addr) + return appState, fmt.Errorf("the application state already contains account %v", addr) } } acc := auth.NewBaseAccountWithAddress(addr) acc.Coins = coins - appState.Accounts = append(appState.Accounts, app.NewGenesisAccount(&acc)) - return cdc.MarshalJSON(appState) + + if !vestingAmt.IsZero() { + var vacc auth.VestingAccount + + bvacc := &auth.BaseVestingAccount{ + BaseAccount: &acc, + OriginalVesting: vestingAmt, + EndTime: vestingEnd, + } + + if bvacc.OriginalVesting.IsAllGT(acc.Coins) { + return appState, fmt.Errorf("vesting amount cannot be greater than total amount") + } + if vestingStart >= vestingEnd { + return appState, fmt.Errorf("vesting start time must before end time") + } + + if vestingStart != 0 { + vacc = &auth.ContinuousVestingAccount{ + BaseVestingAccount: bvacc, + StartTime: vestingStart, + } + } else { + vacc = &auth.DelayedVestingAccount{ + BaseVestingAccount: bvacc, + } + } + + appState.Accounts = append(appState.Accounts, app.NewGenesisAccountI(vacc)) + } else { + appState.Accounts = append(appState.Accounts, app.NewGenesisAccount(&acc)) + } + + return appState, nil } diff --git a/cmd/gaia/init/genesis_accts_test.go b/cmd/gaia/init/genesis_accts_test.go index 42a36b263..2c17acc54 100644 --- a/cmd/gaia/init/genesis_accts_test.go +++ b/cmd/gaia/init/genesis_accts_test.go @@ -15,9 +15,12 @@ func TestAddGenesisAccount(t *testing.T) { cdc := codec.New() addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) type args struct { - appState app.GenesisState - addr sdk.AccAddress - coins sdk.Coins + appState app.GenesisState + addr sdk.AccAddress + coins sdk.Coins + vestingAmt sdk.Coins + vestingStart int64 + vestingEnd int64 } tests := []struct { name string @@ -30,16 +33,55 @@ func TestAddGenesisAccount(t *testing.T) { app.GenesisState{}, addr1, sdk.Coins{}, + sdk.Coins{}, + 0, + 0, }, - false}, - {"dup account", args{ - app.GenesisState{Accounts: []app.GenesisAccount{{Address: addr1}}}, - addr1, - sdk.Coins{}}, true}, + false, + }, + { + "dup account", + args{ + app.GenesisState{Accounts: []app.GenesisAccount{{Address: addr1}}}, + addr1, + sdk.Coins{}, + sdk.Coins{}, + 0, + 0, + }, + true, + }, + { + "invalid vesting amount", + args{ + app.GenesisState{}, + addr1, + sdk.Coins{sdk.NewInt64Coin("stake", 50)}, + sdk.Coins{sdk.NewInt64Coin("stake", 100)}, + 0, + 0, + }, + true, + }, + { + "invalid vesting times", + args{ + app.GenesisState{}, + addr1, + sdk.Coins{sdk.NewInt64Coin("stake", 50)}, + sdk.Coins{sdk.NewInt64Coin("stake", 50)}, + 1654668078, + 1554668078, + }, + true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := addGenesisAccount(cdc, tt.args.appState, tt.args.addr, tt.args.coins) + _, err := addGenesisAccount( + cdc, tt.args.appState, tt.args.addr, tt.args.coins, + tt.args.vestingAmt, tt.args.vestingStart, tt.args.vestingEnd, + ) require.Equal(t, tt.wantErr, (err != nil)) }) } diff --git a/cmd/gaia/init/gentx.go b/cmd/gaia/init/gentx.go index 25de93aab..ef3ee7106 100644 --- a/cmd/gaia/init/gentx.go +++ b/cmd/gaia/init/gentx.go @@ -1,5 +1,7 @@ package init +// DONTCOVER + import ( "bytes" "fmt" @@ -26,15 +28,17 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/staking/client/cli" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) -const ( - defaultAmount = "100" + stakingTypes.DefaultBondDenom +var ( + defaultTokens = staking.TokensFromTendermintPower(100) + defaultAmount = defaultTokens.String() + staking.DefaultBondDenom defaultCommissionRate = "0.1" defaultCommissionMaxRate = "0.2" defaultCommissionMaxChangeRate = "0.01" + defaultMinSelfDelegation = "1" ) // GenTxCmd builds the gaiad gentx command. @@ -52,7 +56,8 @@ following delegation and commission default parameters: commission rate: %s commission max rate: %s commission max change rate: %s -`, defaultAmount, defaultCommissionRate, defaultCommissionMaxRate, defaultCommissionMaxChangeRate), + minimum self delegation: %s +`, defaultAmount, defaultCommissionRate, defaultCommissionMaxRate, defaultCommissionMaxChangeRate, defaultMinSelfDelegation), RunE: func(cmd *cobra.Command, args []string) error { config := ctx.Config @@ -61,12 +66,19 @@ following delegation and commission default parameters: if err != nil { return err } - ip, err := server.ExternalIP() - if err != nil { - return err + + // Read --nodeID, if empty take it from priv_validator.json + if nodeIDString := viper.GetString(cli.FlagNodeID); nodeIDString != "" { + nodeID = nodeIDString } - genDoc, err := loadGenesisDoc(cdc, config.GenesisFile()) + ip := viper.GetString(cli.FlagIP) + if ip == "" { + fmt.Fprintf(os.Stderr, "couldn't retrieve an external IP; "+ + "the tx's memo field will be unset") + } + + genDoc, err := LoadGenesisDoc(cdc, config.GenesisFile()) if err != nil { return err } @@ -76,7 +88,7 @@ following delegation and commission default parameters: return err } - kb, err := keys.GetKeyBaseFromDir(viper.GetString(flagClientHome)) + kb, err := keys.NewKeyBaseFromDir(viper.GetString(flagClientHome)) if err != nil { return err } @@ -120,7 +132,8 @@ following delegation and commission default parameters: // write the unsigned transaction to the buffer w := bytes.NewBuffer([]byte{}) - if err := utils.PrintUnsignedStdTx(w, txBldr, cliCtx, []sdk.Msg{msg}, true); err != nil { + cliCtx = cliCtx.WithOutput(w) + if err = utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg}, true); err != nil { return err } @@ -154,12 +167,17 @@ following delegation and commission default parameters: }, } + ip, _ := server.ExternalIP() + cmd.Flags().String(tmcli.HomeFlag, app.DefaultNodeHome, "node's home directory") cmd.Flags().String(flagClientHome, app.DefaultCLIHome, "client's home directory") cmd.Flags().String(client.FlagName, "", "name of private key with which to sign the gentx") cmd.Flags().String(client.FlagOutputDocument, "", "write the genesis transaction JSON document to the given file instead of the default location") + cmd.Flags().String(cli.FlagIP, ip, "The node's public IP") + cmd.Flags().String(cli.FlagNodeID, "", "The node's NodeID") cmd.Flags().AddFlagSet(cli.FsCommissionCreate) + cmd.Flags().AddFlagSet(cli.FsMinSelfDelegation) cmd.Flags().AddFlagSet(cli.FsAmount) cmd.Flags().AddFlagSet(cli.FsPk) cmd.MarkFlagRequired(client.FlagName) @@ -202,7 +220,7 @@ func prepareFlagsForTxCreateValidator(config *cfg.Config, nodeID, ip, chainID st viper.Set(cli.FlagNodeID, nodeID) // --node-id viper.Set(cli.FlagIP, ip) // --ip viper.Set(cli.FlagPubKey, sdk.MustBech32ifyConsPub(valPubKey)) // --pubkey - viper.Set(cli.FlagGenesisFormat, true) // --genesis-format + viper.Set(client.FlagGenerateOnly, true) // --genesis-format viper.Set(cli.FlagMoniker, config.Moniker) // --moniker if config.Moniker == "" { viper.Set(cli.FlagMoniker, viper.GetString(client.FlagName)) @@ -219,6 +237,9 @@ func prepareFlagsForTxCreateValidator(config *cfg.Config, nodeID, ip, chainID st if viper.GetString(cli.FlagCommissionMaxChangeRate) == "" { viper.Set(cli.FlagCommissionMaxChangeRate, defaultCommissionMaxChangeRate) } + if viper.GetString(cli.FlagMinSelfDelegation) == "" { + viper.Set(cli.FlagMinSelfDelegation, defaultMinSelfDelegation) + } } func makeOutputFilepath(rootDir, nodeID string) (string, error) { diff --git a/cmd/gaia/init/init.go b/cmd/gaia/init/init.go index 922cfbafa..506557d3f 100644 --- a/cmd/gaia/init/init.go +++ b/cmd/gaia/init/init.go @@ -19,9 +19,11 @@ import ( ) const ( - flagOverwrite = "overwrite" - flagClientHome = "home-client" - flagMoniker = "moniker" + flagOverwrite = "overwrite" + flagClientHome = "home-client" + flagVestingStart = "vesting-start-time" + flagVestingEnd = "vesting-end-time" + flagVestingAmt = "vesting-amount" ) type printInfo struct { @@ -32,25 +34,25 @@ type printInfo struct { AppMessage json.RawMessage `json:"app_message"` } -// nolint: errcheck func displayInfo(cdc *codec.Codec, info printInfo) error { out, err := codec.MarshalJSONIndent(cdc, info) if err != nil { return err } - fmt.Fprintf(os.Stderr, "%s\n", string(out)) + + fmt.Fprintf(os.Stderr, "%s\n", string(out)) // nolint: errcheck return nil } -// get cmd to initialize all files for tendermint and application -// nolint -func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { +// InitCmd returns a command that initializes all files needed for Tendermint +// and the respective application. +func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { // nolint: golint cmd := &cobra.Command{ - Use: "init", + Use: "init [moniker]", Short: "Initialize private validator, p2p, genesis, and application configuration files", Long: `Initialize validators's and node's configuration files.`, - Args: cobra.NoArgs, - RunE: func(_ *cobra.Command, _ []string) error { + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { config := ctx.Config config.SetRoot(viper.GetString(cli.HomeFlag)) @@ -64,7 +66,7 @@ func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { return err } - config.Moniker = viper.GetString(flagMoniker) + config.Moniker = args[0] var appState json.RawMessage genFile := config.GenesisFile() @@ -81,7 +83,6 @@ func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { toPrint := newPrintInfo(config.Moniker, chainID, nodeID, "", appState) cfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) - return displayInfo(cdc, toPrint) }, } @@ -89,8 +90,6 @@ func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { cmd.Flags().String(cli.HomeFlag, app.DefaultNodeHome, "node's home directory") cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite the genesis.json file") cmd.Flags().String(client.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") - cmd.Flags().String(flagMoniker, "", "set the validator's moniker") - cmd.MarkFlagRequired(flagMoniker) return cmd } diff --git a/cmd/gaia/init/init_test.go b/cmd/gaia/init/init_test.go index faf324e6c..39975bb29 100644 --- a/cmd/gaia/init/init_test.go +++ b/cmd/gaia/init/init_test.go @@ -8,20 +8,16 @@ import ( "testing" "time" - "github.com/tendermint/tendermint/libs/cli" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" - + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/mock" + "github.com/spf13/viper" "github.com/stretchr/testify/require" abciServer "github.com/tendermint/tendermint/abci/server" tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/libs/cli" "github.com/tendermint/tendermint/libs/log" - - "github.com/cosmos/cosmos-sdk/server" - "github.com/cosmos/cosmos-sdk/server/mock" - - "github.com/spf13/viper" ) func TestInitCmd(t *testing.T) { @@ -36,10 +32,7 @@ func TestInitCmd(t *testing.T) { cdc := app.MakeCodec() cmd := InitCmd(ctx, cdc) - viper.Set(flagMoniker, "gaianode-test") - - err = cmd.RunE(nil, nil) - require.NoError(t, err) + require.NoError(t, cmd.RunE(nil, []string{"gaianode-test"})) } func setupClientHome(t *testing.T) func() { @@ -64,11 +57,9 @@ func TestEmptyState(t *testing.T) { ctx := server.NewContext(cfg, logger) cdc := app.MakeCodec() - viper.Set(flagMoniker, "gaianode-test") cmd := InitCmd(ctx, cdc) - err = cmd.RunE(nil, nil) - require.NoError(t, err) + require.NoError(t, cmd.RunE(nil, []string{"gaianode-test"})) old := os.Stdout r, w, _ := os.Pipe() @@ -88,7 +79,6 @@ func TestEmptyState(t *testing.T) { w.Close() os.Stdout = old out := <-outC - require.Contains(t, out, "WARNING: State is not initialized") require.Contains(t, out, "genesis_time") require.Contains(t, out, "chain_id") require.Contains(t, out, "consensus_params") @@ -103,7 +93,6 @@ func TestStartStandAlone(t *testing.T) { os.RemoveAll(home) }() viper.Set(cli.HomeFlag, home) - viper.Set(client.FlagName, "moniker") defer setupClientHome(t)() logger := log.NewNopLogger() @@ -112,8 +101,7 @@ func TestStartStandAlone(t *testing.T) { ctx := server.NewContext(cfg, logger) cdc := app.MakeCodec() initCmd := InitCmd(ctx, cdc) - err = initCmd.RunE(nil, nil) - require.NoError(t, err) + require.NoError(t, initCmd.RunE(nil, []string{"gaianode-test"})) app, err := mock.NewApp(home, logger) require.Nil(t, err) diff --git a/cmd/gaia/init/testnet.go b/cmd/gaia/init/testnet.go index 111a58032..346e6a4b3 100644 --- a/cmd/gaia/init/testnet.go +++ b/cmd/gaia/init/testnet.go @@ -1,5 +1,7 @@ package init +// DONTCOVER + import ( "encoding/json" "fmt" @@ -7,6 +9,8 @@ import ( "os" "path/filepath" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/codec" @@ -188,23 +192,31 @@ func initTestnet(config *tmconfig.Config, cdc *codec.Codec) error { return err } + accTokens := staking.TokensFromTendermintPower(1000) + accStakingTokens := staking.TokensFromTendermintPower(500) accs = append(accs, app.GenesisAccount{ Address: addr, Coins: sdk.Coins{ - sdk.NewInt64Coin(fmt.Sprintf("%stoken", nodeDirName), 1000), - sdk.NewInt64Coin(stakingtypes.DefaultBondDenom, 500), + sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), accTokens), + sdk.NewCoin(stakingtypes.DefaultBondDenom, accStakingTokens), }, }) + valTokens := staking.TokensFromTendermintPower(100) msg := staking.NewMsgCreateValidator( sdk.ValAddress(addr), valPubKeys[i], - sdk.NewInt64Coin(stakingtypes.DefaultBondDenom, 100), + sdk.NewCoin(stakingtypes.DefaultBondDenom, valTokens), staking.NewDescription(nodeDirName, "", "", ""), staking.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), ) + kb, err := keys.NewKeyBaseFromDir(clientDir) + if err != nil { + return err + } tx := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{}, memo) - txBldr := authtx.NewTxBuilderFromCLI().WithChainID(chainID).WithMemo(memo) + txBldr := authtx.NewTxBuilderFromCLI().WithChainID(chainID).WithMemo(memo).WithKeybase(kb) signedTx, err := txBldr.SignStdTx(nodeDirName, app.DefaultKeyPass, tx, false) if err != nil { @@ -295,7 +307,7 @@ func collectGenFiles( nodeID, valPubKey := nodeIDs[i], valPubKeys[i] initCfg := newInitConfig(chainID, gentxsDir, moniker, nodeID, valPubKey) - genDoc, err := loadGenesisDoc(cdc, config.GenesisFile()) + genDoc, err := LoadGenesisDoc(cdc, config.GenesisFile()) if err != nil { return err } diff --git a/cmd/gaia/init/utils.go b/cmd/gaia/init/utils.go index 149b4d10e..4e2f2f26f 100644 --- a/cmd/gaia/init/utils.go +++ b/cmd/gaia/init/utils.go @@ -88,7 +88,8 @@ func InitializeNodeValidatorFiles( return nodeID, valPubKey, nil } -func loadGenesisDoc(cdc *amino.Codec, genFile string) (genDoc types.GenesisDoc, err error) { +// LoadGenesisDoc reads and unmarshals GenesisDoc from the given file. +func LoadGenesisDoc(cdc *amino.Codec, genFile string) (genDoc types.GenesisDoc, err error) { genContents, err := ioutil.ReadFile(genFile) if err != nil { return genDoc, err diff --git a/cmd/gaia/init/utils_test.go b/cmd/gaia/init/utils_test.go new file mode 100644 index 000000000..3a5a13690 --- /dev/null +++ b/cmd/gaia/init/utils_test.go @@ -0,0 +1,49 @@ +package init + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/tests" + + "github.com/stretchr/testify/require" +) + +func TestExportGenesisFileWithTime(t *testing.T) { + t.Parallel() + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + + fname := filepath.Join(dir, "genesis.json") + require.NoError(t, ExportGenesisFileWithTime(fname, "test", nil, json.RawMessage(""), time.Now())) +} + +func TestLoadGenesisDoc(t *testing.T) { + t.Parallel() + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + + fname := filepath.Join(dir, "genesis.json") + require.NoError(t, ExportGenesisFileWithTime(fname, "test", nil, json.RawMessage(""), time.Now())) + + _, err := LoadGenesisDoc(codec.Cdc, fname) + require.NoError(t, err) + + // Non-existing file + _, err = LoadGenesisDoc(codec.Cdc, "non-existing-file") + require.Error(t, err) + + malformedFilename := filepath.Join(dir, "malformed") + malformedFile, err := os.Create(malformedFilename) + require.NoError(t, err) + fmt.Fprint(malformedFile, "invalidjson") + malformedFile.Close() + // Non-existing file + _, err = LoadGenesisDoc(codec.Cdc, malformedFilename) + require.Error(t, err) +} diff --git a/cmd/gaia/init/validate_genesis.go b/cmd/gaia/init/validate_genesis.go new file mode 100644 index 000000000..eb0eaa3a2 --- /dev/null +++ b/cmd/gaia/init/validate_genesis.go @@ -0,0 +1,51 @@ +package init + +import ( + "fmt" + "os" + + "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" + "github.com/tendermint/tendermint/types" +) + +// Validate genesis command takes +func ValidateGenesisCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "validate-genesis [file]", + Args: cobra.RangeArgs(0, 1), + Short: "validates the genesis file at the default location or at the location passed as an arg", + RunE: func(cmd *cobra.Command, args []string) (err error) { + + // Load default if passed no args, otherwise load passed file + var genesis string + if len(args) == 0 { + genesis = ctx.Config.GenesisFile() + } else { + genesis = args[0] + } + + //nolint + fmt.Fprintf(os.Stderr, "validating genesis file at %s\n", genesis) + + var genDoc types.GenesisDoc + if genDoc, err = LoadGenesisDoc(cdc, genesis); err != nil { + return fmt.Errorf("Error loading genesis doc from %s: %s", genesis, err.Error()) + } + + var genstate app.GenesisState + if err = cdc.UnmarshalJSON(genDoc.AppState, &genstate); err != nil { + return fmt.Errorf("Error unmarshaling genesis doc %s: %s", genesis, err.Error()) + } + + if err = app.GaiaValidateGenesisState(genstate); err != nil { + return fmt.Errorf("Error validating genesis file %s: %s", genesis, err.Error()) + } + + fmt.Printf("File at %s is a valid genesis file for gaiad\n", genesis) + return nil + }, + } +} diff --git a/crypto/keys/hd/hdpath.go b/crypto/keys/hd/hdpath.go index 112abe0b6..050b0a39e 100644 --- a/crypto/keys/hd/hdpath.go +++ b/crypto/keys/hd/hdpath.go @@ -14,6 +14,7 @@ package hd import ( "crypto/hmac" "crypto/sha512" + "encoding/binary" "errors" "fmt" @@ -62,19 +63,7 @@ func NewParamsFromPath(path string) (*BIP44Params, error) { return nil, fmt.Errorf("path length is wrong. Expected 5, got %d", len(spl)) } - if spl[0] != "44'" { - return nil, fmt.Errorf("first field in path must be 44', got %v", spl[0]) - } - - if !isHardened(spl[1]) || !isHardened(spl[2]) { - return nil, - fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %v and %v", spl[1], spl[2]) - } - if isHardened(spl[3]) || isHardened(spl[4]) { - return nil, - fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %v and %v", spl[3], spl[4]) - } - + // Check items can be parsed purpose, err := hardenedInt(spl[0]) if err != nil { return nil, err @@ -91,15 +80,30 @@ func NewParamsFromPath(path string) (*BIP44Params, error) { if err != nil { return nil, err } - if !(change == 0 || change == 1) { - return nil, fmt.Errorf("change field can only be 0 or 1") - } addressIdx, err := hardenedInt(spl[4]) if err != nil { return nil, err } + // Confirm valid values + if spl[0] != "44'" { + return nil, fmt.Errorf("first field in path must be 44', got %v", spl[0]) + } + + if !isHardened(spl[1]) || !isHardened(spl[2]) { + return nil, + fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %v and %v", spl[1], spl[2]) + } + if isHardened(spl[3]) || isHardened(spl[4]) { + return nil, + fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %v and %v", spl[3], spl[4]) + } + + if !(change == 0 || change == 1) { + return nil, fmt.Errorf("change field can only be 0 or 1") + } + return &BIP44Params{ purpose: purpose, coinType: coinType, @@ -132,7 +136,7 @@ func NewFundraiserParams(account uint32, addressIdx uint32) *BIP44Params { return NewParams(44, 118, account, false, addressIdx) } -// Return the BIP44 fields as an array. +// DerivationPath returns the BIP44 fields as an array. func (p BIP44Params) DerivationPath() []uint32 { change := uint32(0) if p.change { @@ -251,8 +255,10 @@ func i64(key []byte, data []byte) (IL [32]byte, IR [32]byte) { mac := hmac.New(sha512.New, key) // sha512 does not err _, _ = mac.Write(data) + I := mac.Sum(nil) copy(IL[:], I[:32]) copy(IR[:], I[32:]) + return } diff --git a/crypto/keys/hd/hdpath_test.go b/crypto/keys/hd/hdpath_test.go index f310fc355..275b714ce 100644 --- a/crypto/keys/hd/hdpath_test.go +++ b/crypto/keys/hd/hdpath_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/cosmos/go-bip39" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var defaultBIP39Passphrase = "" @@ -21,7 +21,27 @@ func mnemonicToSeed(mnemonic string) []byte { func ExampleStringifyPathParams() { path := NewParams(44, 0, 0, false, 0) fmt.Println(path.String()) - // Output: 44'/0'/0'/0/0 + path = NewParams(44, 33, 7, true, 9) + fmt.Println(path.String()) + // Output: + // 44'/0'/0'/0/0 + // 44'/33'/7'/1/9 +} + +func TestStringifyFundraiserPathParams(t *testing.T) { + path := NewFundraiserParams(4, 22) + require.Equal(t, "44'/118'/4'/0/22", path.String()) + + path = NewFundraiserParams(4, 57) + require.Equal(t, "44'/118'/4'/0/57", path.String()) +} + +func TestPathToArray(t *testing.T) { + path := NewParams(44, 118, 1, false, 4) + require.Equal(t, "[44 118 1 0 4]", fmt.Sprintf("%v", path.DerivationPath())) + + path = NewParams(44, 118, 2, true, 15) + require.Equal(t, "[44 118 2 1 15]", fmt.Sprintf("%v", path.DerivationPath())) } func TestParamsFromPath(t *testing.T) { @@ -60,6 +80,11 @@ func TestParamsFromPath(t *testing.T) { {"44'/0'/0'/0/0'"}, // fifth field must not have ' {"44'/-1'/0'/0/0"}, // no negatives {"44'/0'/0'/-1/0"}, // no negatives + {"a'/0'/0'/-1/0"}, // valid values + {"0/X/0'/-1/0"}, // valid values + {"44'/0'/X/-1/0"}, // valid values + {"44'/0'/0'/%/0"}, // valid values + {"44'/0'/0'/0/%"}, // valid values } for i, c := range badCases { @@ -80,14 +105,39 @@ func ExampleSomeBIP32TestVecs() { fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") fmt.Println() // cosmos - priv, _ := DerivePrivateKeyForPath(master, ch, FullFundraiserPath) - fmt.Println(hex.EncodeToString(priv[:])) + priv, err := DerivePrivateKeyForPath(master, ch, FullFundraiserPath) + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } // bitcoin - priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") - fmt.Println(hex.EncodeToString(priv[:])) + priv, err = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } // ether - priv, _ = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") - fmt.Println(hex.EncodeToString(priv[:])) + priv, err = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + // INVALID + priv, err = DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + priv, err = DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } fmt.Println() fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") @@ -121,6 +171,8 @@ func ExampleSomeBIP32TestVecs() { // bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c // e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d // 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc + // INVALID + // INVALID // // keys generated via https://coinomi.com/recovery-phrase-tool.html // diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go index e202cd6d8..1a7b06d16 100644 --- a/crypto/keys/keybase.go +++ b/crypto/keys/keybase.go @@ -4,23 +4,23 @@ import ( "bufio" "fmt" "os" + "reflect" "strings" - "github.com/pkg/errors" - - "github.com/cosmos/go-bip39" + "errors" "github.com/cosmos/cosmos-sdk/crypto" "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/go-bip39" + tmcrypto "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/crypto/secp256k1" dbm "github.com/tendermint/tendermint/libs/db" - - "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" ) var _ Keybase = dbKeybase{} @@ -30,6 +30,7 @@ var _ Keybase = dbKeybase{} // Find a list of all supported languages in the BIP 39 spec (word lists). type Language int +//noinspection ALL const ( // English is the default language to create a mnemonic. // It is the only supported language by this package. @@ -54,7 +55,7 @@ const ( const ( // used for deriving seed from mnemonic - defaultBIP39Passphrase = "" + DefaultBIP39Passphrase = "" // bits of entropy to draw when creating a mnemonic defaultEntropySize = 256 @@ -83,6 +84,9 @@ func New(db dbm.DB) Keybase { } } +// NewInMemory creates a new keybase on top of in-memory storage instance. +func NewInMemory() Keybase { return dbKeybase{dbm.NewMemDB()} } + // CreateMnemonic generates a new key and persists it to storage, encrypted // using the provided password. // It returns the generated mnemonic and the key Info. @@ -109,41 +113,15 @@ func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string return } - seed := bip39.NewSeed(mnemonic, defaultBIP39Passphrase) + seed := bip39.NewSeed(mnemonic, DefaultBIP39Passphrase) info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) return } -// TEMPORARY METHOD UNTIL WE FIGURE OUT USER FACING HD DERIVATION API -func (kb dbKeybase) CreateKey(name, mnemonic, passwd string) (info Info, err error) { - words := strings.Split(mnemonic, " ") - if len(words) != 12 && len(words) != 24 { - err = fmt.Errorf("recovering only works with 12 word (fundraiser) or 24 word mnemonics, got: %v words", len(words)) - return - } - seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) - if err != nil { - return - } - info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) - return -} - -// CreateFundraiserKey converts a mnemonic to a private key and persists it, -// encrypted with the given password. -// TODO(ismail) -func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) { - words := strings.Split(mnemonic, " ") - if len(words) != 12 { - err = fmt.Errorf("recovering only works with 12 word (fundraiser), got: %v words", len(words)) - return - } - seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) - if err != nil { - return - } - info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) - return +// CreateAccount converts a mnemonic to a private key and persists it, encrypted with the given password. +func (kb dbKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) { + hdPath := hd.NewFundraiserParams(account, index) + return kb.Derive(name, mnemonic, bip39Passwd, encryptPasswd, *hdPath) } func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params) (info Info, err error) { @@ -151,29 +129,32 @@ func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string if err != nil { return } - info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) + info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) return } // CreateLedger creates a new locally-stored reference to a Ledger keypair // It returns the created key info and an error if the Ledger could not be queried -func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (Info, error) { +func (kb dbKeybase) CreateLedger(name string, algo SigningAlgo, account uint32, index uint32) (Info, error) { if algo != Secp256k1 { return nil, ErrUnsupportedSigningAlgo } - priv, err := crypto.NewPrivKeyLedgerSecp256k1(path) + + hdPath := hd.NewFundraiserParams(account, index) + priv, err := crypto.NewPrivKeyLedgerSecp256k1(*hdPath) if err != nil { return nil, err } pub := priv.PubKey() - return kb.writeLedgerKey(pub, path, name), nil + + return kb.writeLedgerKey(name, pub, *hdPath), nil } // CreateOffline creates a new reference to an offline keypair // It returns the created key info func (kb dbKeybase) CreateOffline(name string, pub tmcrypto.PubKey) (Info, error) { - return kb.writeOfflineKey(pub, name), nil + return kb.writeOfflineKey(name, pub), nil } func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info Info, err error) { @@ -187,10 +168,10 @@ func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath str // if we have a password, use it to encrypt the private key and store it // else store the public key only if passwd != "" { - info = kb.writeLocalKey(secp256k1.PrivKeySecp256k1(derivedPriv), name, passwd) + info = kb.writeLocalKey(name, secp256k1.PrivKeySecp256k1(derivedPriv), passwd) } else { pubk := secp256k1.PrivKeySecp256k1(derivedPriv).PubKey() - info = kb.writeOfflineKey(pubk, name) + info = kb.writeOfflineKey(name, pubk) } return } @@ -362,7 +343,7 @@ func (kb dbKeybase) ImportPubKey(name string, armor string) (err error) { if err != nil { return } - kb.writeOfflineKey(pubKey, name) + kb.writeOfflineKey(name, pubKey) return } @@ -410,10 +391,10 @@ func (kb dbKeybase) Update(name, oldpass string, getNewpass func() (string, erro if err != nil { return err } - kb.writeLocalKey(key, name, newpass) + kb.writeLocalKey(name, key, newpass) return nil default: - return fmt.Errorf("locally stored key required") + return fmt.Errorf("locally stored key required. Received: %v", reflect.TypeOf(info).String()) } } @@ -422,29 +403,29 @@ func (kb dbKeybase) CloseDB() { kb.db.Close() } -func (kb dbKeybase) writeLocalKey(priv tmcrypto.PrivKey, name, passphrase string) Info { +func (kb dbKeybase) writeLocalKey(name string, priv tmcrypto.PrivKey, passphrase string) Info { // encrypt private key using passphrase privArmor := mintkey.EncryptArmorPrivKey(priv, passphrase) // make Info pub := priv.PubKey() info := newLocalInfo(name, pub, privArmor) - kb.writeInfo(info, name) + kb.writeInfo(name, info) return info } -func (kb dbKeybase) writeLedgerKey(pub tmcrypto.PubKey, path crypto.DerivationPath, name string) Info { +func (kb dbKeybase) writeLedgerKey(name string, pub tmcrypto.PubKey, path hd.BIP44Params) Info { info := newLedgerInfo(name, pub, path) - kb.writeInfo(info, name) + kb.writeInfo(name, info) return info } -func (kb dbKeybase) writeOfflineKey(pub tmcrypto.PubKey, name string) Info { +func (kb dbKeybase) writeOfflineKey(name string, pub tmcrypto.PubKey) Info { info := newOfflineInfo(name, pub) - kb.writeInfo(info, name) + kb.writeInfo(name, info) return info } -func (kb dbKeybase) writeInfo(info Info, name string) { +func (kb dbKeybase) writeInfo(name string, info Info) { // write the info by key key := infoKey(name) kb.db.SetSync(key, writeInfo(info)) diff --git a/crypto/keys/keybase_test.go b/crypto/keys/keybase_test.go index bc7783b67..f5c214a37 100644 --- a/crypto/keys/keybase_test.go +++ b/crypto/keys/keybase_test.go @@ -2,6 +2,7 @@ package keys import ( "fmt" + "io/ioutil" "testing" "github.com/stretchr/testify/assert" @@ -9,26 +10,84 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" - + "github.com/cosmos/cosmos-sdk/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" - dbm "github.com/tendermint/tendermint/libs/db" - - "github.com/cosmos/cosmos-sdk/types" ) func init() { mintkey.BcryptSecurityParameter = 1 } +func TestKeybaseOpenClose(t *testing.T) { + dir, err := ioutil.TempDir("", "TestKeybaseOpenClose") + assert.Nil(t, err) + + kb := New(dbm.NewDB("TestKeybaseOpenClose", dbm.LevelDBBackend, dir)) + kb.CloseDB() + + // The DB has been closed. At the moment, the expected behaviour is to panic + assert.Panics(t, func() { + _, _ = kb.CreateAccount( + "some_account", + "key pair crucial catch public canyon evil outer stage ten gym tornado", + "", "", 0, 1) + }) +} + +func TestLanguage(t *testing.T) { + kb := New(dbm.NewMemDB()) + _, _, err := kb.CreateMnemonic("something", Japanese, "no_pass", Secp256k1) + assert.Error(t, err) + assert.Equal(t, "unsupported language: only english is supported", err.Error()) +} + +func TestCreateAccountInvalidMnemonic(t *testing.T) { + kb := New(dbm.NewMemDB()) + _, err := kb.CreateAccount( + "some_account", + "malarkey pair crucial catch public canyon evil outer stage ten gym tornado", + "", "", 0, 1) + assert.Error(t, err) + assert.Equal(t, "Invalid mnemonic", err.Error()) +} + +func TestCreateLedgerUnsupportedAlgo(t *testing.T) { + kb := New(dbm.NewMemDB()) + _, err := kb.CreateLedger("some_account", Ed25519, 0, 1) + assert.Error(t, err) + assert.Equal(t, "unsupported signing algo: only secp256k1 is supported", err.Error()) +} + +func TestCreateLedger(t *testing.T) { + kb := New(dbm.NewMemDB()) + + // test_cover and test_unit will result in different answers + // test_cover does not compile some dependencies so ledger is disabled + // test_unit may add a ledger mock + // both cases are acceptable + ledger, err := kb.CreateLedger("some_account", Secp256k1, 0, 1) + + if err != nil { + assert.Error(t, err) + assert.Equal(t, "ledger nano S: support for ledger devices is not available in this executable", err.Error()) + assert.Nil(t, ledger) + } else { + // The mock is available, check that the address is correct + pubKey := ledger.GetPubKey() + addr, err := sdk.Bech32ifyAccPub(pubKey) + assert.NoError(t, err) + assert.Equal(t, "cosmospub1addwnpepqfsdqjr68h7wjg5wacksmqaypasnra232fkgu5sxdlnlu8j22ztxvlqvd65", addr) + } +} + // TestKeyManagement makes sure we can manipulate these keys well func TestKeyManagement(t *testing.T) { // make the storage with reasonable defaults db := dbm.NewMemDB() - cstore := New( - db, - ) + cstore := New(db) algo := Secp256k1 n1, n2, n3 := "personal", "business", "other" @@ -112,9 +171,7 @@ func TestKeyManagement(t *testing.T) { // TestSignVerify does some detailed checks on how we sign and validate // signatures func TestSignVerify(t *testing.T) { - cstore := New( - dbm.NewMemDB(), - ) + cstore := New(dbm.NewMemDB()) algo := Secp256k1 n1, n2, n3 := "some dude", "a dudette", "dude-ish" @@ -344,7 +401,7 @@ func TestSeedPhrase(t *testing.T) { // let us re-create it from the mnemonic-phrase params := *hd.NewFundraiserParams(0, 0) - newInfo, err := cstore.Derive(n2, mnemonic, defaultBIP39Passphrase, p2, params) + newInfo, err := cstore.Derive(n2, mnemonic, DefaultBIP39Passphrase, p2, params) require.NoError(t, err) require.Equal(t, n2, newInfo.GetName()) require.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address()) diff --git a/crypto/keys/lazy_keybase.go b/crypto/keys/lazy_keybase.go new file mode 100644 index 000000000..c26083972 --- /dev/null +++ b/crypto/keys/lazy_keybase.go @@ -0,0 +1,166 @@ +package keys + +import ( + "github.com/tendermint/tendermint/crypto" + dbm "github.com/tendermint/tendermint/libs/db" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/types" +) + +var _ Keybase = lazyKeybase{} + +type lazyKeybase struct { + name string + dir string +} + +func NewLazyKeybase(name, dir string) Keybase { + return lazyKeybase{name: name, dir: dir} +} + +func (lkb lazyKeybase) List() ([]Info, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).List() +} + +func (lkb lazyKeybase) Get(name string) (Info, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).Get(name) +} + +func (lkb lazyKeybase) GetByAddress(address types.AccAddress) (Info, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).GetByAddress(address) +} + +func (lkb lazyKeybase) Delete(name, passphrase string, skipPass bool) error { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + return New(db).Delete(name, passphrase, skipPass) +} + +func (lkb lazyKeybase) Sign(name, passphrase string, msg []byte) ([]byte, crypto.PubKey, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, nil, err + } + defer db.Close() + return New(db).Sign(name, passphrase, msg) +} + +func (lkb lazyKeybase) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, "", err + } + defer db.Close() + return New(db).CreateMnemonic(name, language, passwd, algo) +} + +func (lkb lazyKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd, account, index) +} + +func (lkb lazyKeybase) Derive(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).Derive(name, mnemonic, bip39Passwd, encryptPasswd, params) +} + +func (lkb lazyKeybase) CreateLedger(name string, algo SigningAlgo, account uint32, index uint32) (info Info, err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).CreateLedger(name, algo, account, index) +} + +func (lkb lazyKeybase) CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).CreateOffline(name, pubkey) +} + +func (lkb lazyKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + return New(db).Update(name, oldpass, getNewpass) +} + +func (lkb lazyKeybase) Import(name string, armor string) (err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + return New(db).Import(name, armor) +} + +func (lkb lazyKeybase) ImportPubKey(name string, armor string) (err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + return New(db).ImportPubKey(name, armor) +} + +func (lkb lazyKeybase) Export(name string) (armor string, err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + return New(db).Export(name) +} + +func (lkb lazyKeybase) ExportPubKey(name string) (armor string, err error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + return New(db).ExportPubKey(name) +} + +func (lkb lazyKeybase) ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error) { + db, err := dbm.NewGoLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + return New(db).ExportPrivateKeyObject(name, passphrase) +} + +func (lkb lazyKeybase) CloseDB() {} diff --git a/crypto/keys/types.go b/crypto/keys/types.go index 14d050961..52ef88b4b 100644 --- a/crypto/keys/types.go +++ b/crypto/keys/types.go @@ -3,8 +3,6 @@ package keys import ( "github.com/tendermint/tendermint/crypto" - ccrypto "github.com/cosmos/cosmos-sdk/crypto" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/types" ) @@ -23,20 +21,20 @@ type Keybase interface { // CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic // key from that. CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error) - // CreateKey takes a mnemonic and derives, a password. This method is temporary - CreateKey(name, mnemonic, passwd string) (info Info, err error) - // CreateFundraiserKey takes a mnemonic and derives, a password - CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) - // Compute a BIP39 seed from th mnemonic and bip39Passwd. + + // CreateAccount creates an account based using the BIP44 path (44'/118'/{account}'/0/{index} + CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) + + // Derive computes a BIP39 seed from th mnemonic and bip39Passwd. // Derive private key from the seed using the BIP44 params. // Encrypt the key to disk using encryptPasswd. // See https://github.com/cosmos/cosmos-sdk/issues/2095 - Derive(name, mnemonic, bip39Passwd, - encryptPasswd string, params hd.BIP44Params) (Info, error) - // Create, store, and return a new Ledger key reference - CreateLedger(name string, path ccrypto.DerivationPath, algo SigningAlgo) (info Info, err error) + Derive(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) - // Create, store, and return a new offline key reference + // CreateLedger creates, stores, and returns a new Ledger key reference + CreateLedger(name string, algo SigningAlgo, account uint32, index uint32) (info Info, err error) + + // CreateOffline creates, stores, and returns a new offline key reference CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error) // The following operations will *only* work on locally-stored keys @@ -46,10 +44,10 @@ type Keybase interface { Export(name string) (armor string, err error) ExportPubKey(name string) (armor string, err error) - // *only* works on locally-stored keys. Temporary method until we redo the exporting API + // ExportPrivateKeyObject *only* works on locally-stored keys. Temporary method until we redo the exporting API ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error) - // Close closes the database. + // CloseDB closes the database. CloseDB() } @@ -123,12 +121,12 @@ func (i localInfo) GetAddress() types.AccAddress { // ledgerInfo is the public information about a Ledger key type ledgerInfo struct { - Name string `json:"name"` - PubKey crypto.PubKey `json:"pubkey"` - Path ccrypto.DerivationPath `json:"path"` + Name string `json:"name"` + PubKey crypto.PubKey `json:"pubkey"` + Path hd.BIP44Params `json:"path"` } -func newLedgerInfo(name string, pub crypto.PubKey, path ccrypto.DerivationPath) Info { +func newLedgerInfo(name string, pub crypto.PubKey, path hd.BIP44Params) Info { return &ledgerInfo{ Name: name, PubKey: pub, diff --git a/crypto/ledger_mock.go b/crypto/ledger_mock.go new file mode 100644 index 000000000..df7cf6d4e --- /dev/null +++ b/crypto/ledger_mock.go @@ -0,0 +1,79 @@ +// +build ledger,test_ledger_mock + +package crypto + +import ( + "github.com/btcsuite/btcd/btcec" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/tests" + "github.com/cosmos/go-bip39" + "github.com/pkg/errors" + secp256k1 "github.com/tendermint/btcd/btcec" + "github.com/tendermint/tendermint/crypto" +) + +// If ledger support (build tag) has been enabled, which implies a CGO dependency, +// set the discoverLedger function which is responsible for loading the Ledger +// device at runtime or returning an error. +func init() { + discoverLedger = func() (LedgerSECP256K1, error) { + return LedgerSECP256K1Mock{}, nil + } +} + +type LedgerSECP256K1Mock struct { +} + +func (mock LedgerSECP256K1Mock) Close() error { + return nil +} + +func (mock LedgerSECP256K1Mock) GetPublicKeySECP256K1(derivationPath []uint32) ([]byte, error) { + if derivationPath[0] != 44 { + return nil, errors.New("Invalid derivation path") + } + if derivationPath[1] != 118 { + return nil, errors.New("Invalid derivation path") + } + + seed, err := bip39.NewSeedWithErrorChecking(tests.TestMnemonic, "") + if err != nil { + return nil, err + } + + path := hd.NewParams(derivationPath[0], derivationPath[1], derivationPath[2], derivationPath[3] != 0, derivationPath[4]) + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, path.String()) + if err != nil { + return nil, err + } + + _, pubkeyObject := secp256k1.PrivKeyFromBytes(secp256k1.S256(), derivedPriv[:]) + + return pubkeyObject.SerializeUncompressed(), nil +} + +func (mock LedgerSECP256K1Mock) SignSECP256K1(derivationPath []uint32, message []byte) ([]byte, error) { + path := hd.NewParams(derivationPath[0], derivationPath[1], derivationPath[2], derivationPath[3] != 0, derivationPath[4]) + seed, err := bip39.NewSeedWithErrorChecking(tests.TestMnemonic, "") + if err != nil { + return nil, err + } + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, path.String()) + if err != nil { + return nil, err + } + + priv, _ := secp256k1.PrivKeyFromBytes(secp256k1.S256(), derivedPriv[:]) + + sig, err := priv.Sign(crypto.Sha256(message)) + if err != nil { + return nil, err + } + + // Need to return DER as the ledger does + sig2 := btcec.Signature{R: sig.R, S: sig.S} + return sig2.Serialize(), nil +} diff --git a/crypto/ledger_notavail.go b/crypto/ledger_notavail.go new file mode 100644 index 000000000..8ad672720 --- /dev/null +++ b/crypto/ledger_notavail.go @@ -0,0 +1,17 @@ +// +build !cgo !ledger +// test_ledger_mock + +package crypto + +import ( + "github.com/pkg/errors" +) + +// If ledger support (build tag) has been enabled, which implies a CGO dependency, +// set the discoverLedger function which is responsible for loading the Ledger +// device at runtime or returning an error. +func init() { + discoverLedger = func() (LedgerSECP256K1, error) { + return nil, errors.New("support for ledger devices is not available in this executable") + } +} diff --git a/crypto/ledger.go b/crypto/ledger_real.go similarity index 81% rename from crypto/ledger.go rename to crypto/ledger_real.go index 36b5646b8..7aa4f8a84 100644 --- a/crypto/ledger.go +++ b/crypto/ledger_real.go @@ -1,10 +1,8 @@ -// +build cgo,ledger +// +build cgo,ledger,!test_ledger_mock package crypto -import ( - ledger "github.com/zondax/ledger-cosmos-go" -) +import ledger "github.com/zondax/ledger-cosmos-go" // If ledger support (build tag) has been enabled, which implies a CGO dependency, // set the discoverLedger function which is responsible for loading the Ledger diff --git a/crypto/ledger_secp256k1.go b/crypto/ledger_secp256k1.go index ff05d31ba..c19c82fd8 100644 --- a/crypto/ledger_secp256k1.go +++ b/crypto/ledger_secp256k1.go @@ -2,12 +2,15 @@ package crypto import ( "fmt" + "os" + "github.com/btcsuite/btcd/btcec" "github.com/pkg/errors" - - secp256k1 "github.com/btcsuite/btcd/btcec" + tmbtcec "github.com/tendermint/btcd/btcec" tmcrypto "github.com/tendermint/tendermint/crypto" tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" ) var ( @@ -22,12 +25,10 @@ type ( // dependencies when Ledger support is potentially not enabled. discoverLedgerFn func() (LedgerSECP256K1, error) - // DerivationPath represents a Ledger derivation path. - DerivationPath []uint32 - // LedgerSECP256K1 reflects an interface a Ledger API must implement for // the SECP256K1 scheme. LedgerSECP256K1 interface { + Close() error GetPublicKeySECP256K1([]uint32) ([]byte, error) SignSECP256K1([]uint32, []byte) ([]byte, error) } @@ -39,35 +40,25 @@ type ( // go-amino so we can view the address later, even without having the // ledger attached. CachedPubKey tmcrypto.PubKey - Path DerivationPath - ledger LedgerSECP256K1 + Path hd.BIP44Params } ) // NewPrivKeyLedgerSecp256k1 will generate a new key and store the public key // for later use. -// -// CONTRACT: The ledger device, ledgerDevice, must be loaded and set prior to -// any creation of a PrivKeyLedgerSecp256k1. -func NewPrivKeyLedgerSecp256k1(path DerivationPath) (tmcrypto.PrivKey, error) { - if discoverLedger == nil { - return nil, errors.New("no Ledger discovery function defined") - } - - device, err := discoverLedger() +func NewPrivKeyLedgerSecp256k1(path hd.BIP44Params) (tmcrypto.PrivKey, error) { + device, err := getLedgerDevice() if err != nil { - return nil, errors.Wrap(err, "failed to create PrivKeyLedgerSecp256k1") + return nil, err } + defer warnIfErrors(device.Close) - pkl := &PrivKeyLedgerSecp256k1{Path: path, ledger: device} - - pubKey, err := pkl.getPubKey() + pubKey, err := getPubKey(device, path) if err != nil { return nil, err } - pkl.CachedPubKey = pubKey - return pkl, err + return PrivKeyLedgerSecp256k1{pubKey, path}, nil } // PubKey returns the cached public key. @@ -75,21 +66,27 @@ func (pkl PrivKeyLedgerSecp256k1) PubKey() tmcrypto.PubKey { return pkl.CachedPubKey } +// Sign returns a secp256k1 signature for the corresponding message +func (pkl PrivKeyLedgerSecp256k1) Sign(message []byte) ([]byte, error) { + device, err := getLedgerDevice() + if err != nil { + return nil, err + } + defer warnIfErrors(device.Close) + + return sign(device, pkl, message) +} + // ValidateKey allows us to verify the sanity of a public key after loading it // from disk. func (pkl PrivKeyLedgerSecp256k1) ValidateKey() error { - // getPubKey will return an error if the ledger is not - pub, err := pkl.getPubKey() + device, err := getLedgerDevice() if err != nil { return err } + defer warnIfErrors(device.Close) - // verify this matches cached address - if !pub.Equals(pkl.CachedPubKey) { - return fmt.Errorf("cached key does not match retrieved key") - } - - return nil + return validateKey(device, pkl) } // AssertIsPrivKeyInner implements the PrivKey interface. It performs a no-op. @@ -104,11 +101,54 @@ func (pkl PrivKeyLedgerSecp256k1) Bytes() []byte { // Equals implements the PrivKey interface. It makes sure two private keys // refer to the same public key. func (pkl PrivKeyLedgerSecp256k1) Equals(other tmcrypto.PrivKey) bool { - if ledger, ok := other.(*PrivKeyLedgerSecp256k1); ok { - return pkl.CachedPubKey.Equals(ledger.CachedPubKey) + if otherKey, ok := other.(PrivKeyLedgerSecp256k1); ok { + return pkl.CachedPubKey.Equals(otherKey.CachedPubKey) + } + return false +} + +// warnIfErrors wraps a function and writes a warning to stderr. This is required +// to avoid ignoring errors when defer is used. Using defer may result in linter warnings. +func warnIfErrors(f func() error) { + if err := f(); err != nil { + _, _ = fmt.Fprint(os.Stderr, "received error when closing ledger connection", err) + } +} + +func convertDERtoBER(signatureDER []byte) ([]byte, error) { + sigDER, err := btcec.ParseDERSignature(signatureDER[:], btcec.S256()) + if err != nil { + return nil, err + } + sigBER := tmbtcec.Signature{R: sigDER.R, S: sigDER.S} + return sigBER.Serialize(), nil +} + +func getLedgerDevice() (LedgerSECP256K1, error) { + if discoverLedger == nil { + return nil, errors.New("no Ledger discovery function defined") } - return false + device, err := discoverLedger() + if err != nil { + return nil, errors.Wrap(err, "ledger nano S") + } + + return device, nil +} + +func validateKey(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1) error { + pub, err := getPubKey(device, pkl.Path) + if err != nil { + return err + } + + // verify this matches cached address + if !pub.Equals(pkl.CachedPubKey) { + return fmt.Errorf("cached key does not match retrieved key") + } + + return nil } // Sign calls the ledger and stores the PubKey for future use. @@ -116,45 +156,37 @@ func (pkl PrivKeyLedgerSecp256k1) Equals(other tmcrypto.PrivKey) bool { // Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, returning // an error, so this should only trigger if the private key is held in memory // for a while before use. -func (pkl PrivKeyLedgerSecp256k1) Sign(msg []byte) ([]byte, error) { - sig, err := pkl.signLedgerSecp256k1(msg) +func sign(device LedgerSECP256K1, pkl PrivKeyLedgerSecp256k1, msg []byte) ([]byte, error) { + err := validateKey(device, pkl) if err != nil { return nil, err } - return sig, nil + sig, err := device.SignSECP256K1(pkl.Path.DerivationPath(), msg) + if err != nil { + return nil, err + } + + return convertDERtoBER(sig) } // getPubKey reads the pubkey the ledger itself // since this involves IO, it may return an error, which is not exposed // in the PubKey interface, so this function allows better error handling -func (pkl PrivKeyLedgerSecp256k1) getPubKey() (key tmcrypto.PubKey, err error) { - key, err = pkl.pubkeyLedgerSecp256k1() +func getPubKey(device LedgerSECP256K1, path hd.BIP44Params) (tmcrypto.PubKey, error) { + publicKey, err := device.GetPublicKeySECP256K1(path.DerivationPath()) if err != nil { - return key, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err) + return nil, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err) } - return key, err -} - -func (pkl PrivKeyLedgerSecp256k1) signLedgerSecp256k1(msg []byte) ([]byte, error) { - return pkl.ledger.SignSECP256K1(pkl.Path, msg) -} - -func (pkl PrivKeyLedgerSecp256k1) pubkeyLedgerSecp256k1() (pub tmcrypto.PubKey, err error) { - key, err := pkl.ledger.GetPublicKeySECP256K1(pkl.Path) - if err != nil { - return nil, fmt.Errorf("error fetching public key: %v", err) - } - - var pk tmsecp256k1.PubKeySecp256k1 - // re-serialize in the 33-byte compressed format - cmp, err := secp256k1.ParsePubKey(key[:], secp256k1.S256()) + cmp, err := btcec.ParsePubKey(publicKey[:], btcec.S256()) if err != nil { return nil, fmt.Errorf("error parsing public key: %v", err) } - copy(pk[:], cmp.SerializeCompressed()) - return pk, nil + var compressedPublicKey tmsecp256k1.PubKeySecp256k1 + copy(compressedPublicKey[:], cmp.SerializeCompressed()) + + return compressedPublicKey, nil } diff --git a/crypto/ledger_test.go b/crypto/ledger_test.go index 1aae158ef..6418a5732 100644 --- a/crypto/ledger_test.go +++ b/crypto/ledger_test.go @@ -2,22 +2,127 @@ package crypto import ( "fmt" - "os" "testing" - "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/tests" + + tmcrypto "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/encoding/amino" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" ) -var ledgerEnabledEnv = "TEST_WITH_LEDGER" +func TestLedgerErrorHandling(t *testing.T) { + // first, try to generate a key, must return an error + // (no panic) + path := *hd.NewParams(44, 555, 0, false, 0) + _, err := NewPrivKeyLedgerSecp256k1(path) + require.Error(t, err) +} + +func TestPublicKey(t *testing.T) { + path := *hd.NewFundraiserParams(0, 0) + priv, err := NewPrivKeyLedgerSecp256k1(path) + require.Nil(t, err, "%s", err) + require.NotNil(t, priv) + + pubKeyAddr, err := sdk.Bech32ifyAccPub(priv.PubKey()) + require.NoError(t, err) + require.Equal(t, "cosmospub1addwnpepqd87l8xhcnrrtzxnkql7k55ph8fr9jarf4hn6udwukfprlalu8lgw0urza0", + pubKeyAddr, "Is your device using test mnemonic: %s ?", tests.TestMnemonic) + + require.Equal(t, "5075624b6579536563703235366b317b303334464546394344374334433633353838443342303"+ + "3464542353238314239443233324342413334443646334437314145453539323131464642464531464538377d", + fmt.Sprintf("%x", priv.PubKey())) +} + +func TestPublicKeyHDPath(t *testing.T) { + expectedAnswers := []string{ + "cosmospub1addwnpepqd87l8xhcnrrtzxnkql7k55ph8fr9jarf4hn6udwukfprlalu8lgw0urza0", + "cosmospub1addwnpepqfsdqjr68h7wjg5wacksmqaypasnra232fkgu5sxdlnlu8j22ztxvlqvd65", + "cosmospub1addwnpepqw3xwqun6q43vtgw6p4qspq7srvxhcmvq4jrx5j5ma6xy3r7k6dtxmrkh3d", + "cosmospub1addwnpepqvez9lrp09g8w7gkv42y4yr5p6826cu28ydrhrujv862yf4njmqyyjr4pjs", + "cosmospub1addwnpepq06hw3enfrtmq8n67teytcmtnrgcr0yntmyt25kdukfjkerdc7lqg32rcz7", + "cosmospub1addwnpepqg3trf2gd0s2940nckrxherwqhgmm6xd5h4pcnrh4x7y35h6yafmcpk5qns", + "cosmospub1addwnpepqdm6rjpx6wsref8wjn7ym6ntejet430j4szpngfgc20caz83lu545vuv8hp", + "cosmospub1addwnpepqvdhtjzy2wf44dm03jxsketxc07vzqwvt3vawqqtljgsr9s7jvydjmt66ew", + "cosmospub1addwnpepqwystfpyxwcava7v3t7ndps5xzu6s553wxcxzmmnxevlzvwrlqpzz695nw9", + "cosmospub1addwnpepqw970u6gjqkccg9u3rfj99857wupj2z9fqfzy2w7e5dd7xn7kzzgkgqch0r", + } + + const numIters = 10 + + privKeys := make([]tmcrypto.PrivKey, numIters) + + // Check with device + for i := uint32(0); i < 10; i++ { + path := *hd.NewFundraiserParams(0, i) + fmt.Printf("Checking keys at %v\n", path) + + priv, err := NewPrivKeyLedgerSecp256k1(path) + require.Nil(t, err, "%s", err) + require.NotNil(t, priv) + + // Check other methods + require.NoError(t, priv.(PrivKeyLedgerSecp256k1).ValidateKey()) + tmp := priv.(PrivKeyLedgerSecp256k1) + (&tmp).AssertIsPrivKeyInner() + + pubKeyAddr, err := sdk.Bech32ifyAccPub(priv.PubKey()) + require.NoError(t, err) + require.Equal(t, + expectedAnswers[i], pubKeyAddr, + "Is your device using test mnemonic: %s ?", tests.TestMnemonic) + + // Store and restore + serializedPk := priv.Bytes() + require.NotNil(t, serializedPk) + require.Equal(t, 44, len(serializedPk)) + + privKeys[i] = priv + } + + // Now check equality + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + require.Equal(t, i == j, privKeys[i].Equals(privKeys[j])) + require.Equal(t, i == j, privKeys[j].Equals(privKeys[i])) + } + } +} + +func getFakeTx(accountNumber uint32) []byte { + tmp := fmt.Sprintf( + `{"account_number":"%d","chain_id":"1234","fee":{"amount":[{"amount":"150","denom":"atom"}],"gas":"5000"},"memo":"memo","msgs":[[""]],"sequence":"6"}`, + accountNumber) + + return []byte(tmp) +} + +func TestSignaturesHD(t *testing.T) { + for account := uint32(0); account < 100; account += 30 { + msg := getFakeTx(account) + + path := *hd.NewFundraiserParams(account, account/5) + fmt.Printf("Checking signature at %v --- PLEASE REVIEW AND ACCEPT IN THE DEVICE\n", path) + + priv, err := NewPrivKeyLedgerSecp256k1(path) + require.Nil(t, err, "%s", err) + + pub := priv.PubKey() + sig, err := priv.Sign(msg) + require.Nil(t, err) + + valid := pub.VerifyBytes(msg, sig) + require.True(t, valid, "Is your device using test mnemonic: %s ?", tests.TestMnemonic) + } +} func TestRealLedgerSecp256k1(t *testing.T) { - if os.Getenv(ledgerEnabledEnv) == "" { - t.Skip(fmt.Sprintf("Set '%s' to run code on a real ledger", ledgerEnabledEnv)) - } - msg := []byte("{\"account_number\":\"3\",\"chain_id\":\"1234\",\"fee\":{\"amount\":[{\"amount\":\"150\",\"denom\":\"atom\"}],\"gas\":\"5000\"},\"memo\":\"memo\",\"msgs\":[[\"%s\"]],\"sequence\":\"6\"}") - path := DerivationPath{44, 60, 0, 0, 0} - + msg := getFakeTx(50) + path := *hd.NewFundraiserParams(0, 0) priv, err := NewPrivKeyLedgerSecp256k1(path) require.Nil(t, err, "%s", err) @@ -48,17 +153,3 @@ func TestRealLedgerSecp256k1(t *testing.T) { require.NoError(t, err) require.Equal(t, pub, bpub) } - -// TestRealLedgerErrorHandling calls. These tests assume -// the ledger is not plugged in.... -func TestRealLedgerErrorHandling(t *testing.T) { - if os.Getenv(ledgerEnabledEnv) != "" { - t.Skip(fmt.Sprintf("Unset '%s' to run code as if without a real Ledger", ledgerEnabledEnv)) - } - - // first, try to generate a key, must return an error - // (no panic) - path := DerivationPath{44, 60, 0, 0, 0} - _, err := NewPrivKeyLedgerSecp256k1(path) - require.Error(t, err) -} diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a007b9fe9..669c2467e 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -21,7 +21,7 @@ module.exports = { }, nav: [ { text: "Back to Cosmos", link: "https://cosmos.network" }, - { text: "RPC", link: "../rpc/" } + { text: "RPC", link: "https://cosmos.network/rpc/" } ], sidebar: [ { @@ -44,9 +44,10 @@ module.exports = { "/gaia/validators/overview", "/gaia/validators/security", "/gaia/validators/validator-faq", - "/gaia/deploy-testnet", + "/gaia/delegator-guide-cli", "/gaia/ledger", - "/gaia/gaiacli" + "/gaia/gaiacli", + "/gaia/deploy-testnet" ] }, { diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index c8968486a..8ae483531 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -4,9 +4,9 @@ - [ ] 2. Add commits/PRs that are desired for this release **that haven’t already been added to develop** - [ ] 3. Merge items in `PENDING.md` into the `CHANGELOG.md`. While doing this make sure that each entry contains links to issues/PRs for each item - [ ] 4. Summarize breaking API changes section under “Breaking Changes” section to the `CHANGELOG.md` to bring attention to any breaking API changes that affect RPC consumers. -- [ ] 5. Tag the commit `{ .Release.Name }-rcN` +- [ ] 5. Tag the commit `git tag -a { .Release.Name }-rcN -m 'Release { .Release.Name }'` - [ ] 6. Open a branch & PR to merge the pending release back into `develop`. If any changes are made to the release branch, they must also be made to the pending develop merge, and both must pass tests. - [ ] 7. Kick off 1 day of automated fuzz testing - [ ] 8. Release Lead assigns 2 people to perform [buddy testing script](/docs/RELEASE_TEST_SCRIPT.md) and update the relevant documentation - [ ] 9. If errors are found in either #6 or #7 go back to #2 (*NOTE*: be sure to increment the `rcN`) -- [ ] 10. After #6 and #7 have successfully completed then merge the release PR and push the final release tag +- [ ] 10. After #6 and #7 have successfully completed then merge the release PR and push the final annotated release tag (created with `git tag -a`) diff --git a/docs/_attic/sdk/core/examples/app1.go b/docs/_attic/sdk/core/examples/app1.go index 715a88bc7..8cf30f4bb 100644 --- a/docs/_attic/sdk/core/examples/app1.go +++ b/docs/_attic/sdk/core/examples/app1.go @@ -154,7 +154,7 @@ func handleFrom(store sdk.KVStore, from sdk.AccAddress, amt sdk.Coins) sdk.Resul senderCoins := acc.Coins.Minus(amt) // If any coin has negative amount, return insufficient coins error. - if !senderCoins.IsNotNegative() { + if senderCoins.IsAnyNegative() { return sdk.ErrInsufficientCoins("Insufficient coins in account").Result() } diff --git a/docs/clients/lite/getting_started.md b/docs/clients/lite/getting_started.md index faa3ea91b..47ee16f9e 100644 --- a/docs/clients/lite/getting_started.md +++ b/docs/clients/lite/getting_started.md @@ -19,17 +19,18 @@ gaiacli rest-server --chain-id=test \ --trust-node=false ``` -The server listens on HTTPS by default. You can set the SSL certificate to be used by the server with these additional flags: +The server listens on HTTP by default. You can enable the secure layer by adding the `--tls` flag. +By default a self-signed certificate will be generated and its fingerprint printed out. You can +configure the server to use a SSL certificate by passing the certificate and key files via the +`--ssl-certfile` and `--ssl-keyfile` flags: ```bash gaiacli rest-server --chain-id=test \ --laddr=tcp://localhost:1317 \ --node tcp://localhost:26657 \ --trust-node=false \ + --tls \ --ssl-certfile=mycert.pem --ssl-keyfile=mykey.key ``` -If no certificate/keyfile pair is supplied, a self-signed certificate will be generated and its fingerprint printed out. -Append `--insecure` to the command line if you want to disable the secure layer and listen on an insecure HTTP port. - For more information about the Gaia-Lite RPC, see the [swagger documentation](https://cosmos.network/rpc/) diff --git a/docs/clients/service-providers.md b/docs/clients/service-providers.md index 358031b96..68024d641 100644 --- a/docs/clients/service-providers.md +++ b/docs/clients/service-providers.md @@ -105,10 +105,19 @@ The recommended way to listen for incoming transaction is to periodically query ## Rest API -The Rest API documents all the available endpoints that you can use to interract with your full node. It can be found [here](https://cosmos.network/rpc/). +The Rest API documents all the available endpoints that you can use to interact +with your full node. It can be found [here](https://cosmos.network/rpc/). -The API is divided into ICS standards for each category of endpoints. For example, the [ICS20](https://cosmos.network/rpc/#/ICS20/) describes the API to interact with tokens. +The API is divided into ICS standards for each category of endpoints. For +example, the [ICS20](https://cosmos.network/rpc/#/ICS20/) describes the API to +interact with tokens. -To give more flexibility to implementers, we have included the ability to generate unsigned transactions, [sign](https://cosmos.network/rpc/#/ICS20/post_tx_sign) and [broadcast](https://cosmos.network/rpc/#/ICS20/post_tx_broadcast) them with different API endpoints. This allows service providers to use their own signing mechanism for instance. +To give more flexibility to implementers, we have included the ability to +generate unsigned transactions, [sign](https://cosmos.network/rpc/#/ICS20/post_tx_sign) +and [broadcast](https://cosmos.network/rpc/#/ICS20/post_tx_broadcast) them with +different API endpoints. This allows service providers to use their own signing +mechanism for instance. -In order to generate an unsigned transaction (example with [coin transfer](https://cosmos.network/rpc/#/ICS20/post_bank_accounts__address__transfers)), you need to use the flag `?generate_only`. +In order to generate an unsigned transaction (example with +[coin transfer](https://cosmos.network/rpc/#/ICS20/post_bank_accounts__address__transfers)), +you need to use the field `generate_only` in the body of `base_req`. diff --git a/docs/gaia/delegator-guide-cli.md b/docs/gaia/delegator-guide-cli.md new file mode 100644 index 000000000..91a0976ac --- /dev/null +++ b/docs/gaia/delegator-guide-cli.md @@ -0,0 +1,417 @@ +# Delegator Guide (CLI) + +This document contains all the necessary information for delegators to interact with the Cosmos Hub through the Command-Line Interface (CLI). + +It also contains instructions on how to manage accounts, restore accounts from the fundraiser and use a ledger nano device. + +## Table of contents + +- [Installing `gaiacli`](#installing-gaiacli) +- [Cosmos Accounts](#cosmos-accounts) + + [Restoring an account from the fundrasier](#restoring-an-account-from-the-fundraiser) + + [Creating an account](#creating-an-account) +- [Accessing the Cosmos Hub network](#accessing-the-cosmos-hub-network) + + [Running your own full-node](#running-your-own-full-node) + + [Connecting to a remote full-node](#connecting-to-a-remote-full-node) +- [Setting up `gaiacli`](#setting-up-gaiacli) +- [Querying the state](#querying-the-state) +- [Bonding Atoms and Withdrawing rewards](#bonding-atoms-and-withdrawing-rewards) +- [Participating in Governance](#participating-in-governance) +- [Signing transactions from an offline computer](#signing-transactions-from-an-offline-computer) + +## Installing `gaiacli` + +`gaiacli`: This is the command-line interface to interact with a `gaiad` full-node. + +::: warning +**Please check that you download the latest stable release of `gaiacli` that is available** +::: + +[**Download the binaries**] + +[**Install from source**](https://cosmos.network/docs/gaia/installation.html) + +## Cosmos Accounts + +At the core of every Cosmos account, there is a seed, which takes the form of a 12 or 24-words mnemonic. From this mnemonic, it is possible to create any number of Cosmos accounts, i.e. pairs of private key/public key. This is called an HD wallet (see [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) for more information on the HD wallet specification). + +``` + Account 0 Account 1 Account 2 + ++------------------+ +------------------+ +------------------+ +| | | | | | +| Address 0 | | Address 1 | | Address 2 | +| ^ | | ^ | | ^ | +| | | | | | | | | +| | | | | | | | | +| | | | | | | | | +| + | | + | | + | +| Public key 0 | | Public key 1 | | Public key 2 | +| ^ | | ^ | | ^ | +| | | | | | | | | +| | | | | | | | | +| | | | | | | | | +| + | | + | | + | +| Private key 0 | | Private key 1 | | Private key 2 | +| ^ | | ^ | | ^ | ++------------------+ +------------------+ +------------------+ + | | | + | | | + | | | + +--------------------------------------------------------------------+ + | + | + +---------+---------+ + | | + | Mnemonic (Seed) | + | | + +-------------------+ +``` + +The funds stored in an account are controlled by the private key. This private key is generated using a one-way function from the mnemonic. If you lose the private key, you can retrieve it using the mnemonic. However, if you lose the mnemonic, you will lose access to all the derived private keys. Likewise, if someone gains access to your mnemonic, they gain access to all the associated accounts. + +::: danger +**Do not lose or share your 12 words with anyone. To prevent theft or loss of funds, it is best to ensure that you keep multiple copies of your mnemonic, and store it in a safe, secure place and that only you know how to access. If someone is able to gain access to your mnemonic, they will be able to gain access to your private keys and control the accounts associated with them.** +::: + +The address is a public string with a human-readable prefix (e.g. `cosmos10snjt8dmpr5my0h76xj48ty80uzwhraqalu4eg`) that identifies your account. When someone wants to send you funds, they send it to your address. It is computationally infeasible to find the private key associated with a given address. + +### Restoring an account from the fundraiser + +::: tip +*NOTE: This section only concerns fundraiser participants* +::: + +If you participated in the fundraiser, you should be in possession of a 12-words mnemonic. Newly generated mnemonics use 24 words, but 12-word mnemonics are also compatible with all the Cosmos tools. + +#### On a ledger device + +At the core of a ledger device, there is a mnemonic used to generate accounts on multiple blockchains (including the Cosmos Hub). Usually, you will create a new mnemonic when you initialize your ledger device. However, it is possible to tell the ledger device to use a mnemonic provided by the user instead. Let us go ahead and see how you can input the mnemonic you obtained during the fundraiser as the seed of your ledger device. + +::: warning +*NOTE: To do this, **it is preferable to use a brand new ledger device.**. Indeed, there can be only one mnemonic per ledger device. If, however, you want to use a ledger that is already initialized with a seed, you can reset it by going in `Settings`>`Device`>`Reset All`. **Please note that this will wipe out the seed currently stored on the device. If you have not properly secured the associated mnemonic, you could lose your funds!!!*** +::: + +The following steps need to be performed on an un-initialized ledger device: + +1. Connect your ledger device to the computer via USB +2. Press both buttons +3. Do **NOT** choose the "Config as a new device" option. Instead, choose "Restore Configuration" +4. Choose a PIN +5. Choose the 12 words option +6. Input each of the words you got during the fundraiser, in the correct order. + +Your ledger is now correctly set up with your fundraiser mnemonic! Do not lose this mnemonic! If your ledger is compromised, you can always restore a new device again using the same mnemonic. + +Next, click [here](#using-a-ledger-device) to learn how to generate an account. + +#### On a computer + +::: warning +**NOTE: It is more secure to perform this action on an offline computer** +::: + +To restore an account using a fundraiser mnemonic and store the associated encrypted private key on a computer, use the following command: + +```bash +gaiacli keys add --recover +``` + +You will be prompted to input a passphrase that is used to encrypt the private key of account `0` on disk. Each time you want to send a transaction, this password will be required. If you lose the password, you can always recover the private key with the mnemonic. + +- `` is the name of the account. It is a reference to the account number used to derive the key pair from the mnemonic. You will use this name to identify your account when you want to send a transaction. +- You can add the optional `--account` flag to specify the path (`0`, `1`, `2`, ...) you want to use to generate your account. By default, account `0` is generated. + +### Creating an account + +To create an account, you just need to have `gaiacli` installed. Before creating it, you need to know where you intend to store and interract with your private keys. The best options are to store them in an offline dedicated computer or a ledger device. Storing them on your regular online computer involves more risk, since anyone who infiltrates your computer through the internet could exfiltrate your private keys and steal your funds. + +#### Using a ledger device + +::: warning +**Only use Ledger devices that you bought factory new or trust fully** +::: + +When you initialize your ledger, a 24-word mnemonic is generated and stored in the device. This mnemonic is compatible with Cosmos and Cosmos accounts can be derived from it. Therefore, all you have to do is make your ledger compatible with `gaiacli`. To do so, you need to go through the following steps: + +1. Download the Ledger Live app [here](https://www.ledger.com/pages/ledger-live) +2. Connect your ledger via USB and update to the latest firmware +3. Go to the ledger live app store, and download the "Cosmos" application (this can take a while) +4. Navigate to the Cosmos app on your ledger device + +Then, to create an account, use the following command: + +```bash +gaiacli keys add --ledger +``` + +- `` is the name of the account. It is a reference to the account number used to derive the key pair from the mnemonic. You will use this name to identify your account when you want to send a transaction. +- You can add the optional `--account` flag to specify the path (`0`, `1`, `2`, ...) you want to use to generate your account. By default, account `0` is generated. + +#### Using a computer + +::: warning +**NOTE: It is more secure to perform this action on an offline computer** +::: + +To generate an account, just use the following command: + +```bash +gaiacli keys add +``` + +The command will generate a 24-words mnemonic and save the private and public keys for account `0` at the same time. You will be prompted to input a passphrase that is used to encrypt the private key of account `0` on disk. Each time you want to send a transaction, this password will be required. If you lose the password, you can always recover the private key with the mnemonic. + +::: danger +**Do not lose or share your 12 words with anyone. To prevent theft or loss of funds, it is best to ensure that you keep multiple copies of your mnemonic, and store it in a safe, secure place and that only you know how to access. If someone is able to gain access to your mnemonic, they will be able to gain access to your private keys and control the accounts associated with them.** +::: + +::: warning +After you have secured your mnemonic (triple check!), you can delete bash history to ensure no one can retrieve it: + +```bash +history -c +rm ~/.bash_history +``` +::: + +- `` is the name of the account. It is a reference to the account number used to derive the key pair from the mnemonic. You will use this name to identify your account when you want to send a transaction. +- You can add the optional `--account` flag to specify the path (`0`, `1`, `2`, ...) you want to use to generate your account. By default, account `0` is generated. + + +You can generate more accounts from the same mnemonic using the following command: + +```bash +gaiacli keys add --recover --account 1 +``` + +This command will prompt you to input a passphrase as well as your mnemonic. Change the account number to generate a different account. + + +## Accessing the Cosmos Hub network + +In order to query the state and send transactions, you need a way to access the network. To do so, you can either run your own full-node, or connect to someone else's. + +::: danger +**NOTE: Do not share your mnemonic (12 or 24 words) with anyone. The only person who should ever need to know it is you. This is especially important if you are ever approached via email or direct message by someone requesting that you share your mnemonic for any kind of blockchain services or support. No one from Cosmos, the Tendermint team or the Interchain Foundation will ever send an email that asks for you to share any kind of account credentials or your mnemonic."**. +::: + +### Running your own full-node + +This is the most secure option, but comes with relatively high resource requirements. In order to run your own full-node, you need good bandwidth and at least 1TB of disk space. + +You will find the tutorial on how to install `gaiad` [here](https://cosmos.network/docs/gaia/installation.html), and the guide to run a full-node [here](https://cosmos.network/docs/gaia/join-testnet.html). + +### Connecting to a remote full-node + +If you do not want or cannot run your own node, you can connect to someone else's full-node. You should pick an operator you trust, because a malicious operator could return incorrect query results or censor your transactions. However, they will never be able to steal your funds, as your private keys are stored locally on your computer or ledger device. Possible options of full-node operators include validators, wallet providers or exchanges. + +In order to connect to the full-node, you will need an address of the following form: `https://77.87.106.33:26657` (*Note: This is a placeholder*). This address has to be communicated by the full-node operator you choose to trust. You will use this address in the [following section](#setting-up-gaiacli). + +## Setting up `gaiacli` + +::: warning +**Please check that you are always using the latest stable release of `gaiacli`** +::: + +`gaiacli` is the tool that enables you to interact with the node that runs on the Cosmos Hub network, whether you run it yourself or not (see [accessing the cosmos hub network](#accession-the-cosmos-hub-network)). Let us set it up properly. + +In order to set up `gaiacli`, use the following command: + +```bash +gaiacli config +``` + +It allows you to set a default value for each given flag. + +First, set up the address of the full-node you want to connect to: + +```bash +gaiacli config node : + +// query all delegations made from a delegator given their address (e.g. cosmos10snjt8dmpr5my0h76xj48ty80uzwhraqalu4eg) +gaiacli query delegations + +// query a specific delegation made from a delegator to a validator given their addresses +gaiacli query delegations + +// query the rewards of a delegator given a delegator address (e.g. cosmos10snjt8dmpr5my0h76xj48ty80uzwhraqalu4eg) +gaiacli query distr rewards + +// query all proposals currently open for depositing +gaiacli query proposals --status deposit_period + +// query all proposals currently open for voting +gaiacli query proposals --status voting_period + +// query a proposal given its proposalID +gaiacli query proposal +``` + +For more commands, just type: + +```bash +gaiacli query +``` + +For each command, you can use the `-h` or `--help` flag to get more information. + +## Bonding Atoms and Withdrawing rewards + +::: warning +**Before bonding Atoms, please read the [delegator faq](https://cosmos.network/resources/delegators) to understand the risk and responsabilities involved with delegating** +::: + +::: warning +**Note: These commands need to be run on an online computer. It is more secure to perform them commands using a ledger device. For the offline procedure, click [here](#signing-transactions-from-an-offline-computer).** +::: + +```bash +// Bond Atoms +// ex value for flags: =10000stake, =cosmosvaloper18thamkhnj9wz8pa4nhnp9rldprgant57pk2m8s, =0.001stake + +gaiacli tx staking --amount --validator --from --gas auto --gas-prices + +// Withdraw rewards + +gaiacli tx distr withdraw-rewards --from +``` + +::: tip +If you use a connected Ledger, you will be asked to confirm the transaction on the device before it is signed and broadcast to the network +::: + +To confirm that your transaction went through, you can use the following queries: + +```bash +// your balance should change after you bond Atoms or withdraw rewards +gaiacli query account + +// you should have delegations after you bond Atom +gaiacli query delegations + +// this returns your tx if it has been included +// use the tx hash that was displayed when you created the tx +gaiacli query tx + +``` + +Double check with a block explorer if you interact with the network through a trusted full-node. + +### A note on gas and fees + +Transactions on the Cosmos Hub network need to include a transaction fee in order to be processed. This fee pays for the gas required to run the transaction. The formula is the following: + +``` +fees = gas * gasPrices +``` + +The `gas` is dependent on the transaction. Different transaction require different amount of `gas`. The `gas` amount for a transaction is calculated as it is being processed, but there is a way to estimate it beforehand by using the `auto` value for the `gas` flag. Of course, this only gives an estimate. You can adjust this estimate with the flag `--gas-adjustment` (default `1.0`) if you want to be sure you provide enough `gas` for the transaction. + +The `gasPrice` is the price of each unit of `gas`. Each validator sets a `min-gas-price` value, and will only include transactions that have a `gasPrice` greater than their `min-gas-price`. + +The transaction `fees` are the product of `gas` and `gasPrice`. As a user, you have to input 2 out of 3. The higher the `gasPrice`, the higher the chance that your transaction will get included in a block. + +## Participating in governance + +### Primer on governance + +The Cosmos Hub has a built-in governance system that lets bonded Atom holders vote on proposals. There are three types of proposal: + +- `Text Proposals`: These are the most basic type of proposals. They can be used to get the opinion of the network on a given topic. +- `Parameter Proposals`: These are used to update the value of an existing parameter. +- `Software Upgrade Proposal`: These are used to propose an upgrade of the Hub's software. + +Any Atom holder can submit a proposal. In order for the proposal to be open for voting, it needs to come with a `deposit` that is greater than a parameter called `minDeposit`. The `deposit` need not be provided in its entirety by the submitter. If the initial proposer's `deposit` is not sufficient, the proposal enters the `deposit_period` status. Then, any Atom holder can increase the deposit by sending a `depositTx`. + +Once the `deposit` reaches `minDeposit`, the proposal enters the `voting_period`, which lasts 2 weeks. Any **bonded** Atom holder can then cast a vote on this proposal. The options are `Yes`, `No`, `NoWithVeto` and `Abstain`. The weight of the vote is based on the amount of bonded Atoms of the sender. If they don't vote, delegator inherit the vote of their validator. However, delegators can override their validator's vote by sending a vote themselves. + +At the end of the voting period, the proposal is accepted if there are more than 50% `Yes` votes (excluding `Abstain ` votes) and less than 33.33% of `NoWithVeto` votes (excluding `Abstain` votes). + +### In practice + +::: warning +**Note: These commands need to be run on an online computer. It is more secure to perform them commands using a ledger device. For the offline procedure, click [here](#signing-transactions-from-an-offline-computer).** +::: + +```bash +// Submit a Proposal +// =text/parameter_change/software_upgrade +// ex value for flag: =0.0001stake + +gaiacli tx gov submit-proposal --title "Test Proposal" --description "My awesome proposal" --type --deposit=10stake --gas auto --gas-prices --from + +// Increase deposit of a proposal +// Retrieve proposalID from $gaiacli query gov proposals --status deposit_period +// ex value for parameter: =1stake + +gaiacli tx gov deposit --gas auto --gas-prices --from + +// Vote on a proposal +// Retrieve proposalID from $gaiacli query gov proposals --status voting_period +//