diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..cb2577908 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,222 @@ +version: 2 + +defaults: &defaults + working_directory: /go/src/github.com/tendermint/tendermint + docker: + - image: circleci/golang:1.10.0 + environment: + GOBIN: /tmp/workspace/bin + +jobs: + setup_dependencies: + <<: *defaults + steps: + - run: mkdir -p /tmp/workspace/bin + - run: mkdir -p /tmp/workspace/profiles + - checkout + - restore_cache: + keys: + - v1-pkg-cache + - run: + name: tools + command: | + export PATH="$GOBIN:$PATH" + make get_tools + - run: + name: dependencies + command: | + export PATH="$GOBIN:$PATH" + make get_vendor_deps + - run: + name: binaries + command: | + export PATH="$GOBIN:$PATH" + make install + - persist_to_workspace: + root: /tmp/workspace + paths: + - bin + - profiles + - save_cache: + key: v1-pkg-cache + paths: + - /go/pkg + - save_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + paths: + - /go/src/github.com/tendermint/tendermint + + setup_abci: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Checkout abci + command: | + commit=$(bash scripts/dep_utils/parse.sh abci) + go get -v -u -d github.com/tendermint/abci/... + cd /go/src/github.com/tendermint/abci + git checkout "$commit" + - run: + working_directory: /go/src/github.com/tendermint/abci + name: Install abci + command: | + set -ex + export PATH="$GOBIN:$PATH" + make get_tools + make get_vendor_deps + make install + - run: ls -lah /tmp/workspace/bin + - persist_to_workspace: + root: /tmp/workspace + paths: + - "bin/abci*" + + lint: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: metalinter + command: | + set -ex + export PATH="$GOBIN:$PATH" + make metalinter + + test_apps: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends bsdmainutils + - run: + name: Run tests + command: bash test/app/test.sh + + test_cover: + <<: *defaults + parallelism: 4 + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Run tests + command: | + for pkg in $(go list github.com/tendermint/tendermint/... | grep -v /vendor/ | circleci tests split --split-by=timings); do + id=$(basename "$pkg") + + go test -timeout 5m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" + done + - persist_to_workspace: + root: /tmp/workspace + paths: + - "profiles/*" + + test_libs: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Run tests + command: bash test/test_libs.sh + + test_persistence: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-pkg-cache + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Run tests + command: bash test/persist/test_failure_indices.sh + + test_p2p: + environment: + GOBIN: /home/circleci/.go_workspace/bin + GOPATH: /home/circleci/.go_workspace + machine: + image: circleci/classic:latest + steps: + - checkout + - run: mkdir -p $GOPATH/src/github.com/tendermint + - run: ln -sf /home/circleci/project $GOPATH/src/github.com/tendermint/tendermint + - run: bash test/circleci/p2p.sh + + upload_coverage: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - restore_cache: + key: v1-tree-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: gather + command: | + set -ex + + echo "mode: atomic" > coverage.txt + for prof in $(ls /tmp/workspace/profiles/); do + tail -n +2 /tmp/workspace/profiles/"$prof" >> coverage.txt + done + - run: + name: upload + command: bash <(curl -s https://codecov.io/bash) -f coverage.txt + +workflows: + version: 2 + test-suite: + jobs: + - setup_dependencies + - setup_abci: + requires: + - setup_dependencies + - lint: + requires: + - setup_dependencies + - test_apps: + requires: + - setup_abci + - test_cover: + requires: + - setup_dependencies + - test_libs: + filters: + branches: + only: + - develop + - master + requires: + - setup_dependencies + - test_persistence: + requires: + - setup_abci + - test_p2p + - upload_coverage: + requires: + - test_cover diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 7321557bc..000000000 --- a/.codecov.yml +++ /dev/null @@ -1,26 +0,0 @@ - -# -# This codecov.yml is the default configuration for -# all repositories on Codecov. You may adjust the settings -# below in your own codecov.yml in your repository. -# -coverage: - precision: 2 - round: down - range: 70...100 - - status: - # Learn more at https://codecov.io/docs#yaml_default_commit_status - project: - default: - threshold: 1% # allow this much decrease on project - changes: false - -comment: - layout: "header, diff" - behavior: default # update if exists else create new - -ignore: - - "docs" - - "*.md" - - "*.rst" diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE index 6b0b11f0d..e714a6ffe 100644 --- a/.github/ISSUE_TEMPLATE +++ b/.github/ISSUE_TEMPLATE @@ -37,5 +37,8 @@ in a case of bug. **How to reproduce it** (as minimally and precisely as possible): +**Logs (you can paste a part showing an error or attach the whole file)**: + +**`/dump_consensus_state` output for consensus bugs** **Anything else do we need to know**: diff --git a/.gitignore b/.gitignore index b031ce185..7b7b746fb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ docs/tools scripts/wal2json/wal2json scripts/cutWALUntil/cutWALUntil + +.idea/ +*.iml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3456fb58d..02f032c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,31 @@ BUG FIXES: - Graceful handling/recovery for apps that have non-determinism or fail to halt - Graceful handling/recovery for violations of safety, or liveness -## 0.16.0 (February 20th, 2017) +## 0.17.0 (March 27th, 2018) + +BREAKING: +- [types] WriteSignBytes -> SignBytes + +IMPROVEMENTS: +- [all] renamed `dummy` (`persistent_dummy`) to `kvstore` (`persistent_kvstore`) (name "dummy" is deprecated and will not work in the next breaking release) +- [config] exposed `auth_enc` flag to enable/disable encryption +- [docs] note on determinism (docs/determinism.rst) +- [genesis] `app_options` field is deprecated. please rename it to `app_state` in your genesis file(s). `app_options` will not work in the next breaking release +- [p2p] dial seeds directly without potential peers +- [p2p] exponential backoff for addrs in the address book +- [p2p] mark peer as good if it contributed enough votes or block parts +- [p2p] stop peer if it sends incorrect data, msg to unknown channel, msg we did not expect +- [p2p] when `auth_enc` is true, all dialed peers must have a node ID in their address +- [spec] various improvements +- switched from glide to dep internally for package management +- [wire] prep work for upgrading to new go-wire (which is now called go-amino) +- [types/priv_validator] new format and socket client, allowing for remote signing + +FEATURES: +- [config] added the `--p2p.private_peer_ids` flag and `PrivatePeerIDs` config variable (see config for description) +- [rpc] added `/health` endpoint, which returns empty result for now + +## 0.16.0 (February 20th, 2018) BREAKING CHANGES: - [config] use $TMHOME/config for all config and json files diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b991bcc4e..5fd2d982f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,15 +34,26 @@ Please don't make Pull Requests to `master`. ## Dependencies -We use [glide](https://github.com/masterminds/glide) to manage dependencies. -That said, the master branch of every Tendermint repository should just build with `go get`, which means they should be kept up-to-date with their dependencies so we can get away with telling people they can just `go get` our software. -Since some dependencies are not under our control, a third party may break our build, in which case we can fall back on `glide install`. Even for dependencies under our control, glide helps us keeps multiple repos in sync as they evolve. Anything with an executable, such as apps, tools, and the core, should use glide. +We use [dep](https://github.com/golang/dep) to manage dependencies. -Run `bash scripts/glide/status.sh` to get a list of vendored dependencies that may not be up-to-date. +That said, the master branch of every Tendermint repository should just build +with `go get`, which means they should be kept up-to-date with their +dependencies so we can get away with telling people they can just `go get` our +software. + +Since some dependencies are not under our control, a third party may break our +build, in which case we can fall back on `dep ensure` (or `make +get_vendor_deps`). Even for dependencies under our control, dep helps us to +keep multiple repos in sync as they evolve. Anything with an executable, such +as apps, tools, and the core, should use dep. + +Run `dep status` to get a list of vendored dependencies that may not be +up-to-date. ## Vagrant -If you are a [Vagrant](https://www.vagrantup.com/) user, you can get started hacking Tendermint with the commands below. +If you are a [Vagrant](https://www.vagrantup.com/) user, you can get started +hacking Tendermint with the commands below. NOTE: In case you installed Vagrant in 2017, you might need to run `vagrant box update` to upgrade to the latest `ubuntu/xenial64`. @@ -53,11 +64,14 @@ vagrant ssh make test ``` - ## Testing -All repos should be hooked up to circle. -If they have `.go` files in the root directory, they will be automatically tested by circle using `go test -v -race ./...`. If not, they will need a `circle.yml`. Ideally, every repo has a `Makefile` that defines `make test` and includes its continuous integration status using a badge in the `README.md`. +All repos should be hooked up to [CircleCI](https://circleci.com/). + +If they have `.go` files in the root directory, they will be automatically +tested by circle using `go test -v -race ./...`. If not, they will need a +`circle.yml`. Ideally, every repo has a `Makefile` that defines `make test` and +includes its continuous integration status using a badge in the `README.md`. ## Branching Model and Release diff --git a/DOCKER/Dockerfile.develop b/DOCKER/Dockerfile.develop index 2ffb68281..e6ee6607e 100644 --- a/DOCKER/Dockerfile.develop +++ b/DOCKER/Dockerfile.develop @@ -18,9 +18,9 @@ RUN mkdir -p /go/src/github.com/tendermint/tendermint && \ cd /go/src/github.com/tendermint/tendermint && \ git clone https://github.com/tendermint/tendermint . && \ git checkout develop && \ + make get_tools && \ make get_vendor_deps && \ make install && \ - glide cc && \ cd - && \ rm -rf /go/src/github.com/tendermint/tendermint && \ apk del go build-base git @@ -32,4 +32,4 @@ EXPOSE 46657 ENTRYPOINT ["tendermint"] -CMD ["node", "--moniker=`hostname`", "--proxy_app=dummy"] +CMD ["node", "--moniker=`hostname`", "--proxy_app=kvstore"] diff --git a/DOCKER/README.md b/DOCKER/README.md index 06f400eae..24cd38a51 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -34,13 +34,13 @@ To get started developing applications, see the [application developers guide](h # How to use this image -## Start one instance of the Tendermint core with the `dummy` app +## Start one instance of the Tendermint core with the `kvstore` app A very simple example of a built-in app and Tendermint core in one container. ``` docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint init -docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy_app=dummy +docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy_app=kvstore ``` ## mintnet-kubernetes diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 000000000..894d98ead --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,386 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/btcsuite/btcd" + packages = ["btcec"] + revision = "2be2f12b358dc57d70b8f501b00be450192efbc3" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/ebuchman/fail-test" + packages = ["."] + revision = "95f809107225be108efcf10a3509e4ea6ceef3c4" + +[[projects]] + name = "github.com/fortytw2/leaktest" + packages = ["."] + revision = "a5ef70473c97b71626b9abeda80ee92ba2a7de9e" + version = "v1.2.0" + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + name = "github.com/go-kit/kit" + packages = [ + "log", + "log/level", + "log/term" + ] + revision = "4dc7be5d2d12881735283bcab7352178e190fc71" + version = "v0.6.0" + +[[projects]] + name = "github.com/go-logfmt/logfmt" + packages = ["."] + revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" + version = "v0.3.0" + +[[projects]] + name = "github.com/go-stack/stack" + packages = ["."] + revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" + version = "v1.7.0" + +[[projects]] + name = "github.com/gogo/protobuf" + packages = [ + "gogoproto", + "jsonpb", + "proto", + "protoc-gen-gogo/descriptor", + "sortkeys", + "types" + ] + revision = "1adfc126b41513cc696b209667c8656ea7aac67c" + version = "v1.0.0" + +[[projects]] + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp" + ] + revision = "925541529c1fa6821df4e44ce2723319eb2be768" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/golang/snappy" + packages = ["."] + revision = "553a641470496b2327abcac10b36396bd98e45c9" + +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" + +[[projects]] + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + +[[projects]] + branch = "master" + name = "github.com/jmhodges/levigo" + packages = ["."] + revision = "c42d9e0ca023e2198120196f842701bb4c55d7b9" + +[[projects]] + branch = "master" + name = "github.com/kr/logfmt" + packages = ["."] + revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" + +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/rcrowley/go-metrics" + packages = ["."] + revision = "8732c616f52954686704c8645fe1a9d59e9df7c1" + +[[projects]] + name = "github.com/spf13/afero" + packages = [ + ".", + "mem" + ] + revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" + version = "v1.0.2" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + +[[projects]] + name = "github.com/spf13/cobra" + packages = ["."] + revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" + version = "v0.0.1" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" + version = "v1.0.0" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" + +[[projects]] + name = "github.com/stretchr/testify" + packages = [ + "assert", + "require" + ] + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" + +[[projects]] + branch = "master" + name = "github.com/syndtr/goleveldb" + packages = [ + "leveldb", + "leveldb/cache", + "leveldb/comparer", + "leveldb/errors", + "leveldb/filter", + "leveldb/iterator", + "leveldb/journal", + "leveldb/memdb", + "leveldb/opt", + "leveldb/storage", + "leveldb/table", + "leveldb/util" + ] + revision = "169b1b37be738edb2813dab48c97a549bcf99bb5" + +[[projects]] + name = "github.com/tendermint/abci" + packages = [ + "client", + "example/code", + "example/counter", + "example/kvstore", + "server", + "types" + ] + revision = "46686763ba8ea595ede16530ed4a40fb38f49f94" + version = "v0.10.2" + +[[projects]] + branch = "master" + name = "github.com/tendermint/ed25519" + packages = [ + ".", + "edwards25519", + "extra25519" + ] + revision = "d8387025d2b9d158cf4efb07e7ebf814bcce2057" + +[[projects]] + name = "github.com/tendermint/go-crypto" + packages = ["."] + revision = "c3e19f3ea26f5c3357e0bcbb799b0761ef923755" + version = "v0.5.0" + +[[projects]] + name = "github.com/tendermint/go-wire" + packages = [ + ".", + "data" + ] + revision = "fa721242b042ecd4c6ed1a934ee740db4f74e45c" + source = "github.com/tendermint/go-amino" + version = "v0.7.3" + +[[projects]] + name = "github.com/tendermint/tmlibs" + packages = [ + "autofile", + "cli", + "cli/flags", + "clist", + "common", + "db", + "flowrate", + "log", + "merkle", + "pubsub", + "pubsub/query", + "test" + ] + revision = "24da7009c3d8c019b40ba4287495749e3160caca" + version = "v0.7.1" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "curve25519", + "nacl/box", + "nacl/secretbox", + "openpgp/armor", + "openpgp/errors", + "poly1305", + "ripemd160", + "salsa20/salsa" + ] + revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "context", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "lex/httplex", + "trace" + ] + revision = "6078986fec03a1dcc236c34816c71b0e05018fda" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix"] + revision = "91ee8cde435411ca3f1cd365e8f20131aed4d0a1" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + revision = "f8c8703595236ae70fdf8789ecb656ea0bcdcf46" + +[[projects]] + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "codes", + "connectivity", + "credentials", + "grpclb/grpc_lb_v1/messages", + "grpclog", + "internal", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "stats", + "status", + "tap", + "transport" + ] + revision = "5b3c4e850e90a4cf6a20ebd46c8b32a0a3afcb9e" + version = "v1.7.5" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" + version = "v2.1.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "4dca5dbd2d280d093d7c8fc423606ab86d6ad1b241b076a7716c2093b5a09231" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 000000000..098d5ea9b --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,95 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "github.com/ebuchman/fail-test" + +[[constraint]] + branch = "master" + name = "github.com/fortytw2/leaktest" + +[[constraint]] + name = "github.com/go-kit/kit" + version = "~0.6.0" + +[[constraint]] + name = "github.com/gogo/protobuf" + version = "~1.0.0" + +[[constraint]] + name = "github.com/golang/protobuf" + version = "~1.0.0" + +[[constraint]] + name = "github.com/gorilla/websocket" + version = "~1.2.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "~0.8.0" + +[[constraint]] + branch = "master" + name = "github.com/rcrowley/go-metrics" + +[[constraint]] + name = "github.com/spf13/cobra" + version = "~0.0.1" + +[[constraint]] + name = "github.com/spf13/viper" + version = "~1.0.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "~1.2.1" + +[[constraint]] + name = "github.com/tendermint/abci" + version = "~0.10.2" + +[[constraint]] + name = "github.com/tendermint/go-crypto" + version = "~0.5.0" + +[[constraint]] + name = "github.com/tendermint/go-wire" + source = "github.com/tendermint/go-amino" + version = "~0.7.3" + +[[constraint]] + name = "github.com/tendermint/tmlibs" + version = "~0.7.1" + +[[constraint]] + name = "google.golang.org/grpc" + version = "~1.7.3" + +[prune] + go-tests = true + unused-packages = true diff --git a/Makefile b/Makefile index c765d078f..851151eeb 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ GOTOOLS = \ - github.com/tendermint/glide \ - # gopkg.in/alecthomas/gometalinter.v2 + github.com/golang/dep/cmd/dep \ + gopkg.in/alecthomas/gometalinter.v2 PACKAGES=$(shell go list ./... | grep -v '/vendor/') BUILD_TAGS?=tendermint BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" all: check build test install -check: check_tools get_vendor_deps +check: check_tools ensure_deps ######################################## @@ -40,36 +40,89 @@ check_tools: get_tools: @echo "--> Installing tools" go get -u -v $(GOTOOLS) - # @gometalinter.v2 --install + @gometalinter.v2 --install update_tools: @echo "--> Updating tools" @go get -u $(GOTOOLS) +#Run this from CI get_vendor_deps: @rm -rf vendor/ - @echo "--> Running glide install" - @glide install + @echo "--> Running dep" + @dep ensure -vendor-only + + +#Run this locally. +ensure_deps: + @rm -rf vendor/ + @echo "--> Running dep" + @dep ensure draw_deps: @# requires brew install graphviz or apt-get install graphviz go get github.com/RobotsAndPencils/goviz @goviz -i github.com/tendermint/tendermint/cmd/tendermint -d 3 | dot -Tpng -o dependency-graph.png +get_deps_bin_size: + @# Copy of build recipe with additional flags to perform binary size analysis + $(eval $(shell go build -work -a $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o build/tendermint ./cmd/tendermint/ 2>&1)) + @find $(WORK) -type f -name "*.a" | xargs -I{} du -hxs "{}" | sort -rh | sed -e s:${WORK}/::g > deps_bin_size.log + @echo "Results can be found here: $(CURDIR)/deps_bin_size.log" ######################################## ### Testing -test: - @echo "--> Running go test" - @go test $(PACKAGES) - -test_race: - @echo "--> Running go test --race" - @go test -v -race $(PACKAGES) +## required to be run first by most tests +build_docker_test_image: + docker build -t tester -f ./test/docker/Dockerfile . + +### coverage, app, persistence, and libs tests +test_cover: + # run the go unit tests with coverage + bash test/test_cover.sh + +test_apps: + # run the app tests using bash + # requires `abci-cli` and `tendermint` binaries installed + bash test/app/test.sh + +test_persistence: + # run the persistence tests using bash + # requires `abci-cli` installed + docker run --name run_persistence -t tester bash test/persist/test_failure_indices.sh + + # TODO undockerize + # bash test/persist/test_failure_indices.sh + +test_p2p: + docker rm -f rsyslog || true + rm -rf test/logs || true + mkdir test/logs + cd test/ + docker run -d -v "logs:/var/log/" -p 127.0.0.1:5514:514/udp --name rsyslog voxxit/rsyslog + cd .. + # requires 'tester' the image from above + bash test/p2p/test.sh tester + +need_abci: + bash scripts/install_abci_apps.sh test_integrations: - @bash ./test/test.sh + make build_docker_test_image + make get_tools + make get_vendor_deps + make install + make need_abci + make test_cover + make test_apps + make test_persistence + make test_p2p + +test_libs: + # checkout every github.com/tendermint dir and run its tests + # NOTE: on release-* or master branches only (set by Jenkins) + docker run --name run_libs -t tester bash test/test_libs.sh test_release: @go test -tags release $(PACKAGES) @@ -79,10 +132,17 @@ test100: vagrant_test: vagrant up - vagrant ssh -c 'make install' - vagrant ssh -c 'make test_race' vagrant ssh -c 'make test_integrations' +### go tests +test: + @echo "--> Running go test" + @go test $(PACKAGES) + +test_race: + @echo "--> Running go test --race" + @go test -v -race $(PACKAGES) + ######################################## ### Formatting, linting, and vetting @@ -92,7 +152,7 @@ fmt: metalinter: @echo "--> Running linter" - gometalinter.v2 --vendor --deadline=600s --disable-all \ + @gometalinter.v2 --vendor --deadline=600s --disable-all \ --enable=deadcode \ --enable=gosimple \ --enable=misspell \ @@ -123,8 +183,7 @@ metalinter_all: @echo "--> Running linter (all)" gometalinter.v2 --vendor --deadline=600s --enable-all --disable=lll ./... - # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check build build_race dist install check_tools get_tools update_tools get_vendor_deps draw_deps test test_race test_integrations test_release test100 vagrant_test fmt metalinter metalinter_all +.PHONY: check build build_race dist install check_tools get_tools update_tools get_vendor_deps draw_deps test_cover test_apps test_persistence test_p2p test test_race test_libs test_integrations test_release test100 vagrant_test fmt diff --git a/benchmarks/blockchain/localsync.sh b/benchmarks/blockchain/localsync.sh index 389464b6c..2dc3e49c0 100755 --- a/benchmarks/blockchain/localsync.sh +++ b/benchmarks/blockchain/localsync.sh @@ -14,7 +14,7 @@ if [ ! -d $DATA ]; then echo "starting node" tendermint node \ --home $DATA \ - --proxy_app dummy \ + --proxy_app kvstore \ --p2p.laddr tcp://127.0.0.1:56656 \ --rpc.laddr tcp://127.0.0.1:56657 \ --log_level error & @@ -35,7 +35,7 @@ cp -R $DATA $HOME1 echo "starting validator node" tendermint node \ --home $HOME1 \ - --proxy_app dummy \ + --proxy_app kvstore \ --p2p.laddr tcp://127.0.0.1:56656 \ --rpc.laddr tcp://127.0.0.1:56657 \ --log_level error & @@ -48,7 +48,7 @@ cp $HOME1/genesis.json $HOME2 printf "starting downloader node" tendermint node \ --home $HOME2 \ - --proxy_app dummy \ + --proxy_app kvstore \ --p2p.laddr tcp://127.0.0.1:56666 \ --rpc.laddr tcp://127.0.0.1:56667 \ --p2p.persistent_peers 127.0.0.1:56656 \ diff --git a/blockchain/pool.go b/blockchain/pool.go index d0f4d2976..603b4bf2a 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -1,6 +1,7 @@ package blockchain import ( + "errors" "fmt" "math" "sync" @@ -39,9 +40,12 @@ const ( // Assuming a DSL connection (not a good choice) 128 Kbps (upload) ~ 15 KB/s, // sending data across atlantic ~ 7.5 KB/s. minRecvRate = 7680 + + // Maximum difference between current and new block's height. + maxDiffBetweenCurrentAndReceivedBlockHeight = 100 ) -var peerTimeoutSeconds = time.Duration(15) // not const so we can override with tests +var peerTimeout = 15 * time.Second // not const so we can override with tests /* Peers self report their heights when we join the block pool. @@ -68,10 +72,10 @@ type BlockPool struct { maxPeerHeight int64 requestsCh chan<- BlockRequest - timeoutsCh chan<- p2p.ID + errorsCh chan<- peerError } -func NewBlockPool(start int64, requestsCh chan<- BlockRequest, timeoutsCh chan<- p2p.ID) *BlockPool { +func NewBlockPool(start int64, requestsCh chan<- BlockRequest, errorsCh chan<- peerError) *BlockPool { bp := &BlockPool{ peers: make(map[p2p.ID]*bpPeer), @@ -80,7 +84,7 @@ func NewBlockPool(start int64, requestsCh chan<- BlockRequest, timeoutsCh chan<- numPending: 0, requestsCh: requestsCh, - timeoutsCh: timeoutsCh, + errorsCh: errorsCh, } bp.BaseService = *cmn.NewBaseService(nil, "BlockPool", bp) return bp @@ -128,9 +132,10 @@ func (pool *BlockPool) removeTimedoutPeers() { curRate := peer.recvMonitor.Status().CurRate // curRate can be 0 on start if curRate != 0 && curRate < minRecvRate { - pool.sendTimeout(peer.id) + err := errors.New("peer is not sending us data fast enough") + pool.sendError(err, peer.id) pool.Logger.Error("SendTimeout", "peer", peer.id, - "reason", "peer is not sending us data fast enough", + "reason", err, "curRate", fmt.Sprintf("%d KB/s", curRate/1024), "minRate", fmt.Sprintf("%d KB/s", minRecvRate/1024)) peer.didTimeout = true @@ -199,7 +204,7 @@ func (pool *BlockPool) PopRequest() { delete(pool.requesters, pool.height) pool.height++ } else { - cmn.PanicSanity(cmn.Fmt("Expected requester to pop, got nothing at height %v", pool.height)) + panic(fmt.Sprintf("Expected requester to pop, got nothing at height %v", pool.height)) } } @@ -213,8 +218,9 @@ func (pool *BlockPool) RedoRequest(height int64) p2p.ID { request := pool.requesters[height] if request.block == nil { - cmn.PanicSanity("Expected block to be non-nil") + panic("Expected block to be non-nil") } + // RemovePeer will redo all requesters associated with this peer. pool.removePeer(request.peerID) return request.peerID @@ -227,8 +233,14 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int requester := pool.requesters[block.Height] if requester == nil { - // a block we didn't expect. - // TODO:if height is too far ahead, punish peer + pool.Logger.Info("peer sent us a block we didn't expect", "peer", peerID, "curHeight", pool.height, "blockHeight", block.Height) + diff := pool.height - block.Height + if diff < 0 { + diff *= -1 + } + if diff > maxDiffBetweenCurrentAndReceivedBlockHeight { + pool.sendError(errors.New("peer sent us a block we didn't expect with a height too far ahead/behind"), peerID) + } return } @@ -339,11 +351,11 @@ func (pool *BlockPool) sendRequest(height int64, peerID p2p.ID) { pool.requestsCh <- BlockRequest{height, peerID} } -func (pool *BlockPool) sendTimeout(peerID p2p.ID) { +func (pool *BlockPool) sendError(err error, peerID p2p.ID) { if !pool.IsRunning() { return } - pool.timeoutsCh <- peerID + pool.errorsCh <- peerError{err, peerID} } // unused by tendermint; left for debugging purposes @@ -402,9 +414,9 @@ func (peer *bpPeer) resetMonitor() { func (peer *bpPeer) resetTimeout() { if peer.timeout == nil { - peer.timeout = time.AfterFunc(time.Second*peerTimeoutSeconds, peer.onTimeout) + peer.timeout = time.AfterFunc(peerTimeout, peer.onTimeout) } else { - peer.timeout.Reset(time.Second * peerTimeoutSeconds) + peer.timeout.Reset(peerTimeout) } } @@ -430,8 +442,9 @@ func (peer *bpPeer) onTimeout() { peer.pool.mtx.Lock() defer peer.pool.mtx.Unlock() - peer.pool.sendTimeout(peer.id) - peer.logger.Error("SendTimeout", "reason", "onTimeout") + err := errors.New("peer did not send us anything") + peer.pool.sendError(err, peer.id) + peer.logger.Error("SendTimeout", "reason", err, "timeout", peerTimeout) peer.didTimeout = true } diff --git a/blockchain/pool_test.go b/blockchain/pool_test.go index ce16899a7..82120eaef 100644 --- a/blockchain/pool_test.go +++ b/blockchain/pool_test.go @@ -13,7 +13,7 @@ import ( ) func init() { - peerTimeoutSeconds = time.Duration(2) + peerTimeout = 2 * time.Second } type testPeer struct { @@ -34,9 +34,9 @@ func makePeers(numPeers int, minHeight, maxHeight int64) map[p2p.ID]testPeer { func TestBasic(t *testing.T) { start := int64(42) peers := makePeers(10, start+1, 1000) - timeoutsCh := make(chan p2p.ID, 100) - requestsCh := make(chan BlockRequest, 100) - pool := NewBlockPool(start, requestsCh, timeoutsCh) + errorsCh := make(chan peerError, 1000) + requestsCh := make(chan BlockRequest, 1000) + pool := NewBlockPool(start, requestsCh, errorsCh) pool.SetLogger(log.TestingLogger()) err := pool.Start() @@ -71,8 +71,8 @@ func TestBasic(t *testing.T) { // Pull from channels for { select { - case peerID := <-timeoutsCh: - t.Errorf("timeout: %v", peerID) + case err := <-errorsCh: + t.Error(err) case request := <-requestsCh: t.Logf("Pulled new BlockRequest %v", request) if request.Height == 300 { @@ -91,9 +91,9 @@ func TestBasic(t *testing.T) { func TestTimeout(t *testing.T) { start := int64(42) peers := makePeers(10, start+1, 1000) - timeoutsCh := make(chan p2p.ID, 100) - requestsCh := make(chan BlockRequest, 100) - pool := NewBlockPool(start, requestsCh, timeoutsCh) + errorsCh := make(chan peerError, 1000) + requestsCh := make(chan BlockRequest, 1000) + pool := NewBlockPool(start, requestsCh, errorsCh) pool.SetLogger(log.TestingLogger()) err := pool.Start() if err != nil { @@ -132,9 +132,10 @@ func TestTimeout(t *testing.T) { timedOut := map[p2p.ID]struct{}{} for { select { - case peerID := <-timeoutsCh: - t.Logf("Peer %v timeouted", peerID) - if _, ok := timedOut[peerID]; !ok { + case err := <-errorsCh: + t.Log(err) + // consider error to be always timeout here + if _, ok := timedOut[err.peerID]; !ok { counter++ if counter == len(peers) { return // Done! diff --git a/blockchain/reactor.go b/blockchain/reactor.go index 2ad6770be..3c25eed2f 100644 --- a/blockchain/reactor.go +++ b/blockchain/reactor.go @@ -22,8 +22,7 @@ const ( // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) BlockchainChannel = byte(0x40) - defaultChannelCapacity = 1000 - trySyncIntervalMS = 50 + trySyncIntervalMS = 50 // stop syncing when last block's time is // within this much of the system time. // stopSyncingDurationMinutes = 10 @@ -40,6 +39,15 @@ type consensusReactor interface { SwitchToConsensus(sm.State, int) } +type peerError struct { + err error + peerID p2p.ID +} + +func (e peerError) Error() string { + return fmt.Sprintf("error with peer %v: %s", e.peerID, e.err.Error()) +} + // BlockchainReactor handles long-term catchup syncing. type BlockchainReactor struct { p2p.BaseReactor @@ -56,7 +64,7 @@ type BlockchainReactor struct { fastSync bool requestsCh <-chan BlockRequest - timeoutsCh <-chan p2p.ID + errorsCh <-chan peerError } // NewBlockchainReactor returns new reactor instance. @@ -64,17 +72,20 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *Bl fastSync bool) *BlockchainReactor { if state.LastBlockHeight != store.Height() { - cmn.PanicSanity(cmn.Fmt("state (%v) and store (%v) height mismatch", state.LastBlockHeight, + panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, store.Height())) } - requestsCh := make(chan BlockRequest, defaultChannelCapacity) - timeoutsCh := make(chan p2p.ID, defaultChannelCapacity) + const capacity = 1000 // must be bigger than peers count + requestsCh := make(chan BlockRequest, capacity) + errorsCh := make(chan peerError, capacity) // so we don't block in #Receive#pool.AddBlock + pool := NewBlockPool( store.Height()+1, requestsCh, - timeoutsCh, + errorsCh, ) + bcR := &BlockchainReactor{ params: state.ConsensusParams, initialState: state, @@ -83,7 +94,7 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *Bl pool: pool, fastSync: fastSync, requestsCh: requestsCh, - timeoutsCh: timeoutsCh, + errorsCh: errorsCh, } bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) return bcR @@ -166,7 +177,8 @@ func (bcR *BlockchainReactor) respondToPeer(msg *bcBlockRequestMessage, func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { _, msg, err := DecodeMessage(msgBytes, bcR.maxMsgSize()) if err != nil { - bcR.Logger.Error("Error decoding message", "err", err) + bcR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + bcR.Switch.StopPeerForError(src, err) return } @@ -230,7 +242,7 @@ func (bcR *BlockchainReactor) poolRoutine() { FOR_LOOP: for { select { - case request := <-bcR.requestsCh: // chan BlockRequest + case request := <-bcR.requestsCh: peer := bcR.Switch.Peers().Get(request.PeerID) if peer == nil { continue FOR_LOOP // Peer has since been disconnected. @@ -242,11 +254,10 @@ FOR_LOOP: // The pool handles timeouts, just let it go. continue FOR_LOOP } - case peerID := <-bcR.timeoutsCh: // chan string - // Peer timed out. - peer := bcR.Switch.Peers().Get(peerID) + case err := <-bcR.errorsCh: + peer := bcR.Switch.Peers().Get(err.peerID) if peer != nil { - bcR.Switch.StopPeerForError(peer, errors.New("BlockchainReactor Timeout")) + bcR.Switch.StopPeerForError(peer, err) } case <-statusUpdateTicker.C: // ask for status updates diff --git a/blockchain/store.go b/blockchain/store.go index a9a543436..b949bc904 100644 --- a/blockchain/store.go +++ b/blockchain/store.go @@ -76,7 +76,7 @@ func (bs *BlockStore) LoadBlock(height int64) *types.Block { } blockMeta := wire.ReadBinary(&types.BlockMeta{}, r, 0, &n, &err).(*types.BlockMeta) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading block meta: %v", err)) + panic(fmt.Sprintf("Error reading block meta: %v", err)) } bytez := []byte{} for i := 0; i < blockMeta.BlockID.PartsHeader.Total; i++ { @@ -85,7 +85,7 @@ func (bs *BlockStore) LoadBlock(height int64) *types.Block { } block := wire.ReadBinary(&types.Block{}, bytes.NewReader(bytez), 0, &n, &err).(*types.Block) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading block: %v", err)) + panic(fmt.Sprintf("Error reading block: %v", err)) } return block } @@ -102,7 +102,7 @@ func (bs *BlockStore) LoadBlockPart(height int64, index int) *types.Part { } part := wire.ReadBinary(&types.Part{}, r, 0, &n, &err).(*types.Part) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading block part: %v", err)) + panic(fmt.Sprintf("Error reading block part: %v", err)) } return part } @@ -118,7 +118,7 @@ func (bs *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { } blockMeta := wire.ReadBinary(&types.BlockMeta{}, r, 0, &n, &err).(*types.BlockMeta) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading block meta: %v", err)) + panic(fmt.Sprintf("Error reading block meta: %v", err)) } return blockMeta } @@ -136,7 +136,7 @@ func (bs *BlockStore) LoadBlockCommit(height int64) *types.Commit { } commit := wire.ReadBinary(&types.Commit{}, r, 0, &n, &err).(*types.Commit) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading commit: %v", err)) + panic(fmt.Sprintf("Error reading commit: %v", err)) } return commit } @@ -153,7 +153,7 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit { } commit := wire.ReadBinary(&types.Commit{}, r, 0, &n, &err).(*types.Commit) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading commit: %v", err)) + panic(fmt.Sprintf("Error reading commit: %v", err)) } return commit } @@ -262,7 +262,7 @@ func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON { bsj := BlockStoreStateJSON{} err := json.Unmarshal(bytes, &bsj) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Could not unmarshal bytes: %X", bytes)) + panic(fmt.Sprintf("Could not unmarshal bytes: %X", bytes)) } return bsj } diff --git a/circle.yml b/circle.yml deleted file mode 100644 index fd5fe180b..000000000 --- a/circle.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -machine: - environment: - MACH_PREFIX: tendermint-test-mach - DOCKER_VERSION: 1.10.0 - DOCKER_MACHINE_VERSION: 0.9.0 - GOPATH: "$HOME/.go_project" - PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" - PROJECT_PATH: "$PROJECT_PARENT_PATH/$CIRCLE_PROJECT_REPONAME" - PATH: "$HOME/.go_project/bin:${PATH}" - hosts: - localhost: 127.0.0.1 - -dependencies: - override: - - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | sudo bash -s -- $DOCKER_VERSION - - sudo start docker - - sudo curl -sSL -o /usr/bin/docker-machine "https://github.com/docker/machine/releases/download/v$DOCKER_MACHINE_VERSION/docker-machine-`uname -s`-`uname -m`"; sudo chmod 0755 /usr/bin/docker-machine - - mkdir -p "$PROJECT_PARENT_PATH" - - ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$PROJECT_PATH" - post: - - go version - - docker version - - docker-machine version - -test: - override: - - cd "$PROJECT_PATH" && set -o pipefail && make test_integrations 2>&1 | tee test_integrations.log: - timeout: 1800 - post: - - cd "$PROJECT_PATH" && mv test_integrations.log "${CIRCLE_ARTIFACTS}" - - cd "$PROJECT_PATH" && bash <(curl -s https://codecov.io/bash) -f coverage.txt - - cd "$PROJECT_PATH" && mv coverage.txt "${CIRCLE_ARTIFACTS}" - - cd "$PROJECT_PATH" && cp test/logs/messages "${CIRCLE_ARTIFACTS}/docker.log" - - cd "${CIRCLE_ARTIFACTS}" && tar czf logs.tar.gz *.log diff --git a/cmd/priv_val_server/main.go b/cmd/priv_val_server/main.go new file mode 100644 index 000000000..9f3ec73ca --- /dev/null +++ b/cmd/priv_val_server/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "flag" + "os" + + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + priv_val "github.com/tendermint/tendermint/types/priv_validator" +) + +func main() { + var ( + addr = flag.String("addr", ":46659", "Address of client to connect to") + chainID = flag.String("chain-id", "mychain", "chain id") + privValPath = flag.String("priv", "", "priv val file path") + + logger = log.NewTMLogger( + log.NewSyncWriter(os.Stdout), + ).With("module", "priv_val") + ) + flag.Parse() + + logger.Info( + "Starting private validator", + "addr", *addr, + "chainID", *chainID, + "privPath", *privValPath, + ) + + privVal := priv_val.LoadPrivValidatorJSON(*privValPath) + + rs := priv_val.NewRemoteSigner( + logger, + *chainID, + *addr, + privVal, + crypto.GenPrivKeyEd25519(), + ) + err := rs.Start() + if err != nil { + panic(err) + } + + cmn.TrapSignal(func() { + err := rs.Stop() + if err != nil { + panic(err) + } + }) +} diff --git a/cmd/tendermint/commands/init.go b/cmd/tendermint/commands/init.go index 0bc9e6269..70648f8ff 100644 --- a/cmd/tendermint/commands/init.go +++ b/cmd/tendermint/commands/init.go @@ -24,7 +24,7 @@ func initFiles(cmd *cobra.Command, args []string) { } else { privValidator = types.GenPrivValidatorFS(privValFile) privValidator.Save() - logger.Info("Genetated private validator", "path", privValFile) + logger.Info("Generated private validator", "path", privValFile) } // genesis file @@ -43,6 +43,6 @@ func initFiles(cmd *cobra.Command, args []string) { if err := genDoc.SaveAs(genFile); err != nil { panic(err) } - logger.Info("Genetated genesis file", "path", genFile) + logger.Info("Generated genesis file", "path", genFile) } } diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index d94f95ba4..5ea20a785 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -1,6 +1,9 @@ package commands import ( + "fmt" + "net/url" + "github.com/spf13/cobra" cmn "github.com/tendermint/tmlibs/common" @@ -32,12 +35,36 @@ var ( func init() { LiteCmd.Flags().StringVar(&listenAddr, "laddr", ":8888", "Serve the proxy on the given port") - LiteCmd.Flags().StringVar(&nodeAddr, "node", "localhost:46657", "Connect to a Tendermint node at this address") + LiteCmd.Flags().StringVar(&nodeAddr, "node", "tcp://localhost:46657", "Connect to a Tendermint node at this address") LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") } +func ensureAddrHasSchemeOrDefaultToTCP(addr string) (string, error) { + u, err := url.Parse(nodeAddr) + if err != nil { + return "", err + } + switch u.Scheme { + case "tcp", "unix": + case "": + u.Scheme = "tcp" + default: + return "", fmt.Errorf("unknown scheme %q, use either tcp or unix", u.Scheme) + } + return u.String(), nil +} + func runProxy(cmd *cobra.Command, args []string) error { + nodeAddr, err := ensureAddrHasSchemeOrDefaultToTCP(nodeAddr) + if err != nil { + return err + } + listenAddr, err := ensureAddrHasSchemeOrDefaultToTCP(listenAddr) + if err != nil { + return err + } + // First, connect a client node := rpcclient.NewHTTP(nodeAddr, "/websocket") diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index 76d61671d..6ad92441b 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -14,11 +14,14 @@ func AddNodeFlags(cmd *cobra.Command) { // bind flags cmd.Flags().String("moniker", config.Moniker, "Node Name") + // priv val flags + cmd.Flags().String("priv_validator_laddr", config.PrivValidatorListenAddr, "Socket address to listen on for connections from external priv_validator process") + // node flags cmd.Flags().Bool("fast_sync", config.FastSync, "Fast blockchain syncing") // abci flags - cmd.Flags().String("proxy_app", config.ProxyApp, "Proxy app address, or 'nilapp' or 'dummy' for local testing.") + cmd.Flags().String("proxy_app", config.ProxyApp, "Proxy app address, or 'nilapp' or 'kvstore' for local testing.") cmd.Flags().String("abci", config.ABCI, "Specify abci transport (socket | grpc)") // rpc flags @@ -28,18 +31,19 @@ func AddNodeFlags(cmd *cobra.Command) { // p2p flags cmd.Flags().String("p2p.laddr", config.P2P.ListenAddress, "Node listen address. (0.0.0.0:0 means any interface, any port)") - cmd.Flags().String("p2p.seeds", config.P2P.Seeds, "Comma delimited host:port seed nodes") - cmd.Flags().String("p2p.persistent_peers", config.P2P.PersistentPeers, "Comma delimited host:port persistent peers") + cmd.Flags().String("p2p.seeds", config.P2P.Seeds, "Comma-delimited ID@host:port seed nodes") + cmd.Flags().String("p2p.persistent_peers", config.P2P.PersistentPeers, "Comma-delimited ID@host:port persistent peers") cmd.Flags().Bool("p2p.skip_upnp", config.P2P.SkipUPNP, "Skip UPNP configuration") cmd.Flags().Bool("p2p.pex", config.P2P.PexReactor, "Enable/disable Peer-Exchange") cmd.Flags().Bool("p2p.seed_mode", config.P2P.SeedMode, "Enable/disable seed mode") + cmd.Flags().String("p2p.private_peer_ids", config.P2P.PrivatePeerIDs, "Comma-delimited private peer IDs") // consensus flags cmd.Flags().Bool("consensus.create_empty_blocks", config.Consensus.CreateEmptyBlocks, "Set this to false to only produce blocks when there are txs or when the AppHash changes") } -// NewRunNodeCmd returns the command that allows the CLI to start a -// node. It can be used with a custom PrivValidator and in-process ABCI application. +// NewRunNodeCmd returns the command that allows the CLI to start a node. +// It can be used with a custom PrivValidator and in-process ABCI application. func NewRunNodeCmd(nodeProvider nm.NodeProvider) *cobra.Command { cmd := &cobra.Command{ Use: "node", diff --git a/cmd/tendermint/commands/show_node_id.go b/cmd/tendermint/commands/show_node_id.go new file mode 100644 index 000000000..92af49417 --- /dev/null +++ b/cmd/tendermint/commands/show_node_id.go @@ -0,0 +1,25 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/tendermint/tendermint/p2p" +) + +// ShowNodeIDCmd dumps node's ID to the standard output. +var ShowNodeIDCmd = &cobra.Command{ + Use: "show_node_id", + Short: "Show this node's ID", + RunE: showNodeID, +} + +func showNodeID(cmd *cobra.Command, args []string) error { + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return err + } + fmt.Println(nodeKey.ID()) + return nil +} diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index f5551a95e..3864e7ad5 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -75,15 +75,16 @@ func testnetFiles(cmd *cobra.Command, args []string) { // Initialize per-machine core directory func initMachCoreDirectory(base, mach string) error { + // Create priv_validator.json file if not present + defaultConfig := cfg.DefaultBaseConfig() dir := filepath.Join(base, mach) - err := cmn.EnsureDir(dir, 0777) + privValPath := filepath.Join(dir, defaultConfig.PrivValidator) + dir = filepath.Dir(privValPath) + err := cmn.EnsureDir(dir, 0700) if err != nil { return err } - - // Create priv_validator.json file if not present - defaultConfig := cfg.DefaultBaseConfig() - ensurePrivValidator(filepath.Join(dir, defaultConfig.PrivValidator)) + ensurePrivValidator(privValPath) return nil } diff --git a/cmd/tendermint/main.go b/cmd/tendermint/main.go index 17b5a585b..fd6287115 100644 --- a/cmd/tendermint/main.go +++ b/cmd/tendermint/main.go @@ -24,6 +24,7 @@ func main() { cmd.ResetPrivValidatorCmd, cmd.ShowValidatorCmd, cmd.TestnetFilesCmd, + cmd.ShowNodeIDCmd, cmd.VersionCmd) // NOTE: diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..6db0f15d5 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,18 @@ +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + threshold: 1% + patch: on + changes: off + +comment: + layout: "diff, files" + behavior: default + require_changes: no + require_base: no + require_head: yes diff --git a/config/config.go b/config/config.go index 621847bd0..6b3f87b57 100644 --- a/config/config.go +++ b/config/config.go @@ -20,9 +20,10 @@ var ( defaultConfigFileName = "config.toml" defaultGenesisJSONName = "genesis.json" - defaultPrivValName = "priv_validator.json" - defaultNodeKeyName = "node_key.json" - defaultAddrBookName = "addrbook.json" + + defaultPrivValName = "priv_validator.json" + defaultNodeKeyName = "node_key.json" + defaultAddrBookName = "addrbook.json" defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName) defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName) @@ -103,6 +104,10 @@ type BaseConfig struct { // A custom human readable name for this node Moniker string `mapstructure:"moniker"` + // TCP or UNIX socket address for Tendermint to listen on for + // connections from an external PrivValidator process + PrivValidatorListenAddr string `mapstructure:"priv_validator_laddr"` + // TCP or UNIX socket address of the ABCI application, // or the name of an ABCI application compiled in with the Tendermint binary ProxyApp string `mapstructure:"proxy_app"` @@ -158,7 +163,7 @@ func DefaultBaseConfig() BaseConfig { func TestBaseConfig() BaseConfig { conf := DefaultBaseConfig() conf.chainID = "tendermint_test" - conf.ProxyApp = "dummy" + conf.ProxyApp = "kvstore" conf.FastSync = false conf.DBBackend = "memdb" return conf @@ -245,8 +250,8 @@ type P2PConfig struct { // We only use these if we can’t connect to peers in the addrbook Seeds string `mapstructure:"seeds"` - // Comma separated list of persistent peers to connect to - // We always connect to these + // Comma separated list of nodes to keep persistent connections to + // Do not add private peers to this list if you don't want them advertised PersistentPeers string `mapstructure:"persistent_peers"` // Skip UPNP port forwarding @@ -281,6 +286,12 @@ type P2PConfig struct { // // Does not work if the peer-exchange reactor is disabled. SeedMode bool `mapstructure:"seed_mode"` + + // Authenticated encryption + AuthEnc bool `mapstructure:"auth_enc"` + + // Comma separated list of peer IDs to keep private (will not be gossiped to other peers) + PrivatePeerIDs string `mapstructure:"private_peer_ids"` } // DefaultP2PConfig returns a default configuration for the peer-to-peer layer @@ -296,6 +307,7 @@ func DefaultP2PConfig() *P2PConfig { RecvRate: 512000, // 500 kB/s PexReactor: true, SeedMode: false, + AuthEnc: true, } } diff --git a/config/toml.go b/config/toml.go index e69cf37fe..13f71db86 100644 --- a/config/toml.go +++ b/config/toml.go @@ -127,6 +127,7 @@ laddr = "{{ .P2P.ListenAddress }}" seeds = "" # Comma separated list of nodes to keep persistent connections to +# Do not add private peers to this list if you don't want them advertised persistent_peers = "" # Path to address book @@ -159,6 +160,12 @@ pex = {{ .P2P.PexReactor }} # Does not work if the peer-exchange reactor is disabled. seed_mode = {{ .P2P.SeedMode }} +# Authenticated encryption +auth_enc = {{ .P2P.AuthEnc }} + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "{{ .P2P.PrivatePeerIDs }}" + ##### mempool configuration options ##### [mempool] diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index 38bb37474..f8163c07d 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -46,9 +46,9 @@ func TestByzantine(t *testing.T) { eventChans := make([]chan interface{}, N) reactors := make([]p2p.Reactor, N) for i := 0; i < N; i++ { + // make first val byzantine if i == 0 { css[i].privValidator = NewByzantinePrivValidator(css[i].privValidator) - // make byzantine css[i].decideProposal = func(j int) func(int64, int) { return func(height int64, round int) { byzantineDecideProposalFunc(t, height, round, css[j], switches[j]) @@ -74,9 +74,11 @@ func TestByzantine(t *testing.T) { var conRI p2p.Reactor // nolint: gotype, gosimple conRI = conR + // make first val byzantine if i == 0 { conRI = NewByzantineReactor(conR) } + reactors[i] = conRI } @@ -115,19 +117,19 @@ func TestByzantine(t *testing.T) { // and the other block to peers[1] and peers[2]. // note peers and switches order don't match. peers := switches[0].Peers().List() + + // partition A ind0 := getSwitchIndex(switches, peers[0]) + + // partition B ind1 := getSwitchIndex(switches, peers[1]) ind2 := getSwitchIndex(switches, peers[2]) - - // connect the 2 peers in the larger partition p2p.Connect2Switches(switches, ind1, ind2) - // wait for someone in the big partition to make a block + // wait for someone in the big partition (B) to make a block <-eventChans[ind2] t.Log("A block has been committed. Healing partition") - - // connect the partitions p2p.Connect2Switches(switches, ind0, ind1) p2p.Connect2Switches(switches, ind0, ind2) @@ -289,17 +291,17 @@ func (privVal *ByzantinePrivValidator) GetPubKey() crypto.PubKey { } func (privVal *ByzantinePrivValidator) SignVote(chainID string, vote *types.Vote) (err error) { - vote.Signature, err = privVal.Sign(types.SignBytes(chainID, vote)) + vote.Signature, err = privVal.Sign(vote.SignBytes(chainID)) return err } func (privVal *ByzantinePrivValidator) SignProposal(chainID string, proposal *types.Proposal) (err error) { - proposal.Signature, _ = privVal.Sign(types.SignBytes(chainID, proposal)) + proposal.Signature, _ = privVal.Sign(proposal.SignBytes(chainID)) return nil } func (privVal *ByzantinePrivValidator) SignHeartbeat(chainID string, heartbeat *types.Heartbeat) (err error) { - heartbeat.Signature, _ = privVal.Sign(types.SignBytes(chainID, heartbeat)) + heartbeat.Signature, _ = privVal.Sign(heartbeat.SignBytes(chainID)) return nil } diff --git a/consensus/common_test.go b/consensus/common_test.go index c27b50c4f..6d16958ca 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -26,7 +26,7 @@ import ( "github.com/tendermint/tmlibs/log" "github.com/tendermint/abci/example/counter" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/go-kit/kit/log/term" ) @@ -50,7 +50,7 @@ func ResetConfig(name string) *cfg.Config { } //------------------------------------------------------------------------------- -// validator stub (a dummy consensus peer we control) +// validator stub (a kvstore consensus peer we control) type validatorStub struct { Index int // Validator index. NOTE: we don't assume validator set changes. @@ -488,7 +488,7 @@ func newCounter() abci.Application { return counter.NewCounterApplication(true) } -func newPersistentDummy() abci.Application { - dir, _ := ioutil.TempDir("/tmp", "persistent-dummy") - return dummy.NewPersistentDummyApplication(dir) +func newPersistentKVStore() abci.Application { + dir, _ := ioutil.TempDir("/tmp", "persistent-kvstore") + return kvstore.NewPersistentKVStoreApplication(dir) } diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index d1714a741..d283ff4ba 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -152,6 +152,7 @@ func TestMempoolRmBadTx(t *testing.T) { txs := cs.mempool.Reap(1) if len(txs) == 0 { emptyMempoolCh <- struct{}{} + return } time.Sleep(10 * time.Millisecond) } diff --git a/consensus/reactor.go b/consensus/reactor.go index b63793670..a265f76c0 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -27,6 +27,8 @@ const ( VoteSetBitsChannel = byte(0x23) maxConsensusMessageSize = 1048576 // 1MB; NOTE/TODO: keep in sync with types.PartSet sizes. + + blocksToContributeToBecomeGoodPeer = 10000 ) //----------------------------------------------------------------------------- @@ -179,7 +181,7 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) _, msg, err := DecodeMessage(msgBytes) if err != nil { conR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) - // TODO punish peer? + conR.Switch.StopPeerForError(src, err) return } conR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) @@ -251,6 +253,9 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) ps.ApplyProposalPOLMessage(msg) case *BlockPartMessage: ps.SetHasProposalBlockPart(msg.Height, msg.Round, msg.Part.Index) + if numBlocks := ps.RecordBlockPart(msg); numBlocks%blocksToContributeToBecomeGoodPeer == 0 { + conR.Switch.MarkPeerAsGood(src) + } conR.conS.peerMsgQueue <- msgInfo{msg, src.ID()} default: conR.Logger.Error(cmn.Fmt("Unknown message type %v", reflect.TypeOf(msg))) @@ -270,6 +275,9 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) ps.EnsureVoteBitArrays(height, valSize) ps.EnsureVoteBitArrays(height-1, lastCommitSize) ps.SetHasVote(msg.Vote) + if blocks := ps.RecordVote(msg.Vote); blocks%blocksToContributeToBecomeGoodPeer == 0 { + conR.Switch.MarkPeerAsGood(src) + } cs.peerMsgQueue <- msgInfo{msg, src.ID()} @@ -831,6 +839,21 @@ type PeerState struct { mtx sync.Mutex cstypes.PeerRoundState + + stats *peerStateStats +} + +// peerStateStats holds internal statistics for a peer. +type peerStateStats struct { + lastVoteHeight int64 + votes int + + lastBlockPartHeight int64 + blockParts int +} + +func (pss peerStateStats) String() string { + return fmt.Sprintf("peerStateStats{votes: %d, blockParts: %d}", pss.votes, pss.blockParts) } // NewPeerState returns a new PeerState for the given Peer @@ -844,6 +867,7 @@ func NewPeerState(peer p2p.Peer) *PeerState { LastCommitRound: -1, CatchupCommitRound: -1, }, + stats: &peerStateStats{}, } } @@ -1055,6 +1079,43 @@ func (ps *PeerState) ensureVoteBitArrays(height int64, numValidators int) { } } +// RecordVote updates internal statistics for this peer by recording the vote. +// It returns the total number of votes (1 per block). This essentially means +// the number of blocks for which peer has been sending us votes. +func (ps *PeerState) RecordVote(vote *types.Vote) int { + if ps.stats.lastVoteHeight >= vote.Height { + return ps.stats.votes + } + ps.stats.lastVoteHeight = vote.Height + ps.stats.votes += 1 + return ps.stats.votes +} + +// VotesSent returns the number of blocks for which peer has been sending us +// votes. +func (ps *PeerState) VotesSent() int { + return ps.stats.votes +} + +// RecordVote updates internal statistics for this peer by recording the block part. +// It returns the total number of block parts (1 per block). This essentially means +// the number of blocks for which peer has been sending us block parts. +func (ps *PeerState) RecordBlockPart(bp *BlockPartMessage) int { + if ps.stats.lastBlockPartHeight >= bp.Height { + return ps.stats.blockParts + } + + ps.stats.lastBlockPartHeight = bp.Height + ps.stats.blockParts += 1 + return ps.stats.blockParts +} + +// BlockPartsSent returns the number of blocks for which peer has been sending +// us block parts. +func (ps *PeerState) BlockPartsSent() int { + return ps.stats.blockParts +} + // SetHasVote sets the given vote as known by the peer func (ps *PeerState) SetHasVote(vote *types.Vote) { ps.mtx.Lock() @@ -1201,11 +1262,13 @@ func (ps *PeerState) StringIndented(indent string) string { ps.mtx.Lock() defer ps.mtx.Unlock() return fmt.Sprintf(`PeerState{ -%s Key %v -%s PRS %v +%s Key %v +%s PRS %v +%s Stats %v %s}`, indent, ps.Peer.ID(), indent, ps.PeerRoundState.StringIndented(indent+" "), + indent, ps.stats, indent) } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index f66f4e9e5..26fc7e171 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -10,11 +10,14 @@ import ( "testing" "time" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" + wire "github.com/tendermint/tendermint/wire" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/p2p" + p2pdummy "github.com/tendermint/tendermint/p2p/dummy" "github.com/tendermint/tendermint/types" "github.com/stretchr/testify/assert" @@ -121,13 +124,119 @@ func TestReactorProposalHeartbeats(t *testing.T) { }, css) } +// Test we record block parts from other peers +func TestReactorRecordsBlockParts(t *testing.T) { + // create dummy peer + peer := p2pdummy.NewPeer() + ps := NewPeerState(peer).SetLogger(log.TestingLogger()) + peer.Set(types.PeerStateKey, ps) + + // create reactor + css := randConsensusNet(1, "consensus_reactor_records_block_parts_test", newMockTickerFunc(true), newPersistentKVStore) + reactor := NewConsensusReactor(css[0], false) // so we dont start the consensus states + reactor.SetEventBus(css[0].eventBus) + reactor.SetLogger(log.TestingLogger()) + sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) + reactor.SetSwitch(sw) + err := reactor.Start() + require.NoError(t, err) + defer reactor.Stop() + + // 1) new block part + parts := types.NewPartSetFromData(cmn.RandBytes(100), 10) + msg := &BlockPartMessage{ + Height: 2, + Round: 0, + Part: parts.GetPart(0), + } + bz, err := wire.MarshalBinary(struct{ ConsensusMessage }{msg}) + require.NoError(t, err) + + reactor.Receive(DataChannel, peer, bz) + assert.Equal(t, 1, ps.BlockPartsSent(), "number of block parts sent should have increased by 1") + + // 2) block part with the same height, but different round + msg.Round = 1 + + bz, err = wire.MarshalBinary(struct{ ConsensusMessage }{msg}) + require.NoError(t, err) + + reactor.Receive(DataChannel, peer, bz) + assert.Equal(t, 1, ps.BlockPartsSent(), "number of block parts sent should stay the same") + + // 3) block part from earlier height + msg.Height = 1 + msg.Round = 0 + + bz, err = wire.MarshalBinary(struct{ ConsensusMessage }{msg}) + require.NoError(t, err) + + reactor.Receive(DataChannel, peer, bz) + assert.Equal(t, 1, ps.BlockPartsSent(), "number of block parts sent should stay the same") +} + +// Test we record votes from other peers +func TestReactorRecordsVotes(t *testing.T) { + // create dummy peer + peer := p2pdummy.NewPeer() + ps := NewPeerState(peer).SetLogger(log.TestingLogger()) + peer.Set(types.PeerStateKey, ps) + + // create reactor + css := randConsensusNet(1, "consensus_reactor_records_votes_test", newMockTickerFunc(true), newPersistentKVStore) + reactor := NewConsensusReactor(css[0], false) // so we dont start the consensus states + reactor.SetEventBus(css[0].eventBus) + reactor.SetLogger(log.TestingLogger()) + sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) + reactor.SetSwitch(sw) + err := reactor.Start() + require.NoError(t, err) + defer reactor.Stop() + _, val := css[0].state.Validators.GetByIndex(0) + + // 1) new vote + vote := &types.Vote{ + ValidatorIndex: 0, + ValidatorAddress: val.Address, + Height: 2, + Round: 0, + Timestamp: time.Now().UTC(), + Type: types.VoteTypePrevote, + BlockID: types.BlockID{}, + } + bz, err := wire.MarshalBinary(struct{ ConsensusMessage }{&VoteMessage{vote}}) + require.NoError(t, err) + + reactor.Receive(VoteChannel, peer, bz) + assert.Equal(t, 1, ps.VotesSent(), "number of votes sent should have increased by 1") + + // 2) vote with the same height, but different round + vote.Round = 1 + + bz, err = wire.MarshalBinary(struct{ ConsensusMessage }{&VoteMessage{vote}}) + require.NoError(t, err) + + reactor.Receive(VoteChannel, peer, bz) + assert.Equal(t, 1, ps.VotesSent(), "number of votes sent should stay the same") + + // 3) vote from earlier height + vote.Height = 1 + vote.Round = 0 + + bz, err = wire.MarshalBinary(struct{ ConsensusMessage }{&VoteMessage{vote}}) + require.NoError(t, err) + + reactor.Receive(VoteChannel, peer, bz) + assert.Equal(t, 1, ps.VotesSent(), "number of votes sent should stay the same") +} + //------------------------------------------------------------- // ensure we can make blocks despite cycling a validator set func TestReactorVotingPowerChange(t *testing.T) { nVals := 4 logger := log.TestingLogger() - css := randConsensusNet(nVals, "consensus_voting_power_changes_test", newMockTickerFunc(true), newPersistentDummy) + css := randConsensusNet(nVals, "consensus_voting_power_changes_test", newMockTickerFunc(true), newPersistentKVStore) reactors, eventChans, eventBuses := startConsensusNet(t, css, nVals) defer stopConsensusNet(logger, reactors, eventBuses) @@ -146,7 +255,7 @@ func TestReactorVotingPowerChange(t *testing.T) { logger.Debug("---------------------------- Testing changing the voting power of one validator a few times") val1PubKey := css[0].privValidator.GetPubKey() - updateValidatorTx := dummy.MakeValSetChangeTx(val1PubKey.Bytes(), 25) + updateValidatorTx := kvstore.MakeValSetChangeTx(val1PubKey.Bytes(), 25) previousTotalVotingPower := css[0].GetRoundState().LastValidators.TotalVotingPower() waitForAndValidateBlock(t, nVals, activeVals, eventChans, css, updateValidatorTx) @@ -158,7 +267,7 @@ func TestReactorVotingPowerChange(t *testing.T) { t.Fatalf("expected voting power to change (before: %d, after: %d)", previousTotalVotingPower, css[0].GetRoundState().LastValidators.TotalVotingPower()) } - updateValidatorTx = dummy.MakeValSetChangeTx(val1PubKey.Bytes(), 2) + updateValidatorTx = kvstore.MakeValSetChangeTx(val1PubKey.Bytes(), 2) previousTotalVotingPower = css[0].GetRoundState().LastValidators.TotalVotingPower() waitForAndValidateBlock(t, nVals, activeVals, eventChans, css, updateValidatorTx) @@ -170,7 +279,7 @@ func TestReactorVotingPowerChange(t *testing.T) { t.Fatalf("expected voting power to change (before: %d, after: %d)", previousTotalVotingPower, css[0].GetRoundState().LastValidators.TotalVotingPower()) } - updateValidatorTx = dummy.MakeValSetChangeTx(val1PubKey.Bytes(), 26) + updateValidatorTx = kvstore.MakeValSetChangeTx(val1PubKey.Bytes(), 26) previousTotalVotingPower = css[0].GetRoundState().LastValidators.TotalVotingPower() waitForAndValidateBlock(t, nVals, activeVals, eventChans, css, updateValidatorTx) @@ -186,7 +295,7 @@ func TestReactorVotingPowerChange(t *testing.T) { func TestReactorValidatorSetChanges(t *testing.T) { nPeers := 7 nVals := 4 - css := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentDummy) + css := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentKVStore) logger := log.TestingLogger() @@ -208,7 +317,7 @@ func TestReactorValidatorSetChanges(t *testing.T) { logger.Info("---------------------------- Testing adding one validator") newValidatorPubKey1 := css[nVals].privValidator.GetPubKey() - newValidatorTx1 := dummy.MakeValSetChangeTx(newValidatorPubKey1.Bytes(), testMinPower) + newValidatorTx1 := kvstore.MakeValSetChangeTx(newValidatorPubKey1.Bytes(), testMinPower) // wait till everyone makes block 2 // ensure the commit includes all validators @@ -234,7 +343,7 @@ func TestReactorValidatorSetChanges(t *testing.T) { logger.Info("---------------------------- Testing changing the voting power of one validator") updateValidatorPubKey1 := css[nVals].privValidator.GetPubKey() - updateValidatorTx1 := dummy.MakeValSetChangeTx(updateValidatorPubKey1.Bytes(), 25) + updateValidatorTx1 := kvstore.MakeValSetChangeTx(updateValidatorPubKey1.Bytes(), 25) previousTotalVotingPower := css[nVals].GetRoundState().LastValidators.TotalVotingPower() waitForAndValidateBlock(t, nPeers, activeVals, eventChans, css, updateValidatorTx1) @@ -250,10 +359,10 @@ func TestReactorValidatorSetChanges(t *testing.T) { logger.Info("---------------------------- Testing adding two validators at once") newValidatorPubKey2 := css[nVals+1].privValidator.GetPubKey() - newValidatorTx2 := dummy.MakeValSetChangeTx(newValidatorPubKey2.Bytes(), testMinPower) + newValidatorTx2 := kvstore.MakeValSetChangeTx(newValidatorPubKey2.Bytes(), testMinPower) newValidatorPubKey3 := css[nVals+2].privValidator.GetPubKey() - newValidatorTx3 := dummy.MakeValSetChangeTx(newValidatorPubKey3.Bytes(), testMinPower) + newValidatorTx3 := kvstore.MakeValSetChangeTx(newValidatorPubKey3.Bytes(), testMinPower) waitForAndValidateBlock(t, nPeers, activeVals, eventChans, css, newValidatorTx2, newValidatorTx3) waitForAndValidateBlockWithTx(t, nPeers, activeVals, eventChans, css, newValidatorTx2, newValidatorTx3) @@ -265,8 +374,8 @@ func TestReactorValidatorSetChanges(t *testing.T) { //--------------------------------------------------------------------------- logger.Info("---------------------------- Testing removing two validators at once") - removeValidatorTx2 := dummy.MakeValSetChangeTx(newValidatorPubKey2.Bytes(), 0) - removeValidatorTx3 := dummy.MakeValSetChangeTx(newValidatorPubKey3.Bytes(), 0) + removeValidatorTx2 := kvstore.MakeValSetChangeTx(newValidatorPubKey2.Bytes(), 0) + removeValidatorTx3 := kvstore.MakeValSetChangeTx(newValidatorPubKey3.Bytes(), 0) waitForAndValidateBlock(t, nPeers, activeVals, eventChans, css, removeValidatorTx2, removeValidatorTx3) waitForAndValidateBlockWithTx(t, nPeers, activeVals, eventChans, css, removeValidatorTx2, removeValidatorTx3) diff --git a/consensus/replay.go b/consensus/replay.go index 2e6b36a22..39dd592a4 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -2,6 +2,7 @@ package consensus import ( "bytes" + "encoding/json" "fmt" "hash/crc32" "io" @@ -190,13 +191,23 @@ type Handshaker struct { stateDB dbm.DB initialState sm.State store types.BlockStore + appState json.RawMessage logger log.Logger nBlocks int // number of blocks applied to the state } -func NewHandshaker(stateDB dbm.DB, state sm.State, store types.BlockStore) *Handshaker { - return &Handshaker{stateDB, state, store, log.NewNopLogger(), 0} +func NewHandshaker(stateDB dbm.DB, state sm.State, + store types.BlockStore, appState json.RawMessage) *Handshaker { + + return &Handshaker{ + stateDB: stateDB, + initialState: state, + store: store, + appState: appState, + logger: log.NewNopLogger(), + nBlocks: 0, + } } func (h *Handshaker) SetLogger(l log.Logger) { @@ -249,9 +260,12 @@ func (h *Handshaker) ReplayBlocks(state sm.State, appHash []byte, appBlockHeight // If appBlockHeight == 0 it means that we are at genesis and hence should send InitChain if appBlockHeight == 0 { validators := types.TM2PB.Validators(state.Validators) - // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) - var genesisBytes []byte - if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { + req := abci.RequestInitChain{ + Validators: validators, + AppStateBytes: h.appState, + } + _, err := proxyApp.Consensus().InitChainSync(req) + if err != nil { return nil, err } } diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 979425f87..2fcd9b472 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -287,14 +287,19 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo // Get State stateDB := dbm.NewDB("state", dbType, config.DBDir()) - state, err := sm.MakeGenesisStateFromFile(config.GenesisFile()) + gdoc, err := sm.MakeGenesisDocFromFile(config.GenesisFile()) + if err != nil { + cmn.Exit(err.Error()) + } + state, err := sm.MakeGenesisState(gdoc) if err != nil { cmn.Exit(err.Error()) } // Create proxyAppConn connection (consensus, mempool, query) clientCreator := proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()) - proxyApp := proxy.NewAppConns(clientCreator, NewHandshaker(stateDB, state, blockStore)) + proxyApp := proxy.NewAppConns(clientCreator, + NewHandshaker(stateDB, state, blockStore, gdoc.AppState())) err = proxyApp.Start() if err != nil { cmn.Exit(cmn.Fmt("Error starting proxy app conns: %v", err)) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 31fb9908f..9d7bbc8e4 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" abci "github.com/tendermint/abci/types" crypto "github.com/tendermint/go-crypto" wire "github.com/tendermint/go-wire" @@ -55,7 +55,7 @@ func startNewConsensusStateAndWaitForBlock(t *testing.T, lastBlockHeight int64, logger := log.TestingLogger() state, _ := sm.LoadStateFromDBOrGenesisFile(stateDB, consensusReplayConfig.GenesisFile()) privValidator := loadPrivValidator(consensusReplayConfig) - cs := newConsensusStateWithConfigAndBlockStore(consensusReplayConfig, state, privValidator, dummy.NewDummyApplication(), blockDB) + cs := newConsensusStateWithConfigAndBlockStore(consensusReplayConfig, state, privValidator, kvstore.NewKVStoreApplication(), blockDB) cs.SetLogger(logger) bytes, _ := ioutil.ReadFile(cs.config.WalFile()) @@ -141,7 +141,7 @@ LOOP: state, _ := sm.MakeGenesisStateFromFile(consensusReplayConfig.GenesisFile()) privValidator := loadPrivValidator(consensusReplayConfig) blockDB := dbm.NewMemDB() - cs := newConsensusStateWithConfigAndBlockStore(consensusReplayConfig, state, privValidator, dummy.NewDummyApplication(), blockDB) + cs := newConsensusStateWithConfigAndBlockStore(consensusReplayConfig, state, privValidator, kvstore.NewKVStoreApplication(), blockDB) cs.SetLogger(logger) // start sending transactions @@ -351,8 +351,8 @@ func testHandshakeReplay(t *testing.T, nBlocks int, mode uint) { latestAppHash := state.AppHash // make a new client creator - dummyApp := dummy.NewPersistentDummyApplication(path.Join(config.DBDir(), "2")) - clientCreator2 := proxy.NewLocalClientCreator(dummyApp) + kvstoreApp := kvstore.NewPersistentKVStoreApplication(path.Join(config.DBDir(), "2")) + clientCreator2 := proxy.NewLocalClientCreator(kvstoreApp) if nBlocks > 0 { // run nBlocks against a new client to build up the app state. // use a throwaway tendermint state @@ -362,7 +362,7 @@ func testHandshakeReplay(t *testing.T, nBlocks int, mode uint) { } // now start the app using the handshake - it should sync - handshaker := NewHandshaker(stateDB, state, store) + handshaker := NewHandshaker(stateDB, state, store, nil) proxyApp := proxy.NewAppConns(clientCreator2, handshaker) if err := proxyApp.Start(); err != nil { t.Fatalf("Error starting proxy app connections: %v", err) @@ -412,9 +412,9 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, } defer proxyApp.Stop() - validators := types.TM2PB.Validators(state.Validators) // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) var genesisBytes []byte + validators := types.TM2PB.Validators(state.Validators) if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { panic(err) } @@ -432,7 +432,7 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, } if mode == 2 { - // update the dummy height and apphash + // update the kvstore height and apphash // as if we ran commit but not state = applyBlock(stateDB, state, chain[nBlocks-1], proxyApp) } @@ -442,16 +442,16 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, chain []*types.Block, mode uint) sm.State { // run the whole chain against this client to build up the tendermint state - clientCreator := proxy.NewLocalClientCreator(dummy.NewPersistentDummyApplication(path.Join(config.DBDir(), "1"))) + clientCreator := proxy.NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(path.Join(config.DBDir(), "1"))) proxyApp := proxy.NewAppConns(clientCreator, nil) // sm.NewHandshaker(config, state, store, ReplayLastBlock)) if err := proxyApp.Start(); err != nil { panic(err) } defer proxyApp.Stop() - validators := types.TM2PB.Validators(state.Validators) // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) var genesisBytes []byte + validators := types.TM2PB.Validators(state.Validators) if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { panic(err) } diff --git a/consensus/state.go b/consensus/state.go index aa334fdd0..b59baf1ee 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -477,6 +477,9 @@ func (cs *ConsensusState) updateToState(state sm.State) { cs.LockedRound = 0 cs.LockedBlock = nil cs.LockedBlockParts = nil + cs.ValidRound = 0 + cs.ValidBlock = nil + cs.ValidBlockParts = nil cs.Votes = cstypes.NewHeightVoteSet(state.ChainID, height, validators) cs.CommitRound = -1 cs.LastCommit = lastPrecommits @@ -580,6 +583,10 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { err := cs.tryAddVote(msg.Vote, peerID) if err == ErrAddingVote { // TODO: punish peer + // We probably don't want to stop the peer here. The vote does not + // necessarily comes from a malicious peer but can be just broadcasted by + // a typical peer. + // https://github.com/tendermint/tendermint/issues/1281 } // NOTE: the vote is broadcast to peers by the reactor listening @@ -798,6 +805,9 @@ func (cs *ConsensusState) defaultDecideProposal(height int64, round int) { if cs.LockedBlock != nil { // If we're locked onto a block, just choose that. block, blockParts = cs.LockedBlock, cs.LockedBlockParts + } else if cs.ValidBlock != nil { + // If there is valid block, choose that. + block, blockParts = cs.ValidBlock, cs.ValidBlockParts } else { // Create a new proposal block from state/txs from the mempool. block, blockParts = cs.createProposalBlock() @@ -1263,7 +1273,7 @@ func (cs *ConsensusState) defaultSetProposal(proposal *types.Proposal) error { } // Verify signature - if !cs.Validators.GetProposer().PubKey.VerifyBytes(types.SignBytes(cs.state.ChainID, proposal), proposal.Signature) { + if !cs.Validators.GetProposer().PubKey.VerifyBytes(proposal.SignBytes(cs.state.ChainID), proposal.Signature) { return ErrInvalidProposalSignature } @@ -1349,99 +1359,115 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, return added, ErrVoteHeightMismatch } added, err = cs.LastCommit.AddVote(vote) - if added { - cs.Logger.Info(cmn.Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) - cs.eventBus.PublishEventVote(types.EventDataVote{vote}) - - // if we can skip timeoutCommit and have all the votes now, - if cs.config.SkipTimeoutCommit && cs.LastCommit.HasAll() { - // go straight to new round (skip timeout commit) - // cs.scheduleTimeout(time.Duration(0), cs.Height, 0, cstypes.RoundStepNewHeight) - cs.enterNewRound(cs.Height, 0) - } + if !added { + return added, err + } + + cs.Logger.Info(cmn.Fmt("Added to lastPrecommits: %v", cs.LastCommit.StringShort())) + cs.eventBus.PublishEventVote(types.EventDataVote{vote}) + + // if we can skip timeoutCommit and have all the votes now, + if cs.config.SkipTimeoutCommit && cs.LastCommit.HasAll() { + // go straight to new round (skip timeout commit) + // cs.scheduleTimeout(time.Duration(0), cs.Height, 0, cstypes.RoundStepNewHeight) + cs.enterNewRound(cs.Height, 0) } return } - // A prevote/precommit for this height? - if vote.Height == cs.Height { - height := cs.Height - added, err = cs.Votes.AddVote(vote, peerID) - if added { - cs.eventBus.PublishEventVote(types.EventDataVote{vote}) - - switch vote.Type { - case types.VoteTypePrevote: - prevotes := cs.Votes.Prevotes(vote.Round) - cs.Logger.Info("Added to prevote", "vote", vote, "prevotes", prevotes.StringShort()) - // First, unlock if prevotes is a valid POL. - // >> lockRound < POLRound <= unlockOrChangeLockRound (see spec) - // NOTE: If (lockRound < POLRound) but !(POLRound <= unlockOrChangeLockRound), - // we'll still enterNewRound(H,vote.R) and enterPrecommit(H,vote.R) to process it - // there. - if (cs.LockedBlock != nil) && (cs.LockedRound < vote.Round) && (vote.Round <= cs.Round) { - blockID, ok := prevotes.TwoThirdsMajority() - if ok && !cs.LockedBlock.HashesTo(blockID.Hash) { - cs.Logger.Info("Unlocking because of POL.", "lockedRound", cs.LockedRound, "POLRound", vote.Round) - cs.LockedRound = 0 - cs.LockedBlock = nil - cs.LockedBlockParts = nil - cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()) - } - } - if cs.Round <= vote.Round && prevotes.HasTwoThirdsAny() { - // Round-skip over to PrevoteWait or goto Precommit. - cs.enterNewRound(height, vote.Round) // if the vote is ahead of us - if prevotes.HasTwoThirdsMajority() { - cs.enterPrecommit(height, vote.Round) - } else { - cs.enterPrevote(height, vote.Round) // if the vote is ahead of us - cs.enterPrevoteWait(height, vote.Round) - } - } else if cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round { - // If the proposal is now complete, enter prevote of cs.Round. - if cs.isProposalComplete() { - cs.enterPrevote(height, cs.Round) - } - } - case types.VoteTypePrecommit: - precommits := cs.Votes.Precommits(vote.Round) - cs.Logger.Info("Added to precommit", "vote", vote, "precommits", precommits.StringShort()) - blockID, ok := precommits.TwoThirdsMajority() - if ok { - if len(blockID.Hash) == 0 { - cs.enterNewRound(height, vote.Round+1) - } else { - cs.enterNewRound(height, vote.Round) - cs.enterPrecommit(height, vote.Round) - cs.enterCommit(height, vote.Round) - - if cs.config.SkipTimeoutCommit && precommits.HasAll() { - // if we have all the votes now, - // go straight to new round (skip timeout commit) - // cs.scheduleTimeout(time.Duration(0), cs.Height, 0, cstypes.RoundStepNewHeight) - cs.enterNewRound(cs.Height, 0) - } - - } - } else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() { - cs.enterNewRound(height, vote.Round) - cs.enterPrecommit(height, vote.Round) - cs.enterPrecommitWait(height, vote.Round) + // Height mismatch is ignored. + // Not necessarily a bad peer, but not favourable behaviour. + if vote.Height != cs.Height { + err = ErrVoteHeightMismatch + cs.Logger.Info("Vote ignored and not added", "voteHeight", vote.Height, "csHeight", cs.Height, "err", err) + return + } + + height := cs.Height + added, err = cs.Votes.AddVote(vote, peerID) + if !added { + // Either duplicate, or error upon cs.Votes.AddByIndex() + return + } + + cs.eventBus.PublishEventVote(types.EventDataVote{vote}) + + switch vote.Type { + case types.VoteTypePrevote: + prevotes := cs.Votes.Prevotes(vote.Round) + cs.Logger.Info("Added to prevote", "vote", vote, "prevotes", prevotes.StringShort()) + blockID, ok := prevotes.TwoThirdsMajority() + // First, unlock if prevotes is a valid POL. + // >> lockRound < POLRound <= unlockOrChangeLockRound (see spec) + // NOTE: If (lockRound < POLRound) but !(POLRound <= unlockOrChangeLockRound), + // we'll still enterNewRound(H,vote.R) and enterPrecommit(H,vote.R) to process it + // there. + if (cs.LockedBlock != nil) && (cs.LockedRound < vote.Round) && (vote.Round <= cs.Round) { + if ok && !cs.LockedBlock.HashesTo(blockID.Hash) { + cs.Logger.Info("Unlocking because of POL.", "lockedRound", cs.LockedRound, "POLRound", vote.Round) + cs.LockedRound = 0 + cs.LockedBlock = nil + cs.LockedBlockParts = nil + cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()) + } + } + // Update ValidBlock + if ok && !blockID.IsZero() && !cs.ValidBlock.HashesTo(blockID.Hash) && vote.Round > cs.ValidRound { + // update valid value + if cs.ProposalBlock.HashesTo(blockID.Hash) { + cs.ValidRound = vote.Round + cs.ValidBlock = cs.ProposalBlock + cs.ValidBlockParts = cs.ProposalBlockParts + } + //TODO: We might want to update ValidBlock also in case we don't have that block yet, + // and obtain the required block using gossiping + } + + if cs.Round <= vote.Round && prevotes.HasTwoThirdsAny() { + // Round-skip over to PrevoteWait or goto Precommit. + cs.enterNewRound(height, vote.Round) // if the vote is ahead of us + if prevotes.HasTwoThirdsMajority() { + cs.enterPrecommit(height, vote.Round) + } else { + cs.enterPrevote(height, vote.Round) // if the vote is ahead of us + cs.enterPrevoteWait(height, vote.Round) + } + } else if cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round { + // If the proposal is now complete, enter prevote of cs.Round. + if cs.isProposalComplete() { + cs.enterPrevote(height, cs.Round) + } + } + case types.VoteTypePrecommit: + precommits := cs.Votes.Precommits(vote.Round) + cs.Logger.Info("Added to precommit", "vote", vote, "precommits", precommits.StringShort()) + blockID, ok := precommits.TwoThirdsMajority() + if ok { + if len(blockID.Hash) == 0 { + cs.enterNewRound(height, vote.Round+1) + } else { + cs.enterNewRound(height, vote.Round) + cs.enterPrecommit(height, vote.Round) + cs.enterCommit(height, vote.Round) + + if cs.config.SkipTimeoutCommit && precommits.HasAll() { + // if we have all the votes now, + // go straight to new round (skip timeout commit) + // cs.scheduleTimeout(time.Duration(0), cs.Height, 0, cstypes.RoundStepNewHeight) + cs.enterNewRound(cs.Height, 0) } - default: - cmn.PanicSanity(cmn.Fmt("Unexpected vote type %X", vote.Type)) // Should not happen. + } + } else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() { + cs.enterNewRound(height, vote.Round) + cs.enterPrecommit(height, vote.Round) + cs.enterPrecommitWait(height, vote.Round) } - // Either duplicate, or error upon cs.Votes.AddByIndex() - return - } else { - err = ErrVoteHeightMismatch + default: + panic(cmn.Fmt("Unexpected vote type %X", vote.Type)) // go-wire should prevent this. } - // Height mismatch, bad peer? - cs.Logger.Info("Vote ignored and not added", "voteHeight", vote.Height, "csHeight", cs.Height, "err", err) return } diff --git a/consensus/types/height_vote_set.go b/consensus/types/height_vote_set.go index 17ef334db..a155bce08 100644 --- a/consensus/types/height_vote_set.go +++ b/consensus/types/height_vote_set.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "strings" "sync" @@ -15,6 +16,10 @@ type RoundVoteSet struct { Precommits *types.VoteSet } +var ( + GotVoteFromUnwantedRoundError = errors.New("Peer has sent a vote that does not match our round for more than one round") +) + /* Keeps track of all VoteSets from round 0 to round 'round'. @@ -117,10 +122,8 @@ func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, voteSet = hvs.getVoteSet(vote.Round, vote.Type) hvs.peerCatchupRounds[peerID] = append(rndz, vote.Round) } else { - // Peer has sent a vote that does not match our round, - // for more than one round. Bad peer! - // TODO punish peer. - // log.Warn("Deal with peer giving votes from unwanted rounds") + // punish peer + err = GotVoteFromUnwantedRoundError return } } @@ -218,5 +221,5 @@ func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID p2p.ID, blo if voteSet == nil { return nil // something we don't know about yet } - return voteSet.SetPeerMaj23(peerID, blockID) + return voteSet.SetPeerMaj23(types.P2PID(peerID), blockID) } diff --git a/consensus/types/height_vote_set_test.go b/consensus/types/height_vote_set_test.go index 5719d7eea..246c0b711 100644 --- a/consensus/types/height_vote_set_test.go +++ b/consensus/types/height_vote_set_test.go @@ -34,8 +34,8 @@ func TestPeerCatchupRounds(t *testing.T) { vote1001_0 := makeVoteHR(t, 1, 1001, privVals, 0) added, err = hvs.AddVote(vote1001_0, "peer1") - if err != nil { - t.Error("AddVote error", err) + if err != GotVoteFromUnwantedRoundError { + t.Errorf("Expected GotVoteFromUnwantedRoundError, but got %v", err) } if added { t.Error("Expected to *not* add vote from peer, too many catchup rounds.") diff --git a/consensus/types/state.go b/consensus/types/state.go index b95131f4e..8e79f10d2 100644 --- a/consensus/types/state.go +++ b/consensus/types/state.go @@ -70,6 +70,9 @@ type RoundState struct { LockedRound int LockedBlock *types.Block LockedBlockParts *types.PartSet + ValidRound int + ValidBlock *types.Block + ValidBlockParts *types.PartSet Votes *HeightVoteSet CommitRound int // LastCommit *types.VoteSet // Last precommits at Height-1 @@ -106,6 +109,8 @@ func (rs *RoundState) StringIndented(indent string) string { %s ProposalBlock: %v %v %s LockedRound: %v %s LockedBlock: %v %v +%s ValidRound: %v +%s ValidBlock: %v %v %s Votes: %v %s LastCommit: %v %s LastValidators:%v @@ -118,6 +123,8 @@ func (rs *RoundState) StringIndented(indent string) string { indent, rs.ProposalBlockParts.StringShort(), rs.ProposalBlock.StringShort(), indent, rs.LockedRound, indent, rs.LockedBlockParts.StringShort(), rs.LockedBlock.StringShort(), + indent, rs.ValidRound, + indent, rs.ValidBlockParts.StringShort(), rs.ValidBlock.StringShort(), indent, rs.Votes.StringIndented(indent+" "), indent, rs.LastCommit.StringShort(), indent, rs.LastValidators.StringIndented(indent+" "), diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index 45609e568..de41d4018 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -11,7 +11,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/proxy" @@ -25,13 +25,13 @@ import ( // WALWithNBlocks generates a consensus WAL. It does this by spining up a // stripped down version of node (proxy app, event bus, consensus state) with a -// persistent dummy application and special consensus wal instance +// persistent kvstore application and special consensus wal instance // (byteBufferWAL) and waits until numBlocks are created. Then it returns a WAL // content. func WALWithNBlocks(numBlocks int) (data []byte, err error) { config := getConfig() - app := dummy.NewPersistentDummyApplication(filepath.Join(config.DBDir(), "wal_generator")) + app := kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "wal_generator")) logger := log.TestingLogger().With("wal_generator", "wal_generator") logger.Info("generating WAL (last height msg excluded)", "numBlocks", numBlocks) @@ -52,7 +52,7 @@ func WALWithNBlocks(numBlocks int) (data []byte, err error) { return nil, errors.Wrap(err, "failed to make genesis state") } blockStore := bc.NewBlockStore(blockStoreDB) - handshaker := NewHandshaker(stateDB, state, blockStore) + handshaker := NewHandshaker(stateDB, state, blockStore, genDoc.AppState()) proxyApp := proxy.NewAppConns(proxy.NewLocalClientCreator(app), handshaker) proxyApp.SetLogger(logger.With("module", "proxy")) if err := proxyApp.Start(); err != nil { diff --git a/docs/abci-cli.rst b/docs/abci-cli.rst index efbeb71be..4cdd9b234 100644 --- a/docs/abci-cli.rst +++ b/docs/abci-cli.rst @@ -16,15 +16,15 @@ Next, install the ``abci-cli`` tool and example applications: go get -u github.com/tendermint/abci/cmd/abci-cli -If this fails, you may need to use ``glide`` to get vendored +If this fails, you may need to use `dep `__ to get vendored dependencies: :: - go get github.com/Masterminds/glide cd $GOPATH/src/github.com/tendermint/abci - glide install - go install ./cmd/abci-cli + make get_tools + make get_vendor_deps + make install Now run ``abci-cli`` to see the list of commands: @@ -40,7 +40,7 @@ Now run ``abci-cli`` to see the list of commands: console Start an interactive abci console for multiple commands counter ABCI demo example deliver_tx Deliver a new tx to the application - dummy ABCI demo example + kvstore ABCI demo example echo Have the application echo a message help Help about any command info Get some info about the application @@ -56,8 +56,8 @@ Now run ``abci-cli`` to see the list of commands: Use "abci-cli [command] --help" for more information about a command. -Dummy - First Example ---------------------- +KVStore - First Example +----------------------- The ``abci-cli`` tool lets us send ABCI messages to our application, to help build and debug them. @@ -66,8 +66,8 @@ The most important messages are ``deliver_tx``, ``check_tx``, and ``commit``, but there are others for convenience, configuration, and information purposes. -We'll start a dummy application, which was installed at the same time as -``abci-cli`` above. The dummy just stores transactions in a merkle tree. +We'll start a kvstore application, which was installed at the same time as +``abci-cli`` above. The kvstore just stores transactions in a merkle tree. Its code can be found `here `__ and looks like: @@ -75,20 +75,20 @@ Its code can be found `here `__ and looks like: +Like the kvstore app, its code can be found `here `__ and looks like: .. container:: toggle @@ -288,7 +288,7 @@ other peers. In this instance of the counter app, ``check_tx`` only allows transactions whose integer is greater than the last committed one. -Let's kill the console and the dummy application, and start the counter +Let's kill the console and the kvstore application, and start the counter app: :: @@ -328,7 +328,7 @@ In another window, start the ``abci-cli console``: -> data.hex: 0x7B22686173686573223A302C22747873223A327D This is a very simple application, but between ``counter`` and -``dummy``, its easy to see how you can build out arbitrary application +``kvstore``, its easy to see how you can build out arbitrary application states on top of the ABCI. `Hyperledger's Burrow `__ also runs atop ABCI, bringing with it Ethereum-like accounts, the Ethereum virtual-machine, diff --git a/docs/app-development.rst b/docs/app-development.rst index 18477eda7..42083f742 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -142,10 +142,10 @@ It is unlikely that you will need to implement a client. For details of our client, see `here `__. -Most of the examples below are from `dummy application -`__, -which is a part of the abci repo. `persistent_dummy application -`__ +Most of the examples below are from `kvstore application +`__, +which is a part of the abci repo. `persistent_kvstore application +`__ is used to show ``BeginBlock``, ``EndBlock`` and ``InitChain`` example implementations. @@ -202,7 +202,7 @@ mempool state. .. code-block:: go - func (app *DummyApplication) CheckTx(tx []byte) types.Result { + func (app *KVStoreApplication) CheckTx(tx []byte) types.Result { return types.OK } @@ -263,7 +263,7 @@ merkle root of the data returned by the DeliverTx requests, or both. .. code-block:: go // tx is either "key=value" or just arbitrary bytes - func (app *DummyApplication) DeliverTx(tx []byte) types.Result { + func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { parts := strings.Split(string(tx), "=") if len(parts) == 2 { app.state.Set([]byte(parts[0]), []byte(parts[1])) @@ -327,7 +327,7 @@ job of the `Handshake <#handshake>`__. .. code-block:: go - func (app *DummyApplication) Commit() types.Result { + func (app *KVStoreApplication) Commit() types.Result { hash := app.state.Hash() return types.NewResultOK(hash, "") } @@ -369,7 +369,7 @@ pick up from when it restarts. See information on the Handshake, below. .. code-block:: go // Track the block hash and header information - func (app *PersistentDummyApplication) BeginBlock(params types.RequestBeginBlock) { + func (app *PersistentKVStoreApplication) BeginBlock(params types.RequestBeginBlock) { // update latest block info app.blockHeader = params.Header @@ -423,7 +423,7 @@ for details on how it tracks validators. .. code-block:: go // Update the validator set - func (app *PersistentDummyApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { + func (app *PersistentKVStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} } @@ -477,7 +477,7 @@ Note: these query formats are subject to change! .. code-block:: go - func (app *DummyApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { if reqQuery.Prove { value, proof, exists := app.state.Proof(reqQuery.Data) resQuery.Index = -1 // TODO make Proof return index @@ -561,7 +561,7 @@ all blocks. .. code-block:: go - func (app *DummyApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) { + func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) { return types.ResponseInfo{Data: cmn.Fmt("{\"size\":%v}", app.state.Size())} } @@ -595,7 +595,7 @@ consensus params. .. code-block:: go // Save the validators in the merkle tree - func (app *PersistentDummyApplication) InitChain(params types.RequestInitChain) { + func (app *PersistentKVStoreApplication) InitChain(params types.RequestInitChain) { for _, v := range params.Validators { r := app.updateValidator(v) if r.IsErr() { diff --git a/docs/architecture/adr-008-priv-validator.md b/docs/architecture/adr-008-priv-validator.md new file mode 100644 index 000000000..0861e9415 --- /dev/null +++ b/docs/architecture/adr-008-priv-validator.md @@ -0,0 +1,128 @@ +# ADR 008: PrivValidator + +## Context + +The current PrivValidator is monolithic and isn't easily reuseable by alternative signers. + +For instance, see https://github.com/tendermint/tendermint/issues/673 + +The goal is to have a clean PrivValidator interface like: + +``` +type PrivValidator interface { + Address() data.Bytes + PubKey() crypto.PubKey + + SignVote(chainID string, vote *types.Vote) error + SignProposal(chainID string, proposal *types.Proposal) error + SignHeartbeat(chainID string, heartbeat *types.Heartbeat) error +} +``` + +It should also be easy to re-use the LastSignedInfo logic to avoid double signing. + +## Decision + +Tendermint node's should support only two in-process PrivValidator implementations: + +- PrivValidatorUnencrypted uses an unencrypted private key in a "priv_validator.json" file - no configuration required (just `tendermint init`). +- PrivValidatorSocket uses a socket to send signing requests to another process - user is responsible for starting that process themselves. + +The PrivValidatorSocket address can be provided via flags at the command line - +doing so will cause Tendermint to ignore any "priv_validator.json" file and to listen +on the given address for incoming connections from an external priv_validator process. +It will halt any operation until at least one external process succesfully +connected. + +The external priv_validator process will dial the address to connect to Tendermint, +and then Tendermint will send requests on the ensuing connection to sign votes and proposals. +Thus the external process initiates the connection, but the Tendermint process makes all requests. +In a later stage we're going to support multiple validators for fault +tolerance. To prevent double signing they need to be synced, which is deferred +to an external solution (see #1185). + +In addition, Tendermint will provide implementations that can be run in that external process. +These include: + +- PrivValidatorEncrypted uses an encrypted private key persisted to disk - user must enter password to decrypt key when process is started. +- PrivValidatorLedger uses a Ledger Nano S to handle all signing. + +What follows are descriptions of useful types + +### Signer + +``` +type Signer interface { + Sign(msg []byte) (crypto.Signature, error) +} +``` + +Signer signs a message. It can also return an error. + +### ValidatorID + + +ValidatorID is just the Address and PubKey + +``` +type ValidatorID struct { + Address data.Bytes `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` +} +``` + +### LastSignedInfo + +LastSignedInfo tracks the last thing we signed: + +``` +type LastSignedInfo struct { + Height int64 `json:"height"` + Round int `json:"round"` + Step int8 `json:"step"` + Signature crypto.Signature `json:"signature,omitempty"` // so we dont lose signatures + SignBytes data.Bytes `json:"signbytes,omitempty"` // so we dont lose signatures +} +``` + +It exposes methods for signing votes and proposals using a `Signer`. + +This allows it to easily be reused by developers implemented their own PrivValidator. + +### PrivValidatorUnencrypted + +``` +type PrivValidatorUnencrypted struct { + ID types.ValidatorID `json:"id"` + PrivKey PrivKey `json:"priv_key"` + LastSignedInfo *LastSignedInfo `json:"last_signed_info"` +} +``` + +Has the same structure as currently, but broken up into sub structs. + +Note the LastSignedInfo is mutated in place every time we sign. + +### PrivValidatorJSON + +The "priv_validator.json" file supports only the PrivValidatorUnencrypted type. + +It unmarshals into PrivValidatorJSON, which is used as the default PrivValidator type. +It wraps the PrivValidatorUnencrypted and persists it to disk after every signature. + +## Status + +Accepted. + +## Consequences + +### Positive + +- Cleaner separation of components enabling re-use. + +### Negative + +- More files - led to creation of new directory. + +### Neutral + diff --git a/docs/conf.py b/docs/conf.py index 92c5e1201..6122e90a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,15 +41,15 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] -# source_suffix = '.rst' +#source_suffix = ['.rst', '.md'] +source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Tendermint' -copyright = u'2017, The Authors' +copyright = u'2018, The Authors' author = u'Tendermint' # The version info for the project you're documenting, acts as replacement for @@ -71,7 +71,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'architecture'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'architecture', 'specification/new-spec', 'examples'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -203,6 +203,6 @@ urllib.urlretrieve(tools_repo+tools_branch+'/tm-bench/README.rst', filename=tool #### abci spec ################################# abci_repo = "https://raw.githubusercontent.com/tendermint/abci/" -abci_branch = "spec-docs" +abci_branch = "develop" urllib.urlretrieve(abci_repo+abci_branch+'/specification.rst', filename='abci-spec.rst') diff --git a/docs/determinism.rst b/docs/determinism.rst new file mode 100644 index 000000000..e3fbce3d6 --- /dev/null +++ b/docs/determinism.rst @@ -0,0 +1,8 @@ +On Determinism +============== + +Arguably, the most difficult part of blockchain programming is determinism - that is, ensuring that sources of indeterminism do not creep into the design of such systems. + +See `this issue `__ for more information on the potential sources of indeterminism. + + diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 39e6785ee..cfd3a6e55 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -5,7 +5,7 @@ The growing list of applications built using various pieces of the Tendermint st * https://tendermint.com/ecosystem -We thank the community for their contributions thus far and welcome the addition of new projects. A pull request can be submitted to `this file `__ to include your project. +We thank the community for their contributions thus far and welcome the addition of new projects. A pull request can be submitted to `this file `__ to include your project. Other Tools ----------- diff --git a/docs/examples/getting-started.md b/docs/examples/getting-started.md index 3ae42e27f..1675b55b2 100644 --- a/docs/examples/getting-started.md +++ b/docs/examples/getting-started.md @@ -12,7 +12,7 @@ and want to get started right away, continue. Otherwise, [review the documentati On a fresh Ubuntu 16.04 machine can be done with [this script](https://git.io/vNLfY), like so: ``` -curl -L https://git.io/vNLfY | bash +curl -L https://git.io/vxWlX | bash source ~/.profile ``` @@ -71,7 +71,7 @@ Configuring a cluster is covered further below. Start tendermint with a simple in-process application: ``` -tendermint node --proxy_app=dummy +tendermint node --proxy_app=kvstore ``` and blocks will start to stream in: @@ -89,7 +89,7 @@ curl -s localhost:46657/status ### Sending Transactions -With the dummy app running, we can send transactions: +With the kvstore app running, we can send transactions: ``` curl -s 'localhost:46657/broadcast_tx_commit?tx="abcd"' @@ -131,10 +131,10 @@ This will install `go` and other dependencies, get the Tendermint source code, t Next, `cd` into `docs/examples`. Each command below should be run from each node, in sequence: ``` -tendermint node --home ./node1 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 -tendermint node --home ./node2 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 -tendermint node --home ./node3 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 -tendermint node --home ./node4 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node1 --proxy_app=kvstore --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node2 --proxy_app=kvstore --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node3 --proxy_app=kvstore --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node4 --proxy_app=kvstore --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 ``` Note that after the third node is started, blocks will start to stream in because >2/3 of validators (defined in the `genesis.json`) have come online. Seeds can also be specified in the `config.toml`. See [this PR](https://github.com/tendermint/tendermint/pull/792) for more information about configuration options. diff --git a/docs/examples/install_tendermint.sh b/docs/examples/install_tendermint.sh index ca328dad0..d58b84d04 100644 --- a/docs/examples/install_tendermint.sh +++ b/docs/examples/install_tendermint.sh @@ -4,8 +4,8 @@ # and has only been tested on Digital Ocean # get and unpack golang -curl -O https://storage.googleapis.com/golang/go1.9.2.linux-amd64.tar.gz -tar -xvf go1.9.2.linux-amd64.tar.gz +curl -O https://storage.googleapis.com/golang/go1.10.linux-amd64.tar.gz +tar -xvf go1.10.linux-amd64.tar.gz apt install make @@ -26,7 +26,7 @@ go get $REPO cd $GOPATH/src/$REPO ## build -git checkout v0.15.0 +git checkout v0.17.0 make get_tools make get_vendor_deps -make install \ No newline at end of file +make install diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 94f55c128..fe49e27b5 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -27,38 +27,38 @@ Then run go get -u github.com/tendermint/abci/cmd/abci-cli -If there is an error, install and run the ``glide`` tool to pin the +If there is an error, install and run the `dep `__ tool to pin the dependencies: :: - go get github.com/Masterminds/glide cd $GOPATH/src/github.com/tendermint/abci - glide install - go install ./cmd/abci-cli + make get_tools + make get_vendor_deps + make install Now you should have the ``abci-cli`` installed; you'll see -a couple of commands (``counter`` and ``dummy``) that are +a couple of commands (``counter`` and ``kvstore``) that are example applications written in Go. See below for an application written in JavaScript. Now, let's run some apps! -Dummy - A First Example ------------------------ +KVStore - A First Example +------------------------- -The dummy app is a `Merkle +The kvstore app is a `Merkle tree `__ that just stores all transactions. If the transaction contains an ``=``, e.g. ``key=value``, then the ``value`` is stored under the ``key`` in the Merkle tree. Otherwise, the full transaction bytes are stored as the key and the value. -Let's start a dummy application. +Let's start a kvstore application. :: - abci-cli dummy + abci-cli kvstore In another terminal, we can start Tendermint. If you have never run Tendermint before, use: @@ -85,7 +85,7 @@ The ``-s`` just silences ``curl``. For nicer output, pipe the result into a tool like `jq `__ or `jsonpp `__. -Now let's send some transactions to the dummy. +Now let's send some transactions to the kvstore. :: @@ -192,7 +192,7 @@ In this instance of the counter app, with ``serial=on``, ``CheckTx`` only allows transactions whose integer is greater than the last committed one. -Let's kill the previous instance of ``tendermint`` and the ``dummy`` +Let's kill the previous instance of ``tendermint`` and the ``kvstore`` application, and start the counter app. We can enable ``serial=on`` with a flag: @@ -313,7 +313,7 @@ Neat, eh? Basecoin - A More Interesting Example ------------------------------------- -We saved the best for last; the `Cosmos SDK `__ is a general purpose framework for building cryptocurrencies. Unlike the ``dummy`` and ``counter``, which are strictly for example purposes. The reference implementation of Cosmos SDK is ``basecoin``, which demonstrates how to use the building blocks of the Cosmos SDK. +We saved the best for last; the `Cosmos SDK `__ is a general purpose framework for building cryptocurrencies. Unlike the ``kvstore`` and ``counter``, which are strictly for example purposes. The reference implementation of Cosmos SDK is ``basecoin``, which demonstrates how to use the building blocks of the Cosmos SDK. The default ``basecoin`` application is a multi-asset cryptocurrency that supports inter-blockchain communication (IBC). For more details on how diff --git a/docs/how-to-read-logs.rst b/docs/how-to-read-logs.rst index 890057a2f..c5a352d72 100644 --- a/docs/how-to-read-logs.rst +++ b/docs/how-to-read-logs.rst @@ -5,7 +5,7 @@ Walk through example -------------------- We first create three connections (mempool, consensus and query) to the -application (locally running dummy in this case). +application (running ``kvstore`` locally in this case). :: diff --git a/docs/index.rst b/docs/index.rst index b32ba4841..f2f70d194 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,10 +65,11 @@ Tendermint 201 :maxdepth: 2 specification.rst + determinism.rst * For a deeper dive, see `this thesis `__. * There is also the `original whitepaper `__, though it is now quite outdated. * Readers might also be interested in the `Cosmos Whitepaper `__ which describes Tendermint, ABCI, and how to build a scalable, heterogeneous, cryptocurrency network. * For example applications and related software built by the Tendermint team and other, see the `software ecosystem `__. -Join the `Cosmos and Tendermint Rocket Chat `__ to ask questions and discuss projects. +Join the `community `__ to ask questions and discuss projects. diff --git a/docs/install.rst b/docs/install.rst index 734be1629..4ee572604 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,7 +9,7 @@ To download pre-built binaries, see the `Download page `__, and the Tendermint source code. Install Go ^^^^^^^^^^ @@ -31,21 +31,21 @@ installation worked. If the installation failed, a dependency may have been updated and become incompatible with the latest Tendermint master branch. We solve this -using the ``glide`` tool for dependency management. +using the ``dep`` tool for dependency management. -First, install ``glide``: +First, install ``dep``: :: - go get github.com/Masterminds/glide + make get_tools Now we can fetch the correct versions of each dependency by running: :: cd $GOPATH/src/github.com/tendermint/tendermint - glide install - go install ./cmd/tendermint + make get_vendor_deps + make install Note that even though ``go get`` originally failed, the repository was still cloned to the correct location in the ``$GOPATH``. @@ -60,7 +60,7 @@ If you already have Tendermint installed, and you make updates, simply :: cd $GOPATH/src/github.com/tendermint/tendermint - go install ./cmd/tendermint + make install To upgrade, there are a few options: @@ -72,18 +72,18 @@ To upgrade, there are a few options: its dependencies - fetch and checkout the latest master branch in ``$GOPATH/src/github.com/tendermint/tendermint``, and then run - ``glide install && go install ./cmd/tendermint`` as above. + ``make get_vendor_deps && make install`` as above. Note the first two options should usually work, but may fail. If they -do, use ``glide``, as above: +do, use ``dep``, as above: :: cd $GOPATH/src/github.com/tendermint/tendermint - glide install - go install ./cmd/tendermint + make get_vendor_deps + make install -Since the third option just uses ``glide`` right away, it should always +Since the third option just uses ``dep`` right away, it should always work. Troubleshooting @@ -96,8 +96,8 @@ If ``go get`` failing bothers you, fetch the code using ``git``: mkdir -p $GOPATH/src/github.com/tendermint git clone https://github.com/tendermint/tendermint $GOPATH/src/github.com/tendermint/tendermint cd $GOPATH/src/github.com/tendermint/tendermint - glide install - go install ./cmd/tendermint + make get_vendor_deps + make install Run ^^^ @@ -107,4 +107,4 @@ To start a one-node blockchain with a simple in-process application: :: tendermint init - tendermint node --proxy_app=dummy + tendermint node --proxy_app=kvstore diff --git a/docs/specification.rst b/docs/specification.rst index 3afa9c659..70ebf633f 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -2,7 +2,7 @@ Specification ############# -Here you'll find details of the Tendermint specification. See `the spec repo `__ for upcoming material. Tendermint's types are produced by `godoc `__. +Here you'll find details of the Tendermint specification. Tendermint's types are produced by `godoc `__. .. toctree:: :maxdepth: 2 @@ -10,6 +10,7 @@ Here you'll find details of the Tendermint specification. See `the spec repo `__). -NOTE: This does not (yet) specify the application state (e.g. initial -distribution of tokens). Currently we leave it up to the application to -load the initial application genesis state. In the future, we may -include genesis SetOption messages that get passed from TendermintCore -to the app upon genesis. - Fields ~~~~~~ @@ -26,6 +20,7 @@ Fields - ``app_hash``: The expected application hash (as returned by the ``Commit`` ABCI message) upon genesis. If the app's hash does not match, a warning message is printed. +- ``app_state``: The application state (e.g. initial distribution of tokens). Sample genesis.json ~~~~~~~~~~~~~~~~~~~ @@ -69,5 +64,8 @@ Sample genesis.json "name": "mach4" } ], - "app_hash": "15005165891224E721CB664D15CB972240F5703F" + "app_hash": "15005165891224E721CB664D15CB972240F5703F", + "app_state": { + {"account": "Bob", "coins": 5000} + } } diff --git a/docs/specification/new-spec/README.md b/docs/specification/new-spec/README.md index f3b9e3c29..12f5525c9 100644 --- a/docs/specification/new-spec/README.md +++ b/docs/specification/new-spec/README.md @@ -4,11 +4,6 @@ This is a markdown specification of the Tendermint blockchain. It defines the base data structures, how they are validated, and how they are communicated over the network. -XXX: this spec is a work in progress and not yet complete - see github -[issues](https://github.com/tendermint/tendermint/issues) and -[pull requests](https://github.com/tendermint/tendermint/pulls) -for more details. - If you find discrepancies between the spec and the code that do not have an associated issue or pull request on github, please submit them to our [bug bounty](https://tendermint.com/security)! @@ -24,18 +19,16 @@ please submit them to our [bug bounty](https://tendermint.com/security)! ### P2P and Network Protocols -TODO: update links - -- [The Base P2P Layer](p2p/README.md): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections -- [Peer Exchange (PEX)](pex/README.md): gossip known peer addresses so peers can find each other -- [Block Sync](block_sync/README.md): gossip blocks so peers can catch up quickly -- [Consensus](consensus/README.md): gossip votes and block parts so new blocks can be committed -- [Mempool](mempool/README.md): gossip transactions so they get included in blocks -- [Evidence](evidence/README.md): TODO +- [The Base P2P Layer](p2p): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections +- [Peer Exchange (PEX)](reactors/pex): gossip known peer addresses so peers can find each other +- [Block Sync](reactors/block_sync): gossip blocks so peers can catch up quickly +- [Consensus](reactors/consensus): gossip votes and block parts so new blocks can be committed +- [Mempool](reactors/mempool): gossip transactions so they get included in blocks +- Evidence: TODO ### More -- [Light Client](light_client/README.md): TODO -- [Persistence](persistence/README.md): TODO +- Light Client: TODO +- Persistence: TODO ## Overview diff --git a/docs/specification/new-spec/bft-time.md b/docs/specification/new-spec/bft-time.md index cbb1b65d4..a005e9040 100644 --- a/docs/specification/new-spec/bft-time.md +++ b/docs/specification/new-spec/bft-time.md @@ -5,7 +5,7 @@ Time in Tendermint is defined with the Time field of the block header. It satisfies the following properties: -- Time Monotonicity: Time is monotonically increasing, i.e., given +- Time Monotonicity: Time is monotonically increasing, i.e., given a header H1 for height h1 and a header H2 for height `h2 = h1 + 1`, `H1.Time < H2.Time`. - Time Validity: Given a set of Commit votes that forms the `block.LastCommit` field, a range of valid values for the Time field of the block header is defined only by @@ -16,7 +16,21 @@ In the context of Tendermint, time is of type int64 and denotes UNIX time in mil corresponds to the number of milliseconds since January 1, 1970. Before defining rules that need to be enforced by the Tendermint consensus protocol, so the properties above holds, we introduce the following definition: -- median of a set of `Vote` messages is equal to the median of `Vote.Time` fields of the corresponding `Vote` messages +- median of a set of `Vote` messages is equal to the median of `Vote.Time` fields of the corresponding `Vote` messages, +where the value of `Vote.Time` is counted number of times proportional to the process voting power. As in Tendermint +the voting power is not uniform (one process one vote), a vote message is actually an aggregator of the same votes whose +number is equal to the voting power of the process that has casted the corresponding votes message. + +Let's consider the following example: + - we have four processes p1, p2, p3 and p4, with the following voting power distribution (p1, 23), (p2, 27), (p3, 10) +and (p4, 10). The total voting power is 70 (`N = 3f+1`, where `N` is the total voting power, and `f` is the maximum voting +power of the faulty processes), so we assume that the faulty processes have at most 23 of voting power. +Furthermore, we have the following vote messages in some LastCommit field (we ignore all fields except Time field): + - (p1, 100), (p2, 98), (p3, 1000), (p4, 500). We assume that p3 and p4 are faulty processes. Let's assume that the + `block.LastCommit` message contains votes of processes p2, p3 and p4. Median is then chosen the following way: + the value 98 is counted 27 times, the value 1000 is counted 10 times and the value 500 is counted also 10 times. + So the median value will be the value 98. No matter what set of messages with at least `2f+1` voting power we + choose, the median value will always be between the values sent by correct processes. We ensure Time Monotonicity and Time Validity properties by the following rules: diff --git a/docs/specification/new-spec/reactors/consensus/consensus-reactor.md b/docs/specification/new-spec/reactors/consensus/consensus-reactor.md index 69ca409b4..21098dcac 100644 --- a/docs/specification/new-spec/reactors/consensus/consensus-reactor.md +++ b/docs/specification/new-spec/reactors/consensus/consensus-reactor.md @@ -31,7 +31,7 @@ Updates (state transitions) happen on timeouts, complete proposals, and 2/3 majo It receives messages from peers, internal validators and from Timeout Ticker and invokes the corresponding handlers, potentially updating the RoundState. The details of the protocol (together with formal proofs of correctness) implemented by the Receive Routine are -discussed in separate document (see [spec](https://github.com/tendermint/spec)). For understanding of this document +discussed in separate document. For understanding of this document it is sufficient to understand that the Receive Routine manages and updates RoundState data structure that is then extensively used by the gossip routines to determine what information should be sent to peer processes. diff --git a/docs/specification/new-spec/reactors/consensus/consensus.md b/docs/specification/new-spec/reactors/consensus/consensus.md index 1f311c44e..d5655297b 100644 --- a/docs/specification/new-spec/reactors/consensus/consensus.md +++ b/docs/specification/new-spec/reactors/consensus/consensus.md @@ -11,7 +11,7 @@ next block should be; a validator might vote with a `VoteMessage` for a differen round, enough number of processes vote for the same block, then this block is committed and later added to the blockchain. `ProposalMessage` and `VoteMessage` are signed by the private key of the validator. The internals of the protocol and how it ensures safety and liveness properties are -explained [here](https://github.com/tendermint/spec). +explained in a forthcoming document. For efficiency reasons, validators in Tendermint consensus protocol do not agree directly on the block as the block size is big, i.e., they don't embed the block inside `Proposal` and diff --git a/docs/specification/new-spec/reactors/pex/pex.md b/docs/specification/new-spec/reactors/pex/pex.md index 43d6f80d7..d3981bda2 100644 --- a/docs/specification/new-spec/reactors/pex/pex.md +++ b/docs/specification/new-spec/reactors/pex/pex.md @@ -57,10 +57,17 @@ a trust metric (see below), but it's best to start with something simple. ## Select Peers to Dial When we need more peers, we pick them randomly from the addrbook with some -configurable bias for unvetted peers. The bias should be lower when we have fewer peers, +configurable bias for unvetted peers. The bias should be lower when we have fewer peers and can increase as we obtain more, ensuring that our first peers are more trustworthy, but always giving us the chance to discover new good peers. +We track the last time we dialed a peer and the number of unsuccessful attempts +we've made. If too many attempts are made, we mark the peer as bad. + +Connection attempts are made with exponential backoff (plus jitter). Because +the selection process happens every `ensurePeersPeriod`, we might not end up +dialing a peer for much longer than the backoff duration. + ## Select Peers to Exchange When we’re asked for peers, we select them as follows: diff --git a/docs/specification/rpc.rst b/docs/specification/rpc.rst index 7df394d77..386791aa7 100644 --- a/docs/specification/rpc.rst +++ b/docs/specification/rpc.rst @@ -97,6 +97,7 @@ An HTTP Get request to the root RPC endpoint (e.g. http://localhost:46657/genesis http://localhost:46657/net_info http://localhost:46657/num_unconfirmed_txs + http://localhost:46657/health http://localhost:46657/status http://localhost:46657/unconfirmed_txs http://localhost:46657/unsafe_flush_mempool diff --git a/docs/using-tendermint.rst b/docs/using-tendermint.rst index bf6571f73..e07534c96 100644 --- a/docs/using-tendermint.rst +++ b/docs/using-tendermint.rst @@ -41,18 +41,18 @@ To run a Tendermint node, use tendermint node By default, Tendermint will try to connect to an ABCI application on -`127.0.0.1:46658 <127.0.0.1:46658>`__. If you have the ``dummy`` ABCI +`127.0.0.1:46658 <127.0.0.1:46658>`__. If you have the ``kvstore`` ABCI app installed, run it in another window. If you don't, kill Tendermint -and run an in-process version with +and run an in-process version of the ``kvstore`` app: :: - tendermint node --proxy_app=dummy + tendermint node --proxy_app=kvstore After a few seconds you should see blocks start streaming in. Note that blocks are produced regularly, even if there are no transactions. See *No Empty Blocks*, below, to modify this setting. -Tendermint supports in-process versions of the dummy, counter, and nil +Tendermint supports in-process versions of the ``counter``, ``kvstore`` and ``nil`` apps that ship as examples in the `ABCI repository `__. It's easy to compile your own app in-process with Tendermint if it's written in Go. If your diff --git a/evidence/reactor.go b/evidence/reactor.go index 169a274d3..6647db969 100644 --- a/evidence/reactor.go +++ b/evidence/reactor.go @@ -84,7 +84,8 @@ func (evR *EvidenceReactor) RemovePeer(peer p2p.Peer, reason interface{}) { func (evR *EvidenceReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { _, msg, err := DecodeMessage(msgBytes) if err != nil { - evR.Logger.Error("Error decoding message", "err", err) + evR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + evR.Switch.StopPeerForError(src, err) return } evR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) @@ -95,7 +96,8 @@ func (evR *EvidenceReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { err := evR.evpool.AddEvidence(ev) if err != nil { evR.Logger.Info("Evidence is not valid", "evidence", msg.Evidence, "err", err) - // TODO: punish peer + // punish peer + evR.Switch.StopPeerForError(src, err) } } default: diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 2e864cee3..000000000 --- a/glide.lock +++ /dev/null @@ -1,206 +0,0 @@ -hash: 322a0d4b9be08c59bf65df0e17e3be8d60762eaf9516f0c4126b50f9fd676f26 -updated: 2018-02-21T03:31:35.382568482Z -imports: -- name: github.com/btcsuite/btcd - version: 50de9da05b50eb15658bb350f6ea24368a111ab7 - subpackages: - - btcec -- name: github.com/ebuchman/fail-test - version: 95f809107225be108efcf10a3509e4ea6ceef3c4 -- name: github.com/fsnotify/fsnotify - version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 -- name: github.com/go-kit/kit - version: 4dc7be5d2d12881735283bcab7352178e190fc71 - subpackages: - - log - - log/level - - log/term -- name: github.com/go-logfmt/logfmt - version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 -- name: github.com/go-stack/stack - version: 259ab82a6cad3992b4e21ff5cac294ccb06474bc -- name: github.com/gogo/protobuf - version: 1adfc126b41513cc696b209667c8656ea7aac67c - subpackages: - - gogoproto - - jsonpb - - proto - - protoc-gen-gogo/descriptor - - sortkeys - - types -- name: github.com/golang/protobuf - version: 925541529c1fa6821df4e44ce2723319eb2be768 - subpackages: - - proto - - ptypes - - ptypes/any - - ptypes/duration - - ptypes/timestamp -- name: github.com/golang/snappy - version: 553a641470496b2327abcac10b36396bd98e45c9 -- name: github.com/gorilla/websocket - version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b -- name: github.com/hashicorp/hcl - version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 - subpackages: - - hcl/ast - - hcl/parser - - hcl/scanner - - hcl/strconv - - hcl/token - - json/parser - - json/scanner - - json/token -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/jmhodges/levigo - version: c42d9e0ca023e2198120196f842701bb4c55d7b9 -- name: github.com/kr/logfmt - version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 -- name: github.com/magiconair/properties - version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934 -- name: github.com/mitchellh/mapstructure - version: b4575eea38cca1123ec2dc90c26529b5c5acfcff -- name: github.com/pelletier/go-toml - version: acdc4509485b587f5e675510c4f2c63e90ff68a8 -- name: github.com/pkg/errors - version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/rcrowley/go-metrics - version: 8732c616f52954686704c8645fe1a9d59e9df7c1 -- name: github.com/spf13/afero - version: bb8f1927f2a9d3ab41c9340aa034f6b803f4359c - subpackages: - - mem -- name: github.com/spf13/cast - version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 -- name: github.com/spf13/cobra - version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b -- name: github.com/spf13/jwalterweatherman - version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394 -- name: github.com/spf13/pflag - version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 -- name: github.com/spf13/viper - version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 -- name: github.com/syndtr/goleveldb - version: 34011bf325bce385408353a30b101fe5e923eb6e - subpackages: - - leveldb - - leveldb/cache - - leveldb/comparer - - leveldb/errors - - leveldb/filter - - leveldb/iterator - - leveldb/journal - - leveldb/memdb - - leveldb/opt - - leveldb/storage - - leveldb/table - - leveldb/util -- name: github.com/tendermint/abci - version: 68592f4d8ee34e97db94b7a7976b1309efdb7eb9 - subpackages: - - client - - example/code - - example/counter - - example/dummy - - server - - types -- name: github.com/tendermint/ed25519 - version: d8387025d2b9d158cf4efb07e7ebf814bcce2057 - subpackages: - - edwards25519 - - extra25519 -- name: github.com/tendermint/go-crypto - version: dd20358a264c772b4a83e477b0cfce4c88a7001d -- name: github.com/tendermint/go-wire - version: b6fc872b42d41158a60307db4da051dd6f179415 - subpackages: - - data -- name: github.com/tendermint/tmlibs - version: 1b9b5652a199ab0be2e781393fb275b66377309d - subpackages: - - autofile - - cli - - cli/flags - - clist - - common - - db - - flowrate - - log - - merkle - - pubsub - - pubsub/query - - test -- name: golang.org/x/crypto - version: 1875d0a70c90e57f11972aefd42276df65e895b9 - subpackages: - - curve25519 - - nacl/box - - nacl/secretbox - - openpgp/armor - - openpgp/errors - - poly1305 - - ripemd160 - - salsa20/salsa -- name: golang.org/x/net - version: 2fb46b16b8dda405028c50f7c7f0f9dd1fa6bfb1 - subpackages: - - context - - http2 - - http2/hpack - - idna - - internal/timeseries - - lex/httplex - - trace -- name: golang.org/x/sys - version: 37707fdb30a5b38865cfb95e5aab41707daec7fd - subpackages: - - unix -- name: golang.org/x/text - version: e19ae1496984b1c655b8044a65c0300a3c878dd3 - subpackages: - - secure/bidirule - - transform - - unicode/bidi - - unicode/norm -- name: google.golang.org/genproto - version: 4eb30f4778eed4c258ba66527a0d4f9ec8a36c45 - subpackages: - - googleapis/rpc/status -- name: google.golang.org/grpc - version: 401e0e00e4bb830a10496d64cd95e068c5bf50de - subpackages: - - balancer - - codes - - connectivity - - credentials - - grpclb/grpc_lb_v1/messages - - grpclog - - internal - - keepalive - - metadata - - naming - - peer - - resolver - - stats - - status - - tap - - transport -- name: gopkg.in/yaml.v2 - version: d670f9405373e636a5a2765eea47fac0c9bc91a4 -testImports: -- name: github.com/davecgh/go-spew - version: 346938d642f2ec3594ed81d874461961cd0faa76 - subpackages: - - spew -- name: github.com/fortytw2/leaktest - version: 3b724c3d7b8729a35bf4e577f71653aec6e53513 -- name: github.com/pmezard/go-difflib - version: 792786c7400a136282c1664665ae0a8db921c6c2 - subpackages: - - difflib -- name: github.com/stretchr/testify - version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 - subpackages: - - assert - - require diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index c1b0aef55..000000000 --- a/glide.yaml +++ /dev/null @@ -1,63 +0,0 @@ -package: github.com/tendermint/tendermint -import: -- package: github.com/ebuchman/fail-test -- package: github.com/gogo/protobuf - version: ^1.0.0 - subpackages: - - proto -- package: github.com/golang/protobuf - version: ^1.0.0 - subpackages: - - proto -- package: github.com/gorilla/websocket - version: v1.2.0 -- package: github.com/pkg/errors - version: ~0.8.0 -- package: github.com/rcrowley/go-metrics -- package: github.com/spf13/cobra - version: v0.0.1 -- package: github.com/spf13/viper - version: v1.0.0 -- package: github.com/tendermint/abci - version: 0.10.0 - subpackages: - - client - - example/dummy - - types -- package: github.com/tendermint/go-crypto - version: 0.4.1 -- package: github.com/tendermint/go-wire - version: 0.7.2 - subpackages: - - data -- package: github.com/tendermint/tmlibs - version: 0.7.0 - subpackages: - - autofile - - cli - - cli/flags - - clist - - common - - db - - flowrate - - log - - merkle - - pubsub - - pubsub/query -- package: golang.org/x/crypto - subpackages: - - nacl/box - - nacl/secretbox - - ripemd160 -- package: google.golang.org/grpc - version: v1.7.3 -testImport: -- package: github.com/fortytw2/leaktest -- package: github.com/go-kit/kit - version: ^0.6.0 - subpackages: - - log/term -- package: github.com/stretchr/testify - subpackages: - - assert - - require diff --git a/lite/client/main_test.go b/lite/client/main_test.go index ab9867680..972d37310 100644 --- a/lite/client/main_test.go +++ b/lite/client/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" nm "github.com/tendermint/tendermint/node" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -14,7 +14,7 @@ var node *nm.Node func TestMain(m *testing.M) { // start a tendermint node (and merkleeyes) in the background to test against - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() node = rpctest.StartTendermint(app) code := m.Run() diff --git a/lite/helpers.go b/lite/helpers.go index 9c015a08e..7df77027c 100644 --- a/lite/helpers.go +++ b/lite/helpers.go @@ -77,7 +77,7 @@ func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit vset := v.ToValidators(1, 0) // fill in the votes we want - for i := first; i < last; i++ { + for i := first; i < last && i < len(v); i++ { vote := makeVote(header, vset, v[i]) votes[vote.ValidatorIndex] = vote } @@ -102,7 +102,7 @@ func makeVote(header *types.Header, vals *types.ValidatorSet, key crypto.PrivKey BlockID: types.BlockID{Hash: header.Hash()}, } // Sign it - signBytes := types.SignBytes(header.ChainID, vote) + signBytes := vote.SignBytes(header.ChainID) vote.Signature = key.Sign(signBytes) return vote } diff --git a/lite/memprovider.go b/lite/memprovider.go index ed7cd7725..ac0d83215 100644 --- a/lite/memprovider.go +++ b/lite/memprovider.go @@ -14,6 +14,8 @@ type memStoreProvider struct { // btree would be more efficient for larger sets byHeight fullCommits byHash map[string]FullCommit + + sorted bool } // fullCommits just exists to allow easy sorting @@ -52,25 +54,78 @@ func (m *memStoreProvider) StoreCommit(fc FullCommit) error { defer m.mtx.Unlock() m.byHash[key] = fc m.byHeight = append(m.byHeight, fc) - sort.Sort(m.byHeight) + m.sorted = false return nil } // GetByHeight returns the FullCommit for height h or an error if the commit is not found. func (m *memStoreProvider) GetByHeight(h int64) (FullCommit, error) { + // By heuristics, GetByHeight with linearsearch is fast enough + // for about 50 keys but after that, it needs binary search. + // See https://github.com/tendermint/tendermint/pull/1043#issue-285188242 m.mtx.RLock() - defer m.mtx.RUnlock() + n := len(m.byHeight) + m.mtx.RUnlock() + + if n <= 50 { + return m.getByHeightLinearSearch(h) + } + return m.getByHeightBinarySearch(h) +} + +func (m *memStoreProvider) sortByHeightIfNecessaryLocked() { + if !m.sorted { + sort.Sort(m.byHeight) + m.sorted = true + } +} +func (m *memStoreProvider) getByHeightLinearSearch(h int64) (FullCommit, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + m.sortByHeightIfNecessaryLocked() // search from highest to lowest for i := len(m.byHeight) - 1; i >= 0; i-- { - fc := m.byHeight[i] - if fc.Height() <= h { + if fc := m.byHeight[i]; fc.Height() <= h { return fc, nil } } return FullCommit{}, liteErr.ErrCommitNotFound() } +func (m *memStoreProvider) getByHeightBinarySearch(h int64) (FullCommit, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + m.sortByHeightIfNecessaryLocked() + low, high := 0, len(m.byHeight)-1 + var mid int + var hmid int64 + var midFC FullCommit + // Our goal is to either find: + // * item ByHeight with the query + // * greatest height with a height <= query + for low <= high { + mid = int(uint(low+high) >> 1) // Avoid an overflow + midFC = m.byHeight[mid] + hmid = midFC.Height() + switch { + case hmid == h: + return midFC, nil + case hmid < h: + low = mid + 1 + case hmid > h: + high = mid - 1 + } + } + + if high >= 0 { + if highFC := m.byHeight[high]; highFC.Height() < h { + return highFC, nil + } + } + return FullCommit{}, liteErr.ErrCommitNotFound() +} + // GetByHash returns the FullCommit for the hash or an error if the commit is not found. func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) { m.mtx.RLock() @@ -85,12 +140,13 @@ func (m *memStoreProvider) GetByHash(hash []byte) (FullCommit, error) { // LatestCommit returns the latest FullCommit or an error if no commits exist. func (m *memStoreProvider) LatestCommit() (FullCommit, error) { - m.mtx.RLock() - defer m.mtx.RUnlock() + m.mtx.Lock() + defer m.mtx.Unlock() l := len(m.byHeight) if l == 0 { return FullCommit{}, liteErr.ErrCommitNotFound() } + m.sortByHeightIfNecessaryLocked() return m.byHeight[l-1], nil } diff --git a/lite/performance_test.go b/lite/performance_test.go index 28c73bb08..8cd522cbb 100644 --- a/lite/performance_test.go +++ b/lite/performance_test.go @@ -1,33 +1,129 @@ -package lite_test +package lite import ( "fmt" + "math/rand" + "sync" "testing" - "github.com/tendermint/tendermint/lite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + liteErr "github.com/tendermint/tendermint/lite/errors" ) +func TestMemStoreProvidergetByHeightBinaryAndLinearSameResult(t *testing.T) { + p := NewMemStoreProvider().(*memStoreProvider) + + // Store a bunch of commits at specific heights + // and then ensure that: + // * getByHeightLinearSearch + // * getByHeightBinarySearch + // both return the exact same result + + // 1. Non-existent height commits + nonExistent := []int64{-1000, -1, 0, 1, 10, 11, 17, 31, 67, 1000, 1e9} + ensureNonExistentCommitsAtHeight(t, "getByHeightLinearSearch", p.getByHeightLinearSearch, nonExistent) + ensureNonExistentCommitsAtHeight(t, "getByHeightBinarySearch", p.getByHeightBinarySearch, nonExistent) + + // 2. Save some known height commits + knownHeights := []int64{0, 1, 7, 9, 12, 13, 18, 44, 23, 16, 1024, 100, 199, 1e9} + createAndStoreCommits(t, p, knownHeights) + + // 3. Now check if those heights are retrieved + ensureExistentCommitsAtHeight(t, "getByHeightLinearSearch", p.getByHeightLinearSearch, knownHeights) + ensureExistentCommitsAtHeight(t, "getByHeightBinarySearch", p.getByHeightBinarySearch, knownHeights) + + // 4. And now for the height probing to ensure that any height + // requested returns a fullCommit of height <= requestedHeight. + comparegetByHeightAlgorithms(t, p, 0, 0) + comparegetByHeightAlgorithms(t, p, 1, 1) + comparegetByHeightAlgorithms(t, p, 2, 1) + comparegetByHeightAlgorithms(t, p, 5, 1) + comparegetByHeightAlgorithms(t, p, 7, 7) + comparegetByHeightAlgorithms(t, p, 10, 9) + comparegetByHeightAlgorithms(t, p, 12, 12) + comparegetByHeightAlgorithms(t, p, 14, 13) + comparegetByHeightAlgorithms(t, p, 19, 18) + comparegetByHeightAlgorithms(t, p, 43, 23) + comparegetByHeightAlgorithms(t, p, 45, 44) + comparegetByHeightAlgorithms(t, p, 1025, 1024) + comparegetByHeightAlgorithms(t, p, 101, 100) + comparegetByHeightAlgorithms(t, p, 1e3, 199) + comparegetByHeightAlgorithms(t, p, 1e4, 1024) + comparegetByHeightAlgorithms(t, p, 1e9, 1e9) + comparegetByHeightAlgorithms(t, p, 1e9+1, 1e9) +} + +func createAndStoreCommits(t *testing.T, p Provider, heights []int64) { + chainID := "cache-best-height-binary-and-linear" + appHash := []byte("0xdeadbeef") + keys := GenValKeys(len(heights) / 2) + + for _, h := range heights { + vals := keys.ToValidators(10, int64(len(heights)/2)) + fc := keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5) + err := p.StoreCommit(fc) + require.NoError(t, err, "StoreCommit height=%d", h) + } +} + +func comparegetByHeightAlgorithms(t *testing.T, p *memStoreProvider, ask, expect int64) { + algos := map[string]func(int64) (FullCommit, error){ + "getHeightByLinearSearch": p.getByHeightLinearSearch, + "getHeightByBinarySearch": p.getByHeightBinarySearch, + } + + for algo, fn := range algos { + fc, err := fn(ask) + // t.Logf("%s got=%v want=%d", algo, expect, fc.Height()) + require.Nil(t, err, "%s: %+v", algo, err) + if assert.Equal(t, expect, fc.Height()) { + err = p.StoreCommit(fc) + require.Nil(t, err, "%s: %+v", algo, err) + } + } +} + +var blankFullCommit FullCommit + +func ensureNonExistentCommitsAtHeight(t *testing.T, prefix string, fn func(int64) (FullCommit, error), data []int64) { + for i, qh := range data { + fc, err := fn(qh) + assert.NotNil(t, err, "#%d: %s: height=%d should return non-nil error", i, prefix, qh) + assert.Equal(t, fc, blankFullCommit, "#%d: %s: height=%d\ngot =%+v\nwant=%+v", i, prefix, qh, fc, blankFullCommit) + } +} + +func ensureExistentCommitsAtHeight(t *testing.T, prefix string, fn func(int64) (FullCommit, error), data []int64) { + for i, qh := range data { + fc, err := fn(qh) + assert.Nil(t, err, "#%d: %s: height=%d should not return an error: %v", i, prefix, qh, err) + assert.NotEqual(t, fc, blankFullCommit, "#%d: %s: height=%d got a blankCommit", i, prefix, qh) + } +} + func BenchmarkGenCommit20(b *testing.B) { - keys := lite.GenValKeys(20) + keys := GenValKeys(20) benchmarkGenCommit(b, keys) } func BenchmarkGenCommit100(b *testing.B) { - keys := lite.GenValKeys(100) + keys := GenValKeys(100) benchmarkGenCommit(b, keys) } func BenchmarkGenCommitSec20(b *testing.B) { - keys := lite.GenSecpValKeys(20) + keys := GenSecpValKeys(20) benchmarkGenCommit(b, keys) } func BenchmarkGenCommitSec100(b *testing.B) { - keys := lite.GenSecpValKeys(100) + keys := GenSecpValKeys(100) benchmarkGenCommit(b, keys) } -func benchmarkGenCommit(b *testing.B, keys lite.ValKeys) { +func benchmarkGenCommit(b *testing.B, keys ValKeys) { chainID := fmt.Sprintf("bench-%d", len(keys)) vals := keys.ToValidators(20, 10) for i := 0; i < b.N; i++ { @@ -40,7 +136,7 @@ func benchmarkGenCommit(b *testing.B, keys lite.ValKeys) { // this benchmarks generating one key func BenchmarkGenValKeys(b *testing.B) { - keys := lite.GenValKeys(20) + keys := GenValKeys(20) for i := 0; i < b.N; i++ { keys = keys.Extend(1) } @@ -48,7 +144,7 @@ func BenchmarkGenValKeys(b *testing.B) { // this benchmarks generating one key func BenchmarkGenSecpValKeys(b *testing.B) { - keys := lite.GenSecpValKeys(20) + keys := GenSecpValKeys(20) for i := 0; i < b.N; i++ { keys = keys.Extend(1) } @@ -64,7 +160,7 @@ func BenchmarkToValidators100(b *testing.B) { // this benchmarks constructing the validator set (.PubKey() * nodes) func benchmarkToValidators(b *testing.B, nodes int) { - keys := lite.GenValKeys(nodes) + keys := GenValKeys(nodes) for i := 1; i <= b.N; i++ { keys.ToValidators(int64(2*i), int64(i)) } @@ -76,36 +172,36 @@ func BenchmarkToValidatorsSec100(b *testing.B) { // this benchmarks constructing the validator set (.PubKey() * nodes) func benchmarkToValidatorsSec(b *testing.B, nodes int) { - keys := lite.GenSecpValKeys(nodes) + keys := GenSecpValKeys(nodes) for i := 1; i <= b.N; i++ { keys.ToValidators(int64(2*i), int64(i)) } } func BenchmarkCertifyCommit20(b *testing.B) { - keys := lite.GenValKeys(20) + keys := GenValKeys(20) benchmarkCertifyCommit(b, keys) } func BenchmarkCertifyCommit100(b *testing.B) { - keys := lite.GenValKeys(100) + keys := GenValKeys(100) benchmarkCertifyCommit(b, keys) } func BenchmarkCertifyCommitSec20(b *testing.B) { - keys := lite.GenSecpValKeys(20) + keys := GenSecpValKeys(20) benchmarkCertifyCommit(b, keys) } func BenchmarkCertifyCommitSec100(b *testing.B) { - keys := lite.GenSecpValKeys(100) + keys := GenSecpValKeys(100) benchmarkCertifyCommit(b, keys) } -func benchmarkCertifyCommit(b *testing.B, keys lite.ValKeys) { +func benchmarkCertifyCommit(b *testing.B, keys ValKeys) { chainID := "bench-certify" vals := keys.ToValidators(20, 10) - cert := lite.NewStaticCertifier(chainID, vals) + cert := NewStaticCertifier(chainID, vals) check := keys.GenCommit(chainID, 123, nil, vals, []byte("foo"), []byte("params"), []byte("res"), 0, len(keys)) for i := 0; i < b.N; i++ { err := cert.Certify(check) @@ -115,3 +211,155 @@ func benchmarkCertifyCommit(b *testing.B, keys lite.ValKeys) { } } + +type algo bool + +const ( + linearSearch = true + binarySearch = false +) + +// Lazy load the commits +var fcs5, fcs50, fcs100, fcs500, fcs1000 []FullCommit +var h5, h50, h100, h500, h1000 []int64 +var commitsOnce sync.Once + +func lazyGenerateFullCommits(b *testing.B) { + b.Logf("Generating FullCommits") + commitsOnce.Do(func() { + fcs5, h5 = genFullCommits(nil, nil, 5) + b.Logf("Generated 5 FullCommits") + fcs50, h50 = genFullCommits(fcs5, h5, 50) + b.Logf("Generated 50 FullCommits") + fcs100, h100 = genFullCommits(fcs50, h50, 100) + b.Logf("Generated 100 FullCommits") + fcs500, h500 = genFullCommits(fcs100, h100, 500) + b.Logf("Generated 500 FullCommits") + fcs1000, h1000 = genFullCommits(fcs500, h500, 1000) + b.Logf("Generated 1000 FullCommits") + }) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch5(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs5, h5, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch50(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs50, h50, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch100(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs100, h100, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch500(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs500, h500, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightLinearSearch1000(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs1000, h1000, linearSearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch5(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs5, h5, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch50(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs50, h50, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch100(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs100, h100, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch500(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs500, h500, binarySearch) +} + +func BenchmarkMemStoreProviderGetByHeightBinarySearch1000(b *testing.B) { + benchmarkMemStoreProvidergetByHeight(b, fcs1000, h1000, binarySearch) +} + +var rng = rand.New(rand.NewSource(10)) + +func benchmarkMemStoreProvidergetByHeight(b *testing.B, fcs []FullCommit, fHeights []int64, algo algo) { + lazyGenerateFullCommits(b) + + b.StopTimer() + mp := NewMemStoreProvider() + for i, fc := range fcs { + if err := mp.StoreCommit(fc); err != nil { + b.Fatalf("FullCommit #%d: err: %v", i, err) + } + } + qHeights := make([]int64, len(fHeights)) + copy(qHeights, fHeights) + // Append some non-existent heights to trigger the worst cases. + qHeights = append(qHeights, 19, -100, -10000, 1e7, -17, 31, -1e9) + + memP := mp.(*memStoreProvider) + searchFn := memP.getByHeightLinearSearch + if algo == binarySearch { // nolint + searchFn = memP.getByHeightBinarySearch + } + + hPerm := rng.Perm(len(qHeights)) + b.StartTimer() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, j := range hPerm { + h := qHeights[j] + if _, err := searchFn(h); err != nil { + } + } + } + b.ReportAllocs() +} + +func genFullCommits(prevFC []FullCommit, prevH []int64, want int) ([]FullCommit, []int64) { + fcs := make([]FullCommit, len(prevFC)) + copy(fcs, prevFC) + heights := make([]int64, len(prevH)) + copy(heights, prevH) + + appHash := []byte("benchmarks") + chainID := "benchmarks-gen-full-commits" + n := want + keys := GenValKeys(2 + (n / 3)) + for i := 0; i < n; i++ { + vals := keys.ToValidators(10, int64(n/2)) + h := int64(20 + 10*i) + fcs = append(fcs, keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5)) + heights = append(heights, h) + } + return fcs, heights +} + +func TestMemStoreProviderLatestCommitAlwaysUsesSorted(t *testing.T) { + p := NewMemStoreProvider().(*memStoreProvider) + // 1. With no commits yet stored, it should return ErrCommitNotFound + got, err := p.LatestCommit() + require.Equal(t, err.Error(), liteErr.ErrCommitNotFound().Error(), "should return ErrCommitNotFound()") + require.Equal(t, got, blankFullCommit, "With no fullcommits, it should return a blank FullCommit") + + // 2. Generate some full commits now and we'll add them unsorted. + genAndStoreCommitsOfHeight(t, p, 27, 100, 1, 12, 1000, 17, 91) + fc, err := p.LatestCommit() + require.Nil(t, err, "with commits saved no error expected") + require.NotEqual(t, fc, blankFullCommit, "with commits saved no blank FullCommit") + require.Equal(t, fc.Height(), int64(1000), "the latest commit i.e. the largest expected") +} + +func genAndStoreCommitsOfHeight(t *testing.T, p Provider, heights ...int64) { + n := len(heights) + appHash := []byte("tests") + chainID := "tests-gen-full-commits" + keys := GenValKeys(2 + (n / 3)) + for i := 0; i < n; i++ { + h := heights[i] + vals := keys.ToValidators(10, int64(n/2)) + fc := keys.GenFullCommit(chainID, h, nil, vals, appHash, []byte("params"), []byte("results"), 0, 5) + err := p.StoreCommit(fc) + require.NoError(t, err, "StoreCommit height=%d", h) + } +} diff --git a/lite/provider_test.go b/lite/provider_test.go index b2529b550..77b5b1a85 100644 --- a/lite/provider_test.go +++ b/lite/provider_test.go @@ -103,10 +103,10 @@ func checkProvider(t *testing.T, p lite.Provider, chainID, app string) { // this will make a get height, and if it is good, set the data as well func checkGetHeight(t *testing.T, p lite.Provider, ask, expect int64) { fc, err := p.GetByHeight(ask) - require.Nil(t, err, "%+v", err) + require.Nil(t, err, "GetByHeight") if assert.Equal(t, expect, fc.Height()) { err = p.StoreCommit(fc) - require.Nil(t, err, "%+v", err) + require.Nil(t, err, "StoreCommit") } } diff --git a/lite/proxy/query_test.go b/lite/proxy/query_test.go index 6fc4b9730..d6dcccc96 100644 --- a/lite/proxy/query_test.go +++ b/lite/proxy/query_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/tendermint/tendermint/lite" certclient "github.com/tendermint/tendermint/lite/client" @@ -23,7 +23,7 @@ var node *nm.Node // TODO fix tests!! func TestMain(m *testing.M) { - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() node = rpctest.StartTendermint(app) @@ -34,7 +34,7 @@ func TestMain(m *testing.M) { os.Exit(code) } -func dummyTx(k, v []byte) []byte { +func kvstoreTx(k, v []byte) []byte { return []byte(fmt.Sprintf("%s=%s", k, v)) } @@ -47,7 +47,7 @@ func _TestAppProofs(t *testing.T) { k := []byte("my-key") v := []byte("my-value") - tx := dummyTx(k, v) + tx := kvstoreTx(k, v) br, err := cl.BroadcastTxCommit(tx) require.NoError(err, "%+v", err) require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) @@ -107,7 +107,7 @@ func _TestTxProofs(t *testing.T) { cl := client.NewLocal(node) client.WaitForHeight(cl, 1, nil) - tx := dummyTx([]byte("key-a"), []byte("value-a")) + tx := kvstoreTx([]byte("key-a"), []byte("value-a")) br, err := cl.BroadcastTxCommit(tx) require.NoError(err, "%+v", err) require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 5b04e9493..a67adf6d3 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -12,7 +12,7 @@ import ( "time" "github.com/tendermint/abci/example/counter" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" abci "github.com/tendermint/abci/types" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" @@ -73,7 +73,7 @@ func checkTxs(t *testing.T, mempool *Mempool, count int) types.Txs { } func TestTxsAvailable(t *testing.T) { - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool := newMempoolWithApp(cc) mempool.EnableTxsAvailable() @@ -238,7 +238,7 @@ func TestMempoolCloseWAL(t *testing.T) { // 3. Create the mempool wcfg := cfg.DefaultMempoolConfig() wcfg.RootDir = rootDir - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) appConnMem, _ := cc.NewABCIClient() mempool := NewMempool(wcfg, appConnMem, 10) diff --git a/mempool/reactor.go b/mempool/reactor.go index 58650a197..514347e94 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -73,7 +73,8 @@ func (memR *MempoolReactor) RemovePeer(peer p2p.Peer, reason interface{}) { func (memR *MempoolReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { _, msg, err := DecodeMessage(msgBytes) if err != nil { - memR.Logger.Error("Error decoding message", "err", err) + memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + memR.Switch.StopPeerForError(src, err) return } memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index 3cbc57481..0a6d09153 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-kit/kit/log/term" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/tendermint/tmlibs/log" cfg "github.com/tendermint/tendermint/config" @@ -39,7 +39,7 @@ func makeAndConnectMempoolReactors(config *cfg.Config, N int) []*MempoolReactor reactors := make([]*MempoolReactor, N) logger := mempoolLogger() for i := 0; i < N; i++ { - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool := newMempoolWithApp(cc) diff --git a/node/node.go b/node/node.go index cd2b4bcb5..1f745bf00 100644 --- a/node/node.go +++ b/node/node.go @@ -34,6 +34,7 @@ import ( "github.com/tendermint/tendermint/state/txindex/kv" "github.com/tendermint/tendermint/state/txindex/null" "github.com/tendermint/tendermint/types" + priv_val "github.com/tendermint/tendermint/types/priv_validator" "github.com/tendermint/tendermint/version" _ "net/http/pprof" @@ -82,7 +83,8 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), DefaultGenesisDocProviderFunc(config), DefaultDBProvider, - logger) + logger, + ) } //------------------------------------------------------------------------------ @@ -160,7 +162,7 @@ func NewNode(config *cfg.Config, // and sync tendermint and the app by performing a handshake // and replaying any necessary blocks consensusLogger := logger.With("module", "consensus") - handshaker := cs.NewHandshaker(stateDB, state, blockStore) + handshaker := cs.NewHandshaker(stateDB, state, blockStore, genDoc.AppState()) handshaker.SetLogger(consensusLogger) proxyApp := proxy.NewAppConns(clientCreator, handshaker) proxyApp.SetLogger(logger.With("module", "proxy")) @@ -171,6 +173,27 @@ func NewNode(config *cfg.Config, // reload the state (it may have been updated by the handshake) state = sm.LoadState(stateDB) + // If an address is provided, listen on the socket for a + // connection from an external signing process. + if config.PrivValidatorListenAddr != "" { + var ( + // TODO: persist this key so external signer + // can actually authenticate us + privKey = crypto.GenPrivKeyEd25519() + pvsc = priv_val.NewSocketClient( + logger.With("module", "priv_val"), + config.PrivValidatorListenAddr, + privKey, + ) + ) + + if err := pvsc.Start(); err != nil { + return nil, fmt.Errorf("Error starting private validator client: %v", err) + } + + privValidator = pvsc + } + // Decide whether to fast-sync or not // We don't fast-sync when the only validator is us. fastSync := config.FastSync @@ -258,12 +281,21 @@ func NewNode(config *cfg.Config, if config.P2P.Seeds != "" { seeds = strings.Split(config.P2P.Seeds, ",") } + var privatePeerIDs []string + if config.P2P.PrivatePeerIDs != "" { + privatePeerIDs = strings.Split(config.P2P.PrivatePeerIDs, ",") + } pexReactor := pex.NewPEXReactor(addrBook, - &pex.PEXReactorConfig{Seeds: seeds, SeedMode: config.P2P.SeedMode}) + &pex.PEXReactorConfig{ + Seeds: seeds, + SeedMode: config.P2P.SeedMode, + PrivatePeerIDs: privatePeerIDs}) pexReactor.SetLogger(p2pLogger) sw.AddReactor("PEX", pexReactor) } + sw.SetAddrBook(addrBook) + // Filter peers by addr or pubkey with an ABCI query. // If the query return code is OK, add peer. // XXX: Query format subject to change @@ -279,8 +311,8 @@ func NewNode(config *cfg.Config, } return nil }) - sw.SetPubKeyFilter(func(pubkey crypto.PubKey) error { - resQuery, err := proxyApp.Query().QuerySync(abci.RequestQuery{Path: cmn.Fmt("/p2p/filter/pubkey/%X", pubkey.Bytes())}) + sw.SetIDFilter(func(id p2p.ID) error { + resQuery, err := proxyApp.Query().QuerySync(abci.RequestQuery{Path: cmn.Fmt("/p2p/filter/pubkey/%s", id)}) if err != nil { return err } @@ -375,7 +407,7 @@ func (n *Node) OnStart() error { n.sw.AddListener(l) // Generate node PrivKey - // TODO: pass in like priv_val + // TODO: pass in like privValidator nodeKey, err := p2p.LoadOrGenNodeKey(n.config.NodeKeyFile()) if err != nil { return err @@ -418,8 +450,13 @@ func (n *Node) OnStop() { } n.eventBus.Stop() - n.indexerService.Stop() + + if pvsc, ok := n.privValidator.(*priv_val.SocketClient); ok { + if err := pvsc.Stop(); err != nil { + n.Logger.Error("Error stopping priv validator socket client", "err", err) + } + } } // RunForever waits for an interrupt signal and stops the node. diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index 4cf715dd2..c81b3b285 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -8,12 +8,12 @@ import ( cmn "github.com/tendermint/tmlibs/common" ) -type dummyConn struct { +type kvstoreConn struct { *io.PipeReader *io.PipeWriter } -func (drw dummyConn) Close() (err error) { +func (drw kvstoreConn) Close() (err error) { err2 := drw.PipeWriter.CloseWithError(io.EOF) err1 := drw.PipeReader.Close() if err2 != nil { @@ -23,14 +23,14 @@ func (drw dummyConn) Close() (err error) { } // Each returned ReadWriteCloser is akin to a net.Connection -func makeDummyConnPair() (fooConn, barConn dummyConn) { +func makeKVStoreConnPair() (fooConn, barConn kvstoreConn) { barReader, fooWriter := io.Pipe() fooReader, barWriter := io.Pipe() - return dummyConn{fooReader, fooWriter}, dummyConn{barReader, barWriter} + return kvstoreConn{fooReader, fooWriter}, kvstoreConn{barReader, barWriter} } func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) { - fooConn, barConn := makeDummyConnPair() + fooConn, barConn := makeKVStoreConnPair() fooPrvKey := crypto.GenPrivKeyEd25519().Wrap() fooPubKey := fooPrvKey.PubKey() barPrvKey := crypto.GenPrivKeyEd25519().Wrap() @@ -78,7 +78,7 @@ func TestSecretConnectionHandshake(t *testing.T) { } func TestSecretConnectionReadWrite(t *testing.T) { - fooConn, barConn := makeDummyConnPair() + fooConn, barConn := makeKVStoreConnPair() fooWrites, barWrites := []string{}, []string{} fooReads, barReads := []string{}, []string{} @@ -89,7 +89,7 @@ func TestSecretConnectionReadWrite(t *testing.T) { } // A helper that will run with (fooConn, fooWrites, fooReads) and vice versa - genNodeRunner := func(nodeConn dummyConn, nodeWrites []string, nodeReads *[]string) func() { + genNodeRunner := func(nodeConn kvstoreConn, nodeWrites []string, nodeReads *[]string) func() { return func() { // Node handskae nodePrvKey := crypto.GenPrivKeyEd25519().Wrap() diff --git a/p2p/dummy/peer.go b/p2p/dummy/peer.go new file mode 100644 index 000000000..61c3a8ace --- /dev/null +++ b/p2p/dummy/peer.go @@ -0,0 +1,72 @@ +package dummy + +import ( + p2p "github.com/tendermint/tendermint/p2p" + tmconn "github.com/tendermint/tendermint/p2p/conn" + cmn "github.com/tendermint/tmlibs/common" +) + +type peer struct { + cmn.BaseService + kv map[string]interface{} +} + +var _ p2p.Peer = (*peer)(nil) + +// NewPeer creates new dummy peer. +func NewPeer() *peer { + p := &peer{ + kv: make(map[string]interface{}), + } + p.BaseService = *cmn.NewBaseService(nil, "peer", p) + return p +} + +// ID always returns dummy. +func (p *peer) ID() p2p.ID { + return p2p.ID("dummy") +} + +// IsOutbound always returns false. +func (p *peer) IsOutbound() bool { + return false +} + +// IsPersistent always returns false. +func (p *peer) IsPersistent() bool { + return false +} + +// NodeInfo always returns empty node info. +func (p *peer) NodeInfo() p2p.NodeInfo { + return p2p.NodeInfo{} +} + +// Status always returns empry connection status. +func (p *peer) Status() tmconn.ConnectionStatus { + return tmconn.ConnectionStatus{} +} + +// Send does not do anything and just returns true. +func (p *peer) Send(byte, interface{}) bool { + return true +} + +// TrySend does not do anything and just returns true. +func (p *peer) TrySend(byte, interface{}) bool { + return true +} + +// Set records value under key specified in the map. +func (p *peer) Set(key string, value interface{}) { + p.kv[key] = value +} + +// Get returns a value associated with the key. Nil is returned if no value +// found. +func (p *peer) Get(key string) interface{} { + if value, ok := p.kv[key]; ok { + return value + } + return nil +} diff --git a/p2p/node_info.go b/p2p/node_info.go index a5bb9da53..6f44b49cd 100644 --- a/p2p/node_info.go +++ b/p2p/node_info.go @@ -34,15 +34,10 @@ type NodeInfo struct { } // Validate checks the self-reported NodeInfo is safe. -// It returns an error if the info.PubKey doesn't match the given pubKey, -// or if there are too many Channels or any duplicate Channels. +// It returns an error if there +// are too many Channels or any duplicate Channels. // TODO: constraints for Moniker/Other? Or is that for the UI ? -func (info NodeInfo) Validate(pubKey crypto.PubKey) error { - if !info.PubKey.Equals(pubKey) { - return fmt.Errorf("info.PubKey (%v) doesn't match peer.PubKey (%v)", - info.PubKey, pubKey) - } - +func (info NodeInfo) Validate() error { if len(info.Channels) > maxNumChannels { return fmt.Errorf("info.Channels is too long (%v). Max is %v", len(info.Channels), maxNumChannels) } diff --git a/p2p/peer.go b/p2p/peer.go index 67ce411cd..4af6eeaae 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -34,23 +34,53 @@ type Peer interface { //---------------------------------------------------------- +// peerConn contains the raw connection and its config. +type peerConn struct { + outbound bool + persistent bool + config *PeerConfig + conn net.Conn // source connection +} + +// ID only exists for SecretConnection. +// NOTE: Will panic if conn is not *SecretConnection. +func (pc peerConn) ID() ID { + return PubKeyToID(pc.conn.(*tmconn.SecretConnection).RemotePubKey()) +} + // peer implements Peer. // // Before using a peer, you will need to perform a handshake on connection. type peer struct { cmn.BaseService - outbound bool + // raw peerConn and the multiplex connection + peerConn + mconn *tmconn.MConnection - conn net.Conn // source connection - mconn *tmconn.MConnection // multiplex connection + // peer's node info and the channel it knows about + // channels = nodeInfo.Channels + // cached to avoid copying nodeInfo in hasChannel + nodeInfo NodeInfo + channels []byte - persistent bool - config *PeerConfig + // User data + Data *cmn.CMap +} + +func newPeer(pc peerConn, nodeInfo NodeInfo, + reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, + onPeerError func(Peer, interface{})) *peer { - nodeInfo NodeInfo // peer's node info - channels []byte // channels the peer knows about - Data *cmn.CMap // User data. + p := &peer{ + peerConn: pc, + nodeInfo: nodeInfo, + channels: nodeInfo.Channels, + Data: cmn.NewCMap(), + } + p.mconn = createMConnection(pc.conn, p, reactorsByCh, chDescs, onPeerError, pc.config.MConfig) + p.BaseService = *cmn.NewBaseService(nil, "Peer", p) + return p } // PeerConfig is a Peer configuration. @@ -79,36 +109,42 @@ func DefaultPeerConfig() *PeerConfig { } } -func newOutboundPeer(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig, persistent bool) (*peer, error) { +func newOutboundPeerConn(addr *NetAddress, config *PeerConfig, persistent bool, ourNodePrivKey crypto.PrivKey) (peerConn, error) { + var pc peerConn conn, err := dial(addr, config) if err != nil { - return nil, errors.Wrap(err, "Error creating peer") + return pc, errors.Wrap(err, "Error creating peer") } - peer, err := newPeerFromConnAndConfig(conn, true, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) + pc, err = newPeerConn(conn, config, true, persistent, ourNodePrivKey) if err != nil { - if err := conn.Close(); err != nil { - return nil, err + if err2 := conn.Close(); err2 != nil { + return pc, errors.Wrap(err, err2.Error()) } - return nil, err + return pc, err } - peer.persistent = persistent - return peer, nil + // ensure dialed ID matches connection ID + if config.AuthEnc && addr.ID != pc.ID() { + if err2 := conn.Close(); err2 != nil { + return pc, errors.Wrap(err, err2.Error()) + } + return pc, ErrSwitchAuthenticationFailure{addr, pc.ID()} + } + return pc, nil } -func newInboundPeer(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig) (*peer, error) { +func newInboundPeerConn(conn net.Conn, config *PeerConfig, ourNodePrivKey crypto.PrivKey) (peerConn, error) { // TODO: issue PoW challenge - return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) + return newPeerConn(conn, config, false, false, ourNodePrivKey) } -func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig) (*peer, error) { +func newPeerConn(rawConn net.Conn, + config *PeerConfig, outbound, persistent bool, + ourNodePrivKey crypto.PrivKey) (pc peerConn, err error) { conn := rawConn @@ -118,32 +154,26 @@ func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[ conn = FuzzConnAfterFromConfig(conn, 10*time.Second, config.FuzzConfig) } - // Encrypt connection if config.AuthEnc { + // Set deadline for secret handshake if err := conn.SetDeadline(time.Now().Add(config.HandshakeTimeout * time.Second)); err != nil { - return nil, errors.Wrap(err, "Error setting deadline while encrypting connection") + return pc, errors.Wrap(err, "Error setting deadline while encrypting connection") } - var err error + // Encrypt connection conn, err = tmconn.MakeSecretConnection(conn, ourNodePrivKey) if err != nil { - return nil, errors.Wrap(err, "Error creating peer") + return pc, errors.Wrap(err, "Error creating peer") } } - // NodeInfo is set after Handshake - p := &peer{ - outbound: outbound, - conn: conn, - config: config, - Data: cmn.NewCMap(), - } - - p.mconn = createMConnection(conn, p, reactorsByCh, chDescs, onPeerError, config.MConfig) - - p.BaseService = *cmn.NewBaseService(nil, "Peer", p) - - return p, nil + // Only the information we already have + return peerConn{ + config: config, + outbound: outbound, + persistent: persistent, + conn: conn, + }, nil } //--------------------------------------------------- @@ -175,17 +205,17 @@ func (p *peer) OnStop() { // ID returns the peer's ID - the hex encoded hash of its pubkey. func (p *peer) ID() ID { - return PubKeyToID(p.PubKey()) + return p.nodeInfo.ID() } // IsOutbound returns true if the connection is outbound, false otherwise. func (p *peer) IsOutbound() bool { - return p.outbound + return p.peerConn.outbound } // IsPersistent returns true if the peer is persitent, false otherwise. func (p *peer) IsPersistent() bool { - return p.persistent + return p.peerConn.persistent } // NodeInfo returns a copy of the peer's NodeInfo. @@ -250,68 +280,48 @@ func (p *peer) hasChannel(chID byte) bool { // methods used by the Switch // CloseConn should be called by the Switch if the peer was created but never started. -func (p *peer) CloseConn() { - p.conn.Close() // nolint: errcheck +func (pc *peerConn) CloseConn() { + pc.conn.Close() // nolint: errcheck } // HandshakeTimeout performs the Tendermint P2P handshake between a given node and the peer // by exchanging their NodeInfo. It sets the received nodeInfo on the peer. // NOTE: blocking -func (p *peer) HandshakeTimeout(ourNodeInfo NodeInfo, timeout time.Duration) error { +func (pc *peerConn) HandshakeTimeout(ourNodeInfo NodeInfo, timeout time.Duration) (peerNodeInfo NodeInfo, err error) { // Set deadline for handshake so we don't block forever on conn.ReadFull - if err := p.conn.SetDeadline(time.Now().Add(timeout)); err != nil { - return errors.Wrap(err, "Error setting deadline") + if err := pc.conn.SetDeadline(time.Now().Add(timeout)); err != nil { + return peerNodeInfo, errors.Wrap(err, "Error setting deadline") } - var peerNodeInfo NodeInfo var err1 error var err2 error cmn.Parallel( func() { var n int - wire.WriteBinary(&ourNodeInfo, p.conn, &n, &err1) + wire.WriteBinary(&ourNodeInfo, pc.conn, &n, &err1) }, func() { var n int - wire.ReadBinary(&peerNodeInfo, p.conn, MaxNodeInfoSize(), &n, &err2) - p.Logger.Info("Peer handshake", "peerNodeInfo", peerNodeInfo) + wire.ReadBinary(&peerNodeInfo, pc.conn, MaxNodeInfoSize(), &n, &err2) }) if err1 != nil { - return errors.Wrap(err1, "Error during handshake/write") + return peerNodeInfo, errors.Wrap(err1, "Error during handshake/write") } if err2 != nil { - return errors.Wrap(err2, "Error during handshake/read") + return peerNodeInfo, errors.Wrap(err2, "Error during handshake/read") } // Remove deadline - if err := p.conn.SetDeadline(time.Time{}); err != nil { - return errors.Wrap(err, "Error removing deadline") + if err := pc.conn.SetDeadline(time.Time{}); err != nil { + return peerNodeInfo, errors.Wrap(err, "Error removing deadline") } - p.setNodeInfo(peerNodeInfo) - return nil -} - -func (p *peer) setNodeInfo(nodeInfo NodeInfo) { - p.nodeInfo = nodeInfo - // cache the channels so we dont copy nodeInfo - // every time we check hasChannel - p.channels = nodeInfo.Channels + return peerNodeInfo, nil } // Addr returns peer's remote network address. func (p *peer) Addr() net.Addr { - return p.conn.RemoteAddr() -} - -// PubKey returns peer's public key. -func (p *peer) PubKey() crypto.PubKey { - if !p.nodeInfo.PubKey.Empty() { - return p.nodeInfo.PubKey - } else if p.config.AuthEnc { - return p.conn.(*tmconn.SecretConnection).RemotePubKey() - } - panic("Attempt to get peer's PubKey before calling Handshake") + return p.peerConn.conn.RemoteAddr() } // CanSend returns true if the send queue is not full, false otherwise. @@ -348,7 +358,9 @@ func createMConnection(conn net.Conn, p *peer, reactorsByCh map[byte]Reactor, ch onReceive := func(chID byte, msgBytes []byte) { reactor := reactorsByCh[chID] if reactor == nil { - cmn.PanicSanity(cmn.Fmt("Unknown channel %X", chID)) + // Note that its ok to panic here as it's caught in the conn._recover, + // which does onPeerError. + panic(cmn.Fmt("Unknown channel %X", chID)) } reactor.Receive(chID, p, msgBytes) } diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index e906eb8e7..174e73c9c 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -11,12 +11,13 @@ import ( cmn "github.com/tendermint/tmlibs/common" ) -// Returns an empty dummy peer +// Returns an empty kvstore peer func randPeer() *peer { + pubKey := crypto.GenPrivKeyEd25519().Wrap().PubKey() return &peer{ nodeInfo: NodeInfo{ ListenAddr: cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), - PubKey: crypto.GenPrivKeyEd25519().Wrap().PubKey(), + PubKey: pubKey, }, } } diff --git a/p2p/peer_test.go b/p2p/peer_test.go index a2f5ed05f..0bf7076e1 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -11,6 +11,7 @@ import ( crypto "github.com/tendermint/go-crypto" tmconn "github.com/tendermint/tendermint/p2p/conn" + "github.com/tendermint/tmlibs/log" ) const testCh = 0x01 @@ -35,8 +36,8 @@ func TestPeerBasic(t *testing.T) { assert.False(p.IsPersistent()) p.persistent = true assert.True(p.IsPersistent()) - assert.Equal(rp.Addr().String(), p.Addr().String()) - assert.Equal(rp.PubKey(), p.PubKey()) + assert.Equal(rp.Addr().DialString(), p.Addr().String()) + assert.Equal(rp.ID(), p.ID()) } func TestPeerWithoutAuthEnc(t *testing.T) { @@ -89,11 +90,11 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) } reactorsByCh := map[byte]Reactor{testCh: NewTestReactor(chDescs, true)} pk := crypto.GenPrivKeyEd25519().Wrap() - p, err := newOutboundPeer(addr, reactorsByCh, chDescs, func(p Peer, r interface{}) {}, pk, config, false) + pc, err := newOutboundPeerConn(addr, config, false, pk) if err != nil { return nil, err } - err = p.HandshakeTimeout(NodeInfo{ + nodeInfo, err := pc.HandshakeTimeout(NodeInfo{ PubKey: pk.PubKey(), Moniker: "host_peer", Network: "testing", @@ -103,6 +104,9 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) if err != nil { return nil, err } + + p := newPeer(pc, nodeInfo, reactorsByCh, chDescs, func(p Peer, r interface{}) {}) + p.SetLogger(log.TestingLogger().With("peer", addr)) return p, nil } @@ -117,8 +121,8 @@ func (p *remotePeer) Addr() *NetAddress { return p.addr } -func (p *remotePeer) PubKey() crypto.PubKey { - return p.PrivKey.PubKey() +func (p *remotePeer) ID() ID { + return PubKeyToID(p.PrivKey.PubKey()) } func (p *remotePeer) Start() { @@ -126,7 +130,7 @@ func (p *remotePeer) Start() { if e != nil { golog.Fatalf("net.Listen tcp :0: %+v", e) } - p.addr = NewNetAddress("", l.Addr()) + p.addr = NewNetAddress(PubKeyToID(p.PrivKey.PubKey()), l.Addr()) p.quit = make(chan struct{}) go p.accept(l) } @@ -136,16 +140,18 @@ func (p *remotePeer) Stop() { } func (p *remotePeer) accept(l net.Listener) { + conns := []net.Conn{} + for { conn, err := l.Accept() if err != nil { golog.Fatalf("Failed to accept conn: %+v", err) } - peer, err := newInboundPeer(conn, make(map[byte]Reactor), make([]*tmconn.ChannelDescriptor, 0), func(p Peer, r interface{}) {}, p.PrivKey, p.Config) + pc, err := newInboundPeerConn(conn, p.Config, p.PrivKey) if err != nil { golog.Fatalf("Failed to create a peer: %+v", err) } - err = peer.HandshakeTimeout(NodeInfo{ + _, err = pc.HandshakeTimeout(NodeInfo{ PubKey: p.PrivKey.PubKey(), Moniker: "remote_peer", Network: "testing", @@ -156,10 +162,15 @@ func (p *remotePeer) accept(l net.Listener) { if err != nil { golog.Fatalf("Failed to perform handshake: %+v", err) } + + conns = append(conns, conn) + select { case <-p.quit: - if err := conn.Close(); err != nil { - golog.Fatal(err) + for _, conn := range conns { + if err := conn.Close(); err != nil { + golog.Fatal(err) + } } return default: diff --git a/p2p/pex/addrbook.go b/p2p/pex/addrbook.go index 95ad70fe7..3a3920486 100644 --- a/p2p/pex/addrbook.go +++ b/p2p/pex/addrbook.go @@ -139,6 +139,10 @@ func (a *addrBook) Wait() { a.wg.Wait() } +func (a *addrBook) FilePath() string { + return a.filePath +} + //------------------------------------------------------- // AddOurAddress one of our addresses. diff --git a/p2p/pex/addrbook_test.go b/p2p/pex/addrbook_test.go index 166d31847..4a8df7166 100644 --- a/p2p/pex/addrbook_test.go +++ b/p2p/pex/addrbook_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "math/rand" + "os" "testing" "github.com/stretchr/testify/assert" @@ -26,17 +27,24 @@ func createTempFileName(prefix string) string { return fname } +func deleteTempFile(fname string) { + err := os.Remove(fname) + if err != nil { + panic(err) + } +} + func TestAddrBookPickAddress(t *testing.T) { - assert := assert.New(t) fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) // 0 addresses book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) - assert.Zero(book.Size()) + assert.Zero(t, book.Size()) addr := book.PickAddress(50) - assert.Nil(addr, "expected no address") + assert.Nil(t, addr, "expected no address") randAddrs := randNetAddressPairs(t, 1) addrSrc := randAddrs[0] @@ -44,26 +52,27 @@ func TestAddrBookPickAddress(t *testing.T) { // pick an address when we only have new address addr = book.PickAddress(0) - assert.NotNil(addr, "expected an address") + assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(50) - assert.NotNil(addr, "expected an address") + assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(100) - assert.NotNil(addr, "expected an address") + assert.NotNil(t, addr, "expected an address") // pick an address when we only have old address book.MarkGood(addrSrc.addr) addr = book.PickAddress(0) - assert.NotNil(addr, "expected an address") + assert.NotNil(t, addr, "expected an address") addr = book.PickAddress(50) - assert.NotNil(addr, "expected an address") + assert.NotNil(t, addr, "expected an address") // in this case, nNew==0 but we biased 100% to new, so we return nil addr = book.PickAddress(100) - assert.Nil(addr, "did not expected an address") + assert.Nil(t, addr, "did not expected an address") } func TestAddrBookSaveLoad(t *testing.T) { fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) // 0 addresses book := NewAddrBook(fname, true) @@ -95,6 +104,7 @@ func TestAddrBookSaveLoad(t *testing.T) { func TestAddrBookLookup(t *testing.T) { fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) randAddrs := randNetAddressPairs(t, 100) @@ -115,8 +125,8 @@ func TestAddrBookLookup(t *testing.T) { } func TestAddrBookPromoteToOld(t *testing.T) { - assert := assert.New(t) fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) randAddrs := randNetAddressPairs(t, 100) @@ -147,11 +157,12 @@ func TestAddrBookPromoteToOld(t *testing.T) { t.Errorf("selection could not be bigger than the book") } - assert.Equal(book.Size(), 100, "expecting book size to be 100") + assert.Equal(t, book.Size(), 100, "expecting book size to be 100") } func TestAddrBookHandlesDuplicates(t *testing.T) { fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) @@ -202,6 +213,8 @@ func randIPv4Address(t *testing.T) *p2p.NetAddress { func TestAddrBookRemoveAddress(t *testing.T) { fname := createTempFileName("addrbook_test") + defer deleteTempFile(fname) + book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 5aeca8f76..45a05bdb7 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -6,6 +6,7 @@ import ( "math/rand" "reflect" "sort" + "sync" "time" "github.com/pkg/errors" @@ -38,6 +39,8 @@ const ( defaultSeedDisconnectWaitPeriod = 2 * time.Minute // disconnect after this defaultCrawlPeerInterval = 2 * time.Minute // dont redial for this. TODO: back-off defaultCrawlPeersPeriod = 30 * time.Second // check some peers every this + + maxAttemptsToDial = 16 // ~ 35h in total (last attempt - 18h) ) // PEXReactor handles PEX (peer exchange) and ensures that an @@ -59,6 +62,8 @@ type PEXReactor struct { // maps to prevent abuse requestsSent *cmn.CMap // ID->struct{}: unanswered send requests lastReceivedRequests *cmn.CMap // ID->time.Time: last time peer requested from us + + attemptsToDial sync.Map // address (string) -> {number of attempts (int), last time dialed (time.Time)} } // PEXReactorConfig holds reactor specific configuration data. @@ -69,6 +74,15 @@ type PEXReactorConfig struct { // Seeds is a list of addresses reactor may use // if it can't connect to peers in the addrbook. Seeds []string + + // PrivatePeerIDs is a list of peer IDs, which must not be gossiped to other + // peers. + PrivatePeerIDs []string +} + +type _attemptsToDial struct { + number int + lastDialed time.Time } // NewPEXReactor creates new PEX reactor. @@ -142,7 +156,9 @@ func (r *PEXReactor) AddPeer(p Peer) { // Let the ensurePeersRoutine handle asking for more // peers when we need - we don't trust inbound peers as much. addr := p.NodeInfo().NetAddress() - r.book.AddAddress(addr, addr) + if !isAddrPrivate(addr, r.config.PrivatePeerIDs) { + r.book.AddAddress(addr, addr) + } } } @@ -157,7 +173,8 @@ func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) { func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) { _, msg, err := DecodeMessage(msgBytes) if err != nil { - r.Logger.Error("Error decoding message", "err", err) + r.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + r.Switch.StopPeerForError(src, err) return } r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg) @@ -241,7 +258,7 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { srcAddr := src.NodeInfo().NetAddress() for _, netAddr := range addrs { - if netAddr != nil { + if netAddr != nil && !isAddrPrivate(netAddr, r.config.PrivatePeerIDs) { r.book.AddAddress(netAddr, srcAddr) } } @@ -260,9 +277,17 @@ func (r *PEXReactor) SetEnsurePeersPeriod(d time.Duration) { // Ensures that sufficient peers are connected. (continuous) func (r *PEXReactor) ensurePeersRoutine() { - // Randomize when routine starts - ensurePeersPeriodMs := r.ensurePeersPeriod.Nanoseconds() / 1e6 - time.Sleep(time.Duration(rand.Int63n(ensurePeersPeriodMs)) * time.Millisecond) + var ( + seed = rand.New(rand.NewSource(time.Now().UnixNano())) + jitter = seed.Int63n(r.ensurePeersPeriod.Nanoseconds()) + ) + + // Randomize first round of communication to avoid thundering herd. + // If no potential peers are present directly start connecting so we guarantee + // swift setup with the help of configured seeds. + if r.hasPotentialPeers() { + time.Sleep(time.Duration(jitter)) + } // fire once immediately. // ensures we dial the seeds right away if the book is empty @@ -287,9 +312,18 @@ func (r *PEXReactor) ensurePeersRoutine() { // the node operator. It should not be used to compute what addresses are // already connected or not. func (r *PEXReactor) ensurePeers() { - numOutPeers, numInPeers, numDialing := r.Switch.NumPeers() - numToDial := defaultMinNumOutboundPeers - (numOutPeers + numDialing) - r.Logger.Info("Ensure peers", "numOutPeers", numOutPeers, "numDialing", numDialing, "numToDial", numToDial) + var ( + out, in, dial = r.Switch.NumPeers() + numToDial = defaultMinNumOutboundPeers - (out + dial) + ) + r.Logger.Info( + "Ensure peers", + "numOutPeers", out, + "numInPeers", in, + "numDialing", dial, + "numToDial", numToDial, + ) + if numToDial <= 0 { return } @@ -297,11 +331,12 @@ func (r *PEXReactor) ensurePeers() { // bias to prefer more vetted peers when we have fewer connections. // not perfect, but somewhate ensures that we prioritize connecting to more-vetted // NOTE: range here is [10, 90]. Too high ? - newBias := cmn.MinInt(numOutPeers, 8)*10 + 10 + newBias := cmn.MinInt(out, 8)*10 + 10 toDial := make(map[p2p.ID]*p2p.NetAddress) // Try maxAttempts times to pick numToDial addresses to dial maxAttempts := numToDial * 3 + for i := 0; i < maxAttempts && len(toDial) < numToDial; i++ { try := r.book.PickAddress(newBias) if try == nil { @@ -321,18 +356,8 @@ func (r *PEXReactor) ensurePeers() { } // Dial picked addresses - for _, item := range toDial { - go func(picked *p2p.NetAddress) { - _, err := r.Switch.DialPeerWithAddress(picked, false) - if err != nil { - // TODO: detect more "bad peer" scenarios - if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok { - r.book.MarkBad(picked) - } else { - r.book.MarkAttempt(picked) - } - } - }(item) + for _, addr := range toDial { + go r.dialPeer(addr) } // If we need more addresses, pick a random peer and ask for more. @@ -347,12 +372,58 @@ func (r *PEXReactor) ensurePeers() { } // If we are not connected to nor dialing anybody, fallback to dialing a seed. - if numOutPeers+numInPeers+numDialing+len(toDial) == 0 { + if out+in+dial+len(toDial) == 0 { r.Logger.Info("No addresses to dial nor connected peers. Falling back to seeds") r.dialSeeds() } } +func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) { + var attempts int + var lastDialed time.Time + if lAttempts, attempted := r.attemptsToDial.Load(addr.DialString()); attempted { + attempts = lAttempts.(_attemptsToDial).number + lastDialed = lAttempts.(_attemptsToDial).lastDialed + } + + if attempts > maxAttemptsToDial { + r.Logger.Error("Reached max attempts to dial", "addr", addr, "attempts", attempts) + r.book.MarkBad(addr) + return + } + + // exponential backoff if it's not our first attempt to dial given address + if attempts > 0 { + jitterSeconds := time.Duration(rand.Float64() * float64(time.Second)) // 1s == (1e9 ns) + backoffDuration := jitterSeconds + ((1 << uint(attempts)) * time.Second) + sinceLastDialed := time.Since(lastDialed) + if sinceLastDialed < backoffDuration { + r.Logger.Debug("Too early to dial", "addr", addr, "backoff_duration", backoffDuration, "last_dialed", lastDialed, "time_since", sinceLastDialed) + return + } + } + + err := r.Switch.DialPeerWithAddress(addr, false) + if err != nil { + r.Logger.Error("Dialing failed", "addr", addr, "err", err, "attempts", attempts) + // TODO: detect more "bad peer" scenarios + if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok { + r.book.MarkBad(addr) + r.attemptsToDial.Delete(addr.DialString()) + } else { + r.book.MarkAttempt(addr) + // FIXME: if the addr is going to be removed from the addrbook (hard to + // tell at this point), we need to Delete it from attemptsToDial, not + // record another attempt. + // record attempt + r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()}) + } + } else { + // cleanup any history + r.attemptsToDial.Delete(addr.DialString()) + } +} + // check seed addresses are well formed func (r *PEXReactor) checkSeeds() error { lSeeds := len(r.config.Seeds) @@ -381,17 +452,26 @@ func (r *PEXReactor) dialSeeds() { for _, i := range perm { // dial a random seed seedAddr := seedAddrs[i] - peer, err := r.Switch.DialPeerWithAddress(seedAddr, false) - if err != nil { - r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) - } else { - r.Switch.Logger.Info("Connected to seed", "peer", peer) + err := r.Switch.DialPeerWithAddress(seedAddr, false) + if err == nil { return } + r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) } r.Switch.Logger.Error("Couldn't connect to any seeds") } +// AttemptsToDial returns the number of attempts to dial specific address. It +// returns 0 if never attempted or successfully connected. +func (r *PEXReactor) AttemptsToDial(addr *p2p.NetAddress) int { + lAttempts, attempted := r.attemptsToDial.Load(addr.DialString()) + if attempted { + return lAttempts.(_attemptsToDial).number + } else { + return 0 + } +} + //---------------------------------------------------------- // Explores the network searching for more peers. (continuous) @@ -415,6 +495,14 @@ func (r *PEXReactor) crawlPeersRoutine() { } } +// hasPotentialPeers indicates if there is a potential peer to connect to, by +// consulting the Switch as well as the AddrBook. +func (r *PEXReactor) hasPotentialPeers() bool { + out, in, dial := r.Switch.NumPeers() + + return out+in+dial > 0 && len(r.book.ListOfKnownAddresses()) > 0 +} + // crawlPeerInfo handles temporary data needed for the // network crawling performed during seed/crawler mode. type crawlPeerInfo struct { @@ -470,7 +558,7 @@ func (r *PEXReactor) crawlPeers() { continue } // Otherwise, attempt to connect with the known address - _, err := r.Switch.DialPeerWithAddress(pi.Addr, false) + err := r.Switch.DialPeerWithAddress(pi.Addr, false) if err != nil { r.book.MarkAttempt(pi.Addr) continue @@ -502,6 +590,16 @@ func (r *PEXReactor) attemptDisconnects() { } } +// isAddrPrivate returns true if addr is private. +func isAddrPrivate(addr *p2p.NetAddress, privatePeerIDs []string) bool { + for _, id := range privatePeerIDs { + if string(addr.ID) == id { + return true + } + } + return false +} + //----------------------------------------------------------------------------- // Messages diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 82dafecd4..20276ec86 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "testing" "time" @@ -12,12 +13,11 @@ import ( crypto "github.com/tendermint/go-crypto" wire "github.com/tendermint/go-wire" - cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/log" - cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/p2p/conn" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" ) var ( @@ -30,49 +30,33 @@ func init() { } func TestPEXReactorBasic(t *testing.T) { - assert, require := assert.New(t), require.New(t) + r, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - r.SetLogger(log.TestingLogger()) - - assert.NotNil(r) - assert.NotEmpty(r.GetChannels()) + assert.NotNil(t, r) + assert.NotEmpty(t, r.GetChannels()) } func TestPEXReactorAddRemovePeer(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - r.SetLogger(log.TestingLogger()) + r, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) size := book.Size() peer := p2p.CreateRandomPeer(false) r.AddPeer(peer) - assert.Equal(size+1, book.Size()) + assert.Equal(t, size+1, book.Size()) r.RemovePeer(peer, "peer not available") - assert.Equal(size+1, book.Size()) + assert.Equal(t, size+1, book.Size()) outboundPeer := p2p.CreateRandomPeer(true) r.AddPeer(outboundPeer) - assert.Equal(size+1, book.Size(), "outbound peers should not be added to the address book") + assert.Equal(t, size+1, book.Size(), "outbound peers should not be added to the address book") r.RemovePeer(outboundPeer, "peer not available") - assert.Equal(size+1, book.Size()) + assert.Equal(t, size+1, book.Size()) } func TestPEXReactorRunning(t *testing.T) { @@ -82,7 +66,7 @@ func TestPEXReactorRunning(t *testing.T) { dir, err := ioutil.TempDir("", "pex_reactor") require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) + book := NewAddrBook(filepath.Join(dir, "addrbook.json"), false) book.SetLogger(log.TestingLogger()) // create switches @@ -100,7 +84,7 @@ func TestPEXReactorRunning(t *testing.T) { // fill the address book and add listeners for _, s := range switches { - addr, _ := p2p.NewNetAddressString(s.NodeInfo().ListenAddr) + addr := s.NodeInfo().NetAddress() book.AddAddress(addr, addr) s.AddListener(p2p.NewDefaultListener("tcp", s.NodeInfo().ListenAddr, true, log.TestingLogger())) } @@ -119,50 +103,9 @@ func TestPEXReactorRunning(t *testing.T) { } } -func assertPeersWithTimeout(t *testing.T, switches []*p2p.Switch, checkPeriod, timeout time.Duration, nPeers int) { - ticker := time.NewTicker(checkPeriod) - remaining := timeout - for { - select { - case <-ticker.C: - // check peers are connected - allGood := true - for _, s := range switches { - outbound, inbound, _ := s.NumPeers() - if outbound+inbound < nPeers { - allGood = false - } - } - remaining -= checkPeriod - if remaining < 0 { - remaining = 0 - } - if allGood { - return - } - case <-time.After(remaining): - numPeersStr := "" - for i, s := range switches { - outbound, inbound, _ := s.NumPeers() - numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound) - } - t.Errorf("expected all switches to be connected to at least one peer (switches: %s)", numPeersStr) - return - } - } -} - func TestPEXReactorReceive(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - r.SetLogger(log.TestingLogger()) + r, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) peer := p2p.CreateRandomPeer(false) @@ -173,89 +116,69 @@ func TestPEXReactorReceive(t *testing.T) { addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()} msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) r.Receive(PexChannel, peer, msg) - assert.Equal(size+1, book.Size()) + assert.Equal(t, size+1, book.Size()) msg = wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) r.Receive(PexChannel, peer, msg) } func TestPEXReactorRequestMessageAbuse(t *testing.T) { - assert, require := assert.New(t), require.New(t) + r, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - sw := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) - sw.SetLogger(log.TestingLogger()) - sw.AddReactor("PEX", r) - r.SetSwitch(sw) - r.SetLogger(log.TestingLogger()) + sw := createSwitchAndAddReactors(r) peer := newMockPeer() p2p.AddPeerToSwitch(sw, peer) - assert.True(sw.Peers().Has(peer.ID())) + assert.True(t, sw.Peers().Has(peer.ID())) id := string(peer.ID()) msg := wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) // first time creates the entry r.Receive(PexChannel, peer, msg) - assert.True(r.lastReceivedRequests.Has(id)) - assert.True(sw.Peers().Has(peer.ID())) + assert.True(t, r.lastReceivedRequests.Has(id)) + assert.True(t, sw.Peers().Has(peer.ID())) // next time sets the last time value r.Receive(PexChannel, peer, msg) - assert.True(r.lastReceivedRequests.Has(id)) - assert.True(sw.Peers().Has(peer.ID())) + assert.True(t, r.lastReceivedRequests.Has(id)) + assert.True(t, sw.Peers().Has(peer.ID())) // third time is too many too soon - peer is removed r.Receive(PexChannel, peer, msg) - assert.False(r.lastReceivedRequests.Has(id)) - assert.False(sw.Peers().Has(peer.ID())) + assert.False(t, r.lastReceivedRequests.Has(id)) + assert.False(t, sw.Peers().Has(peer.ID())) } func TestPEXReactorAddrsMessageAbuse(t *testing.T) { - assert, require := assert.New(t), require.New(t) + r, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - sw := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) - sw.SetLogger(log.TestingLogger()) - sw.AddReactor("PEX", r) - r.SetSwitch(sw) - r.SetLogger(log.TestingLogger()) + sw := createSwitchAndAddReactors(r) peer := newMockPeer() p2p.AddPeerToSwitch(sw, peer) - assert.True(sw.Peers().Has(peer.ID())) + assert.True(t, sw.Peers().Has(peer.ID())) id := string(peer.ID()) // request addrs from the peer r.RequestAddrs(peer) - assert.True(r.requestsSent.Has(id)) - assert.True(sw.Peers().Has(peer.ID())) + assert.True(t, r.requestsSent.Has(id)) + assert.True(t, sw.Peers().Has(peer.ID())) addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()} msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) // receive some addrs. should clear the request r.Receive(PexChannel, peer, msg) - assert.False(r.requestsSent.Has(id)) - assert.True(sw.Peers().Has(peer.ID())) + assert.False(t, r.requestsSent.Has(id)) + assert.True(t, sw.Peers().Has(peer.ID())) // receiving more addrs causes a disconnect r.Receive(PexChannel, peer, msg) - assert.False(sw.Peers().Has(peer.ID())) + assert.False(t, sw.Peers().Has(peer.ID())) } func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) { @@ -263,59 +186,68 @@ func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) { require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) + book := NewAddrBook(filepath.Join(dir, "addrbook.json"), false) book.SetLogger(log.TestingLogger()) // 1. create seed - seed := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { - sw.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{}) - r.SetLogger(log.TestingLogger()) - r.SetEnsurePeersPeriod(250 * time.Millisecond) - sw.AddReactor("pex", r) - return sw - }) - seed.AddListener(p2p.NewDefaultListener("tcp", seed.NodeInfo().ListenAddr, true, log.TestingLogger())) - err = seed.Start() - require.Nil(t, err) + seed := p2p.MakeSwitch( + config, + 0, + "127.0.0.1", + "123.123.123", + func(i int, sw *p2p.Switch) *p2p.Switch { + sw.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + sw.AddReactor("pex", r) + return sw + }, + ) + seed.AddListener( + p2p.NewDefaultListener( + "tcp", + seed.NodeInfo().ListenAddr, + true, + log.TestingLogger(), + ), + ) + require.Nil(t, seed.Start()) defer seed.Stop() - // 2. create usual peer - sw := p2p.MakeSwitch(config, 1, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { - sw.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book, &PEXReactorConfig{Seeds: []string{seed.NodeInfo().ListenAddr}}) - r.SetLogger(log.TestingLogger()) - r.SetEnsurePeersPeriod(250 * time.Millisecond) - sw.AddReactor("pex", r) - return sw - }) - err = sw.Start() - require.Nil(t, err) - defer sw.Stop() + // 2. create usual peer with only seed configured. + peer := p2p.MakeSwitch( + config, + 1, + "127.0.0.1", + "123.123.123", + func(i int, sw *p2p.Switch) *p2p.Switch { + sw.SetLogger(log.TestingLogger()) + + r := NewPEXReactor( + book, + &PEXReactorConfig{ + Seeds: []string{seed.NodeInfo().NetAddress().String()}, + }, + ) + r.SetLogger(log.TestingLogger()) + sw.AddReactor("pex", r) + return sw + }, + ) + require.Nil(t, peer.Start()) + defer peer.Stop() - // 3. check that peer at least connects to seed - assertPeersWithTimeout(t, []*p2p.Switch{sw}, 10*time.Millisecond, 10*time.Second, 1) + // 3. check that the peer connects to seed immediately + assertPeersWithTimeout(t, []*p2p.Switch{peer}, 10*time.Millisecond, 1*time.Second, 1) } func TestPEXReactorCrawlStatus(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) - book.SetLogger(log.TestingLogger()) + pexR, book := createReactor(&PEXReactorConfig{SeedMode: true}) + defer teardownReactor(book) - pexR := NewPEXReactor(book, &PEXReactorConfig{SeedMode: true}) // Seed/Crawler mode uses data from the Switch - p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { - pexR.SetLogger(log.TestingLogger()) - sw.SetLogger(log.TestingLogger().With("switch", i)) - sw.AddReactor("pex", pexR) - return sw - }) + _ = createSwitchAndAddReactors(pexR) // Create a peer, add it to the peer set and the addrbook. peer := p2p.CreateRandomPeer(false) @@ -331,11 +263,62 @@ func TestPEXReactorCrawlStatus(t *testing.T) { peerInfos := pexR.getPeersToCrawl() // Make sure it has the proper number of elements - assert.Equal(2, len(peerInfos)) + assert.Equal(t, 2, len(peerInfos)) // TODO: test } +func TestPEXReactorDoesNotAddPrivatePeersToAddrBook(t *testing.T) { + peer := p2p.CreateRandomPeer(false) + + pexR, book := createReactor(&PEXReactorConfig{PrivatePeerIDs: []string{string(peer.NodeInfo().ID())}}) + defer teardownReactor(book) + + // we have to send a request to receive responses + pexR.RequestAddrs(peer) + + size := book.Size() + addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()} + msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) + pexR.Receive(PexChannel, peer, msg) + assert.Equal(t, size, book.Size()) + + pexR.AddPeer(peer) + assert.Equal(t, size, book.Size()) +} + +func TestPEXReactorDialPeer(t *testing.T) { + pexR, book := createReactor(&PEXReactorConfig{}) + defer teardownReactor(book) + + _ = createSwitchAndAddReactors(pexR) + + peer := newMockPeer() + addr := peer.NodeInfo().NetAddress() + + assert.Equal(t, 0, pexR.AttemptsToDial(addr)) + + // 1st unsuccessful attempt + pexR.dialPeer(addr) + + assert.Equal(t, 1, pexR.AttemptsToDial(addr)) + + // 2nd unsuccessful attempt + pexR.dialPeer(addr) + + // must be skipped because it is too early + assert.Equal(t, 1, pexR.AttemptsToDial(addr)) + + if !testing.Short() { + time.Sleep(3 * time.Second) + + // 3rd attempt + pexR.dialPeer(addr) + + assert.Equal(t, 2, pexR.AttemptsToDial(addr)) + } +} + type mockPeer struct { *cmn.BaseService pubKey crypto.PubKey @@ -368,3 +351,77 @@ func (mp mockPeer) Send(byte, interface{}) bool { return false } func (mp mockPeer) TrySend(byte, interface{}) bool { return false } func (mp mockPeer) Set(string, interface{}) {} func (mp mockPeer) Get(string) interface{} { return nil } + +func assertPeersWithTimeout( + t *testing.T, + switches []*p2p.Switch, + checkPeriod, timeout time.Duration, + nPeers int, +) { + var ( + ticker = time.NewTicker(checkPeriod) + remaining = timeout + ) + + for { + select { + case <-ticker.C: + // check peers are connected + allGood := true + for _, s := range switches { + outbound, inbound, _ := s.NumPeers() + if outbound+inbound < nPeers { + allGood = false + } + } + remaining -= checkPeriod + if remaining < 0 { + remaining = 0 + } + if allGood { + return + } + case <-time.After(remaining): + numPeersStr := "" + for i, s := range switches { + outbound, inbound, _ := s.NumPeers() + numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound) + } + t.Errorf( + "expected all switches to be connected to at least one peer (switches: %s)", + numPeersStr, + ) + return + } + } +} + +func createReactor(config *PEXReactorConfig) (r *PEXReactor, book *addrBook) { + dir, err := ioutil.TempDir("", "pex_reactor") + if err != nil { + panic(err) + } + book = NewAddrBook(filepath.Join(dir, "addrbook.json"), true) + book.SetLogger(log.TestingLogger()) + + r = NewPEXReactor(book, config) + r.SetLogger(log.TestingLogger()) + return +} + +func teardownReactor(book *addrBook) { + err := os.RemoveAll(filepath.Dir(book.FilePath())) + if err != nil { + panic(err) + } +} + +func createSwitchAndAddReactors(reactors ...p2p.Reactor) *p2p.Switch { + sw := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) + sw.SetLogger(log.TestingLogger()) + for _, r := range reactors { + sw.AddReactor(r.String(), r) + r.SetSwitch(sw) + } + return sw +} diff --git a/p2p/switch.go b/p2p/switch.go index f1d02dcfc..fa037a9b8 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" - crypto "github.com/tendermint/go-crypto" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/p2p/conn" cmn "github.com/tendermint/tmlibs/common" @@ -36,6 +35,7 @@ const ( type AddrBook interface { AddAddress(addr *NetAddress, src *NetAddress) error + MarkGood(*NetAddress) Save() } @@ -58,9 +58,10 @@ type Switch struct { dialing *cmn.CMap nodeInfo NodeInfo // our node info nodeKey *NodeKey // our node privkey + addrBook AddrBook - filterConnByAddr func(net.Addr) error - filterConnByPubKey func(crypto.PubKey) error + filterConnByAddr func(net.Addr) error + filterConnByID func(ID) error rng *rand.Rand // seed for randomizing dial times and orders } @@ -85,6 +86,7 @@ func NewSwitch(config *cfg.P2PConfig) *Switch { sw.peerConfig.MConfig.SendRate = config.SendRate sw.peerConfig.MConfig.RecvRate = config.RecvRate sw.peerConfig.MConfig.MaxMsgPacketPayloadSize = config.MaxMsgPacketPayloadSize + sw.peerConfig.AuthEnc = config.AuthEnc sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw) return sw @@ -287,14 +289,13 @@ func (sw *Switch) reconnectToPeer(peer Peer) { return } - peer, err := sw.DialPeerWithAddress(netAddr, true) + err := sw.DialPeerWithAddress(netAddr, true) if err != nil { sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "peer", peer) // sleep a set amount sw.randomSleep(reconnectInterval) continue } else { - sw.Logger.Info("Reconnected to peer", "peer", peer) return } } @@ -309,18 +310,28 @@ func (sw *Switch) reconnectToPeer(peer Peer) { // sleep an exponentially increasing amount sleepIntervalSeconds := math.Pow(reconnectBackOffBaseSeconds, float64(i)) sw.randomSleep(time.Duration(sleepIntervalSeconds) * time.Second) - peer, err := sw.DialPeerWithAddress(netAddr, true) - if err != nil { - sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "peer", peer) - continue - } else { - sw.Logger.Info("Reconnected to peer", "peer", peer) - return + err := sw.DialPeerWithAddress(netAddr, true) + if err == nil { + return // success } + sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "peer", peer) } sw.Logger.Error("Failed to reconnect to peer. Giving up", "peer", peer, "elapsed", time.Since(start)) } +// SetAddrBook allows to set address book on Switch. +func (sw *Switch) SetAddrBook(addrBook AddrBook) { + sw.addrBook = addrBook +} + +// MarkPeerAsGood marks the given peer as good when it did something useful +// like contributed to consensus. +func (sw *Switch) MarkPeerAsGood(peer Peer) { + if sw.addrBook != nil { + sw.addrBook.MarkGood(peer.NodeInfo().NetAddress()) + } +} + //--------------------------------------------------------------------- // Dialing @@ -358,11 +369,9 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b go func(i int) { sw.randomSleep(0) j := perm[i] - peer, err := sw.DialPeerWithAddress(netAddrs[j], persistent) + err := sw.DialPeerWithAddress(netAddrs[j], persistent) if err != nil { sw.Logger.Error("Error dialing peer", "err", err) - } else { - sw.Logger.Info("Connected to peer", "peer", peer) } }(i) } @@ -371,7 +380,7 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b // DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects and authenticates successfully. // If `persistent == true`, the switch will always try to reconnect to this peer if the connection ever fails. -func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (Peer, error) { +func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) error { sw.dialing.Set(string(addr.ID), addr) defer sw.dialing.Delete(string(addr.ID)) return sw.addOutboundPeerWithConfig(addr, sw.peerConfig, persistent) @@ -394,10 +403,10 @@ func (sw *Switch) FilterConnByAddr(addr net.Addr) error { return nil } -// FilterConnByPubKey returns an error if connecting to the given public key is forbidden. -func (sw *Switch) FilterConnByPubKey(pubkey crypto.PubKey) error { - if sw.filterConnByPubKey != nil { - return sw.filterConnByPubKey(pubkey) +// FilterConnByID returns an error if connecting to the given peer ID is forbidden. +func (sw *Switch) FilterConnByID(id ID) error { + if sw.filterConnByID != nil { + return sw.filterConnByID(id) } return nil @@ -408,9 +417,9 @@ func (sw *Switch) SetAddrFilter(f func(net.Addr) error) { sw.filterConnByAddr = f } -// SetPubKeyFilter sets the function for filtering connections by public key. -func (sw *Switch) SetPubKeyFilter(f func(crypto.PubKey) error) { - sw.filterConnByPubKey = f +// SetIDFilter sets the function for filtering connections by peer ID. +func (sw *Switch) SetIDFilter(f func(ID) error) { + sw.filterConnByID = f } //------------------------------------------------------------------------------------ @@ -441,14 +450,13 @@ func (sw *Switch) listenerRoutine(l Listener) { } func (sw *Switch) addInboundPeerWithConfig(conn net.Conn, config *PeerConfig) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, config) + peerConn, err := newInboundPeerConn(conn, config, sw.nodeKey.PrivKey) if err != nil { conn.Close() // peer is nil return err } - peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) - if err = sw.addPeer(peer); err != nil { - peer.CloseConn() + if err = sw.addPeer(peerConn); err != nil { + peerConn.CloseConn() return err } @@ -457,31 +465,20 @@ func (sw *Switch) addInboundPeerWithConfig(conn net.Conn, config *PeerConfig) er // dial the peer; make secret connection; authenticate against the dialed ID; // add the peer. -func (sw *Switch) addOutboundPeerWithConfig(addr *NetAddress, config *PeerConfig, persistent bool) (Peer, error) { +func (sw *Switch) addOutboundPeerWithConfig(addr *NetAddress, config *PeerConfig, persistent bool) error { sw.Logger.Info("Dialing peer", "address", addr) - peer, err := newOutboundPeer(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, config, persistent) + peerConn, err := newOutboundPeerConn(addr, config, persistent, sw.nodeKey.PrivKey) if err != nil { sw.Logger.Error("Failed to dial peer", "address", addr, "err", err) - return nil, err - } - peer.SetLogger(sw.Logger.With("peer", addr)) - - // authenticate peer - if addr.ID == "" { - peer.Logger.Info("Dialed peer with unknown ID - unable to authenticate", "addr", addr) - } else if addr.ID != peer.ID() { - peer.CloseConn() - return nil, ErrSwitchAuthenticationFailure{addr, peer.ID()} + return err } - err = sw.addPeer(peer) - if err != nil { + if err := sw.addPeer(peerConn); err != nil { sw.Logger.Error("Failed to add peer", "address", addr, "err", err) - peer.CloseConn() - return nil, err + peerConn.CloseConn() + return err } - sw.Logger.Info("Dialed and added peer", "address", addr, "peer", peer) - return peer, nil + return nil } // addPeer performs the Tendermint P2P handshake with a peer @@ -489,44 +486,70 @@ func (sw *Switch) addOutboundPeerWithConfig(addr *NetAddress, config *PeerConfig // it starts the peer and adds it to the switch. // NOTE: This performs a blocking handshake before the peer is added. // NOTE: If error is returned, caller is responsible for calling peer.CloseConn() -func (sw *Switch) addPeer(peer *peer) error { - // Avoid self - if sw.nodeKey.ID() == peer.ID() { - return ErrSwitchConnectToSelf +func (sw *Switch) addPeer(pc peerConn) error { + + addr := pc.conn.RemoteAddr() + if err := sw.FilterConnByAddr(addr); err != nil { + return err } - // Avoid duplicate - if sw.peers.Has(peer.ID()) { - return ErrSwitchDuplicatePeer + // NOTE: if AuthEnc==false, we don't have a peerID until after the handshake. + // If AuthEnc==true then we already know the ID and could do the checks first before the handshake, + // but it's simple to just deal with both cases the same after the handshake. + // Exchange NodeInfo on the conn + peerNodeInfo, err := pc.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.peerConfig.HandshakeTimeout*time.Second)) + if err != nil { + return err } - // Filter peer against white list - if err := sw.FilterConnByAddr(peer.Addr()); err != nil { - return err + peerID := peerNodeInfo.ID() + + // ensure connection key matches self reported key + if pc.config.AuthEnc { + connID := pc.ID() + + if peerID != connID { + return fmt.Errorf("nodeInfo.ID() (%v) doesn't match conn.ID() (%v)", + peerID, connID) + } } - if err := sw.FilterConnByPubKey(peer.PubKey()); err != nil { + + // Validate the peers nodeInfo + if err := peerNodeInfo.Validate(); err != nil { return err } - // Exchange NodeInfo with the peer - if err := peer.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.peerConfig.HandshakeTimeout*time.Second)); err != nil { - return err + // Avoid self + if sw.nodeKey.ID() == peerID { + return ErrSwitchConnectToSelf } - // Validate the peers nodeInfo against the pubkey - if err := peer.NodeInfo().Validate(peer.PubKey()); err != nil { + // Avoid duplicate + if sw.peers.Has(peerID) { + return ErrSwitchDuplicatePeer + } + + // Filter peer against ID white list + if err := sw.FilterConnByID(peerID); err != nil { return err } // Check version, chain id - if err := sw.nodeInfo.CompatibleWith(peer.NodeInfo()); err != nil { + if err := sw.nodeInfo.CompatibleWith(peerNodeInfo); err != nil { return err } + peer := newPeer(pc, peerNodeInfo, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError) + peer.SetLogger(sw.Logger.With("peer", addr)) + + peer.Logger.Info("Successful handshake with peer", "peerNodeInfo", peerNodeInfo) + // All good. Start peer if sw.IsRunning() { - sw.startInitPeer(peer) + if err = sw.startInitPeer(peer); err != nil { + return err + } } // Add the peer to .peers. @@ -540,14 +563,17 @@ func (sw *Switch) addPeer(peer *peer) error { return nil } -func (sw *Switch) startInitPeer(peer *peer) { +func (sw *Switch) startInitPeer(peer *peer) error { err := peer.Start() // spawn send/recv routines if err != nil { // Should never happen sw.Logger.Error("Error starting peer", "peer", peer, "err", err) + return err } for _, reactor := range sw.reactors { reactor.AddPeer(peer) } + + return nil } diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 745eb44e6..06e8b642e 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -192,7 +192,7 @@ func assertNoPeersAfterTimeout(t *testing.T, sw *Switch, timeout time.Duration) } } -func TestConnPubKeyFilter(t *testing.T) { +func TestConnIDFilter(t *testing.T) { s1 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) s2 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) defer s1.Stop() @@ -200,15 +200,20 @@ func TestConnPubKeyFilter(t *testing.T) { c1, c2 := conn.NetPipe() - // set pubkey filter - s1.SetPubKeyFilter(func(pubkey crypto.PubKey) error { - if bytes.Equal(pubkey.Bytes(), s2.nodeInfo.PubKey.Bytes()) { + s1.SetIDFilter(func(id ID) error { + if id == PubKeyToID(s2.nodeInfo.PubKey) { + return fmt.Errorf("Error: pipe is blacklisted") + } + return nil + }) + + s2.SetIDFilter(func(id ID) error { + if id == PubKeyToID(s1.nodeInfo.PubKey) { return fmt.Errorf("Error: pipe is blacklisted") } return nil }) - // connect to good peer go func() { err := s1.addPeerWithConnection(c1) assert.NotNil(t, err, "expected error") @@ -237,13 +242,16 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig(), false) + pc, err := newOutboundPeerConn(rp.Addr(), DefaultPeerConfig(), false, sw.nodeKey.PrivKey) require.Nil(err) - err = sw.addPeer(peer) + err = sw.addPeer(pc) require.Nil(err) + peer := sw.Peers().Get(rp.ID()) + require.NotNil(peer) + // simulate failure by closing connection - peer.CloseConn() + pc.CloseConn() assertNoPeersAfterTimeout(t, sw, 100*time.Millisecond) assert.False(peer.IsRunning()) @@ -264,13 +272,17 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig(), true) - require.Nil(err) - err = sw.addPeer(peer) + pc, err := newOutboundPeerConn(rp.Addr(), DefaultPeerConfig(), true, sw.nodeKey.PrivKey) + // sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, require.Nil(err) + require.Nil(sw.addPeer(pc)) + + peer := sw.Peers().Get(rp.ID()) + require.NotNil(peer) + // simulate failure by closing connection - peer.CloseConn() + pc.CloseConn() // TODO: remove sleep, detect the disconnection, wait for reconnect npeers := sw.Peers().Size() diff --git a/p2p/test_util.go b/p2p/test_util.go index dea48dfd3..0d6427f43 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -19,12 +19,14 @@ func AddPeerToSwitch(sw *Switch, peer Peer) { func CreateRandomPeer(outbound bool) *peer { addr, netAddr := CreateRoutableAddr() p := &peer{ + peerConn: peerConn{ + outbound: outbound, + }, nodeInfo: NodeInfo{ ListenAddr: netAddr.DialString(), PubKey: crypto.GenPrivKeyEd25519().Wrap().PubKey(), }, - outbound: outbound, - mconn: &conn.MConnection{}, + mconn: &conn.MConnection{}, } p.SetLogger(log.TestingLogger().With("peer", addr)) return p @@ -98,16 +100,15 @@ func Connect2Switches(switches []*Switch, i, j int) { } func (sw *Switch) addPeerWithConnection(conn net.Conn) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, sw.peerConfig) + pc, err := newInboundPeerConn(conn, sw.peerConfig, sw.nodeKey.PrivKey) if err != nil { if err := conn.Close(); err != nil { sw.Logger.Error("Error closing connection", "err", err) } return err } - peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) - if err = sw.addPeer(peer); err != nil { - peer.CloseConn() + if err = sw.addPeer(pc); err != nil { + pc.CloseConn() return err } diff --git a/proxy/app_conn_test.go b/proxy/app_conn_test.go index 0fbad6022..7eb3831cd 100644 --- a/proxy/app_conn_test.go +++ b/proxy/app_conn_test.go @@ -5,7 +5,7 @@ import ( "testing" abcicli "github.com/tendermint/abci/client" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/tendermint/abci/server" "github.com/tendermint/abci/types" cmn "github.com/tendermint/tmlibs/common" @@ -49,7 +49,7 @@ func TestEcho(t *testing.T) { clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server - s := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) + s := server.NewSocketServer(sockPath, kvstore.NewKVStoreApplication()) s.SetLogger(log.TestingLogger().With("module", "abci-server")) if err := s.Start(); err != nil { t.Fatalf("Error starting socket server: %v", err.Error()) @@ -83,7 +83,7 @@ func BenchmarkEcho(b *testing.B) { clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server - s := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) + s := server.NewSocketServer(sockPath, kvstore.NewKVStoreApplication()) s.SetLogger(log.TestingLogger().With("module", "abci-server")) if err := s.Start(); err != nil { b.Fatalf("Error starting socket server: %v", err.Error()) @@ -122,7 +122,7 @@ func TestInfo(t *testing.T) { clientCreator := NewRemoteClientCreator(sockPath, SOCKET, true) // Start server - s := server.NewSocketServer(sockPath, dummy.NewDummyApplication()) + s := server.NewSocketServer(sockPath, kvstore.NewKVStoreApplication()) s.SetLogger(log.TestingLogger().With("module", "abci-server")) if err := s.Start(); err != nil { t.Fatalf("Error starting socket server: %v", err.Error()) diff --git a/proxy/client.go b/proxy/client.go index a70da1cae..6c987368a 100644 --- a/proxy/client.go +++ b/proxy/client.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" abcicli "github.com/tendermint/abci/client" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/tendermint/abci/types" ) @@ -64,10 +64,14 @@ func (r *remoteClientCreator) NewABCIClient() (abcicli.Client, error) { func DefaultClientCreator(addr, transport, dbDir string) ClientCreator { switch addr { + case "kvstore": + fallthrough case "dummy": - return NewLocalClientCreator(dummy.NewDummyApplication()) + return NewLocalClientCreator(kvstore.NewKVStoreApplication()) + case "persistent_kvstore": + fallthrough case "persistent_dummy": - return NewLocalClientCreator(dummy.NewPersistentDummyApplication(dbDir)) + return NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(dbDir)) case "nilapp": return NewLocalClientCreator(types.NewBaseApplication()) default: diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index bc6cf759e..83d77f317 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -122,6 +122,15 @@ func (c *HTTP) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { return result, nil } +func (c *HTTP) Health() (*ctypes.ResultHealth, error) { + result := new(ctypes.ResultHealth) + _, err := c.rpc.Call("health", map[string]interface{}{}, result) + if err != nil { + return nil, errors.Wrap(err, "Health") + } + return result, nil +} + func (c *HTTP) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { result := new(ctypes.ResultBlockchainInfo) _, err := c.rpc.Call("blockchain", diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 6715b64b5..7a065291e 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -83,6 +83,7 @@ type Client interface { type NetworkClient interface { NetInfo() (*ctypes.ResultNetInfo, error) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) + Health() (*ctypes.ResultHealth, error) } // EventsClient is reactive, you can subscribe to any message, given the proper diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index be989ee1c..7e2f8fedc 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -84,6 +84,10 @@ func (Local) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { return core.DumpConsensusState() } +func (Local) Health() (*ctypes.ResultHealth, error) { + return core.Health() +} + func (Local) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { return core.UnsafeDialSeeds(seeds) } diff --git a/rpc/client/main_test.go b/rpc/client/main_test.go index 7c5acd390..82b5a0195 100644 --- a/rpc/client/main_test.go +++ b/rpc/client/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" nm "github.com/tendermint/tendermint/node" rpctest "github.com/tendermint/tendermint/rpc/test" ) @@ -12,8 +12,8 @@ import ( var node *nm.Node func TestMain(m *testing.M) { - // start a tendermint node (and dummy) in the background to test against - app := dummy.NewDummyApplication() + // start a tendermint node (and kvstore) in the background to test against + app := kvstore.NewKVStoreApplication() node = rpctest.StartTendermint(app) code := m.Run() diff --git a/rpc/client/mock/abci_test.go b/rpc/client/mock/abci_test.go index 5a66c997a..564f0129a 100644 --- a/rpc/client/mock/abci_test.go +++ b/rpc/client/mock/abci_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" abci "github.com/tendermint/abci/types" "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tendermint/rpc/client/mock" @@ -156,7 +156,7 @@ func TestABCIRecorder(t *testing.T) { func TestABCIApp(t *testing.T) { assert, require := assert.New(t), require.New(t) - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() m := mock.ABCIApp{app} // get some info diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 165e4ec26..f23ba6160 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -78,6 +78,15 @@ func TestDumpConsensusState(t *testing.T) { } } +func TestHealth(t *testing.T) { + for i, c := range GetClients() { + nc, ok := c.(client.NetworkClient) + require.True(t, ok, "%d", i) + _, err := nc.Health() + require.Nil(t, err, "%d: %+v", i, err) + } +} + func TestGenesisAndValidators(t *testing.T) { for i, c := range GetClients() { @@ -336,7 +345,7 @@ func TestTxSearch(t *testing.T) { require.Nil(t, err, "%+v", err) require.Len(t, results, 0) - // we query using a tag (see dummy application) + // we query using a tag (see kvstore application) results, err = c.TxSearch("app.creator='jae'", false) require.Nil(t, err, "%+v", err) if len(results) == 0 { diff --git a/rpc/core/doc.go b/rpc/core/doc.go index 5f3e2dced..b479482c4 100644 --- a/rpc/core/doc.go +++ b/rpc/core/doc.go @@ -81,6 +81,7 @@ Available endpoints: /net_info /num_unconfirmed_txs /status +/health /unconfirmed_txs /unsafe_flush_mempool /unsafe_stop_cpu_profiler diff --git a/rpc/core/health.go b/rpc/core/health.go new file mode 100644 index 000000000..ab2ceb16b --- /dev/null +++ b/rpc/core/health.go @@ -0,0 +1,31 @@ +package core + +import ( + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// Get node health. Returns empty result (200 OK) on success, no response - in +// case of an error. +// +// ```shell +// curl 'localhost:46657/health' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") +// result, err := client.Health() +// ``` +// +// > The above command returns JSON structured like this: +// +// ```json +// { +// "error": "", +// "result": {}, +// "id": "", +// "jsonrpc": "2.0" +// } +// ``` +func Health() (*ctypes.ResultHealth, error) { + return &ctypes.ResultHealth{}, nil +} diff --git a/rpc/core/net.go b/rpc/core/net.go index 14e7389d5..9b04926ab 100644 --- a/rpc/core/net.go +++ b/rpc/core/net.go @@ -2,6 +2,7 @@ package core import ( "github.com/pkg/errors" + ctypes "github.com/tendermint/tendermint/rpc/core/types" ) diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 2edb3f3d1..1eb00ceeb 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -3,10 +3,10 @@ package core import ( "time" - crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto" "github.com/tendermint/tendermint/consensus" cstypes "github.com/tendermint/tendermint/consensus/types" - p2p "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/state/txindex" diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 3ea7aa08c..0e10cefee 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -12,6 +12,7 @@ var Routes = map[string]*rpc.RPCFunc{ "unsubscribe_all": rpc.NewWSRPCFunc(UnsubscribeAll, ""), // info API + "health": rpc.NewRPCFunc(Health, ""), "status": rpc.NewRPCFunc(Status, ""), "net_info": rpc.NewRPCFunc(NetInfo, ""), "blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index e13edc843..7567fe650 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -155,3 +155,5 @@ type ResultEvent struct { Query string `json:"query"` Data types.TMEventData `json:"data"` } + +type ResultHealth struct{} diff --git a/rpc/grpc/api.go b/rpc/grpc/api.go index f36b58000..c0a920046 100644 --- a/rpc/grpc/api.go +++ b/rpc/grpc/api.go @@ -11,7 +11,7 @@ type broadcastAPI struct { } func (bapi *broadcastAPI) Ping(ctx context.Context, req *RequestPing) (*ResponsePing, error) { - // dummy so we can check if the server is up + // kvstore so we can check if the server is up return &ResponsePing{}, nil } diff --git a/rpc/grpc/grpc_test.go b/rpc/grpc/grpc_test.go index 030a22b8d..20b3ab7b7 100644 --- a/rpc/grpc/grpc_test.go +++ b/rpc/grpc/grpc_test.go @@ -7,14 +7,14 @@ import ( "github.com/stretchr/testify/require" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" "github.com/tendermint/tendermint/rpc/grpc" "github.com/tendermint/tendermint/rpc/test" ) func TestMain(m *testing.M) { // start a tendermint node in the background to test against - app := dummy.NewDummyApplication() + app := kvstore.NewKVStoreApplication() node := rpctest.StartTendermint(app) code := m.Run() diff --git a/rpc/lib/client/http_client.go b/rpc/lib/client/http_client.go index a1b23a258..902b7eebc 100644 --- a/rpc/lib/client/http_client.go +++ b/rpc/lib/client/http_client.go @@ -42,7 +42,7 @@ func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, protocol = "tcp" } - // replace / with . for http requests (dummy domain) + // replace / with . for http requests (kvstore domain) trimmedAddress := strings.Replace(address, "/", ".", -1) return trimmedAddress, func(proto, addr string) (net.Conn, error) { return net.Dial(protocol, address) @@ -187,7 +187,6 @@ func argsToJson(args map[string]interface{}) error { continue } - // Pass everything else to go-wire data, err := json.Marshal(v) if err != nil { return err diff --git a/rpc/lib/client/ws_client.go b/rpc/lib/client/ws_client.go index ca75ad561..fe15cda21 100644 --- a/rpc/lib/client/ws_client.go +++ b/rpc/lib/client/ws_client.go @@ -254,10 +254,8 @@ func (c *WSClient) reconnect() error { c.mtx.Unlock() }() - // 1s == (1e9 ns) == (1 Billion ns) - billionNs := float64(time.Second.Nanoseconds()) for { - jitterSeconds := time.Duration(rand.Float64() * billionNs) + jitterSeconds := time.Duration(rand.Float64() * float64(time.Second)) // 1s == (1e9 ns) backoffDuration := jitterSeconds + ((1 << uint(attempt)) * time.Second) c.Logger.Info("reconnecting", "attempt", attempt+1, "backoff_duration", backoffDuration) diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 1bac625be..19fc0f6e4 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -746,13 +746,13 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st buf.WriteString("
Available endpoints:
") for _, name := range noArgNames { - link := fmt.Sprintf("http://%s/%s", r.Host, name) + link := fmt.Sprintf("//%s/%s", r.Host, name) buf.WriteString(fmt.Sprintf("%s
", link, link)) } buf.WriteString("
Endpoints that require arguments:
") for _, name := range argNames { - link := fmt.Sprintf("http://%s/%s?", r.Host, name) + link := fmt.Sprintf("//%s/%s?", r.Host, name) funcData := funcMap[name] for i, argName := range funcData.argNames { link += argName + "=_" diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index c8671d68e..b67d76349 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -79,7 +79,7 @@ func GetConfig() *cfg.Config { globalConfig.P2P.ListenAddress = tm globalConfig.RPC.ListenAddress = rpc globalConfig.RPC.GRPCListenAddress = grpc - globalConfig.TxIndex.IndexTags = "app.creator" // see dummy application + globalConfig.TxIndex.IndexTags = "app.creator" // see kvstore application } return globalConfig } diff --git a/scripts/dep_utils/checkout.sh b/scripts/dep_utils/checkout.sh new file mode 100644 index 000000000..1d35e97ab --- /dev/null +++ b/scripts/dep_utils/checkout.sh @@ -0,0 +1,35 @@ +#! /bin/bash + +set -ex + +set +u +if [[ "$DEP" == "" ]]; then + DEP=$GOPATH/src/github.com/tendermint/tendermint/Gopkg.lock +fi +set -u + + +set -u + +function getVendoredVersion() { + grep -A100 "$LIB" "$DEP" | grep revision | head -n1 | grep -o '"[^"]\+"' | cut -d '"' -f 2 +} + + +# fetch and checkout vendored dep + +lib=$1 + +echo "----------------------------------" +echo "Getting $lib ..." +go get -t "github.com/tendermint/$lib/..." + +VENDORED=$(getVendoredVersion "$lib") +cd "$GOPATH/src/github.com/tendermint/$lib" || exit +MASTER=$(git rev-parse origin/master) + +if [[ "$VENDORED" != "$MASTER" ]]; then + echo "... VENDORED != MASTER ($VENDORED != $MASTER)" + echo "... Checking out commit $VENDORED" + git checkout "$VENDORED" &> /dev/null +fi diff --git a/scripts/dep_utils/parse.sh b/scripts/dep_utils/parse.sh new file mode 100644 index 000000000..e6519efa0 --- /dev/null +++ b/scripts/dep_utils/parse.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +set +u +if [[ "$DEP" == "" ]]; then + DEP=$GOPATH/src/github.com/tendermint/tendermint/Gopkg.lock +fi +set -u + + +set -euo pipefail + +LIB=$1 + +grep -A100 "$LIB" "$DEP" | grep revision | head -n1 | grep -o '"[^"]\+"' | cut -d '"' -f 2 diff --git a/scripts/glide/checkout.sh b/scripts/glide/checkout.sh deleted file mode 100644 index 4dcc243ca..000000000 --- a/scripts/glide/checkout.sh +++ /dev/null @@ -1,27 +0,0 @@ -#! /bin/bash -set -u - -function parseGlide() { - cat $1 | grep -A1 $2 | grep -v $2 | awk '{print $2}' -} - - -# fetch and checkout vendored dep - -glide=$1 -lib=$2 - -echo "----------------------------------" -echo "Getting $lib ..." -go get -t github.com/tendermint/$lib/... - -VENDORED=$(parseGlide $glide $lib) -cd $GOPATH/src/github.com/tendermint/$lib -MASTER=$(git rev-parse origin/master) - -if [[ "$VENDORED" != "$MASTER" ]]; then - echo "... VENDORED != MASTER ($VENDORED != $MASTER)" - echo "... Checking out commit $VENDORED" - git checkout $VENDORED &> /dev/null -fi - diff --git a/scripts/glide/parse.sh b/scripts/glide/parse.sh deleted file mode 100644 index e559c368e..000000000 --- a/scripts/glide/parse.sh +++ /dev/null @@ -1,13 +0,0 @@ -#! /bin/bash - -set +u -if [[ "$GLIDE" == "" ]]; then - GLIDE=$GOPATH/src/github.com/tendermint/tendermint/glide.lock -fi -set -u - -set -euo pipefail - -LIB=$1 - -cat $GLIDE | grep -A1 $LIB | grep -v $LIB | awk '{print $2}' diff --git a/scripts/glide/status.sh b/scripts/glide/status.sh deleted file mode 100644 index 40503dbda..000000000 --- a/scripts/glide/status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#! /bin/bash - -# for every github.com/tendermint dependency, warn is if its not synced with origin/master - -if [[ "$GLIDE" == "" ]]; then - GLIDE=$GOPATH/src/github.com/tendermint/tendermint/glide.lock -fi - -# make list of libs -LIBS=($(grep "github.com/tendermint" $GLIDE | awk '{print $3}')) - - -UPTODATE=true -for lib in "${LIBS[@]}"; do - # get vendored commit - VENDORED=`grep -A1 $lib $GLIDE | grep -v $lib | awk '{print $2}'` - PWD=`pwd` - cd $GOPATH/src/$lib - MASTER=`git rev-parse origin/master` - HEAD=`git rev-parse HEAD` - cd $PWD - - if [[ "$VENDORED" != "$MASTER" ]]; then - UPTODATE=false - echo "" - if [[ "$VENDORED" != "$HEAD" ]]; then - echo "Vendored version of $lib differs from origin/master and HEAD" - echo "Vendored: $VENDORED" - echo "Master: $MASTER" - echo "Head: $HEAD" - else - echo "Vendored version of $lib differs from origin/master but matches HEAD" - echo "Vendored: $VENDORED" - echo "Master: $MASTER" - fi - elif [[ "$VENDORED" != "$HEAD" ]]; then - echo "" - echo "Vendored version of $lib matches origin/master but differs from HEAD" - echo "Vendored: $VENDORED" - echo "Head: $HEAD" - fi -done - -if [[ "$UPTODATE" == "true" ]]; then - echo "All vendored versions up to date" -fi - diff --git a/scripts/glide/update.sh b/scripts/glide/update.sh deleted file mode 100644 index 1c0936f28..000000000 --- a/scripts/glide/update.sh +++ /dev/null @@ -1,32 +0,0 @@ -#! /bin/bash -set -euo pipefail -IFS=$'\n\t' - -# script to update the given dependency in the glide.lock file with the checked out branch on the local host - -LIB=$1 - -TMCORE=$GOPATH/src/github.com/tendermint/tendermint -set +u -if [[ "$GLIDE" == "" ]]; then - GLIDE=$TMCORE/glide.lock -fi -set -u - -OLD_COMMIT=`bash $TMCORE/scripts/glide/parse.sh $LIB` - -PWD=`pwd` -cd $GOPATH/src/github.com/tendermint/$LIB - -NEW_COMMIT=$(git rev-parse HEAD) - -cd $PWD - -uname -a | grep Linux > /dev/null -if [[ "$?" == 0 ]]; then - # linux - sed -i "s/$OLD_COMMIT/$NEW_COMMIT/g" $GLIDE -else - # mac - sed -i "" "s/$OLD_COMMIT/$NEW_COMMIT/g" $GLIDE -fi diff --git a/scripts/install_abci_apps.sh b/scripts/install_abci_apps.sh index b2c0b085b..eb70070df 100644 --- a/scripts/install_abci_apps.sh +++ b/scripts/install_abci_apps.sh @@ -1,13 +1,12 @@ #! /bin/bash -go get -d github.com/tendermint/abci - # get the abci commit used by tendermint -COMMIT=`bash scripts/glide/parse.sh abci` - +COMMIT=$(bash scripts/dep_utils/parse.sh abci) echo "Checking out vendored commit for abci: $COMMIT" -cd $GOPATH/src/github.com/tendermint/abci -git checkout $COMMIT -glide install -go install ./cmd/... +go get -d github.com/tendermint/abci +cd "$GOPATH/src/github.com/tendermint/abci" || exit +git checkout "$COMMIT" +make get_tools +make get_vendor_deps +make install diff --git a/state/execution_test.go b/state/execution_test.go index 25d88fe41..e508c0fa8 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/abci/example/dummy" + "github.com/tendermint/abci/example/kvstore" abci "github.com/tendermint/abci/types" crypto "github.com/tendermint/go-crypto" "github.com/tendermint/tendermint/proxy" @@ -25,7 +25,7 @@ var ( ) func TestApplyBlock(t *testing.T) { - cc := proxy.NewLocalClientCreator(dummy.NewDummyApplication()) + cc := proxy.NewLocalClientCreator(kvstore.NewKVStoreApplication()) proxyApp := proxy.NewAppConns(cc, nil) err := proxyApp.Start() require.Nil(t, err) @@ -165,7 +165,7 @@ type testApp struct { ByzantineValidators []abci.Evidence } -func NewDummyApplication() *testApp { +func NewKVStoreApplication() *testApp { return &testApp{} } diff --git a/state/state.go b/state/state.go index 575a1630e..fb5d78c47 100644 --- a/state/state.go +++ b/state/state.go @@ -86,7 +86,11 @@ func (s State) Equals(s2 State) bool { // Bytes serializes the State using go-wire. func (s State) Bytes() []byte { - return wire.BinaryBytes(s) + bz, err := wire.MarshalBinary(s) + if err != nil { + panic(err) + } + return bz } // IsEmpty returns true if the State is equal to the empty State. diff --git a/state/state_test.go b/state/state_test.go index 02c94253e..1c5a860d5 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -78,8 +78,8 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // build mock responses block := makeBlock(state, 2) abciResponses := NewABCIResponses(block) - abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: []cmn.KVPair{}} - abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: []cmn.KVPair{}} + abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}} + abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}} abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.Validator{ { PubKey: crypto.GenPrivKeyEd25519().PubKey().Bytes(), diff --git a/state/store.go b/state/store.go index de2d4d67c..df07ec540 100644 --- a/state/store.go +++ b/state/store.go @@ -1,7 +1,6 @@ package state import ( - "bytes" "fmt" abci "github.com/tendermint/abci/types" @@ -70,12 +69,11 @@ func loadState(db dbm.DB, key []byte) (state State) { return state } - r, n, err := bytes.NewReader(buf), new(int), new(error) - wire.ReadBinaryPtr(&state, r, 0, n, err) - if *err != nil { + err := wire.UnmarshalBinary(buf, &state) + if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED cmn.Exit(cmn.Fmt(`LoadState: Data has been corrupted or its spec has changed: - %v\n`, *err)) + %v\n`, err)) } // TODO: ensure that buf is completely read. @@ -113,7 +111,11 @@ func NewABCIResponses(block *types.Block) *ABCIResponses { // Bytes serializes the ABCIResponse using go-wire func (a *ABCIResponses) Bytes() []byte { - return wire.BinaryBytes(*a) + bz, err := wire.MarshalBinary(*a) + if err != nil { + panic(err) + } + return bz } func (a *ABCIResponses) ResultsHash() []byte { @@ -131,12 +133,11 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { } abciResponses := new(ABCIResponses) - r, n, err := bytes.NewReader(buf), new(int), new(error) - wire.ReadBinaryPtr(abciResponses, r, 0, n, err) - if *err != nil { + err := wire.UnmarshalBinary(buf, abciResponses) + if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED cmn.Exit(cmn.Fmt(`LoadABCIResponses: Data has been corrupted or its spec has - changed: %v\n`, *err)) + changed: %v\n`, err)) } // TODO: ensure that buf is completely read. @@ -160,7 +161,11 @@ type ValidatorsInfo struct { // Bytes serializes the ValidatorsInfo using go-wire func (valInfo *ValidatorsInfo) Bytes() []byte { - return wire.BinaryBytes(*valInfo) + bz, err := wire.MarshalBinary(*valInfo) + if err != nil { + panic(err) + } + return bz } // LoadValidators loads the ValidatorSet for a given height. @@ -189,12 +194,11 @@ func loadValidatorsInfo(db dbm.DB, height int64) *ValidatorsInfo { } v := new(ValidatorsInfo) - r, n, err := bytes.NewReader(buf), new(int), new(error) - wire.ReadBinaryPtr(v, r, 0, n, err) - if *err != nil { + err := wire.UnmarshalBinary(buf, v) + if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED cmn.Exit(cmn.Fmt(`LoadValidators: Data has been corrupted or its spec has changed: - %v\n`, *err)) + %v\n`, err)) } // TODO: ensure that buf is completely read. @@ -225,7 +229,11 @@ type ConsensusParamsInfo struct { // Bytes serializes the ConsensusParamsInfo using go-wire func (params ConsensusParamsInfo) Bytes() []byte { - return wire.BinaryBytes(params) + bz, err := wire.MarshalBinary(params) + if err != nil { + panic(err) + } + return bz } // LoadConsensusParams loads the ConsensusParams for a given height. @@ -255,12 +263,11 @@ func loadConsensusParamsInfo(db dbm.DB, height int64) *ConsensusParamsInfo { } paramsInfo := new(ConsensusParamsInfo) - r, n, err := bytes.NewReader(buf), new(int), new(error) - wire.ReadBinaryPtr(paramsInfo, r, 0, n, err) - if *err != nil { + err := wire.UnmarshalBinary(buf, paramsInfo) + if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED cmn.Exit(cmn.Fmt(`LoadConsensusParams: Data has been corrupted or its spec has changed: - %v\n`, *err)) + %v\n`, err)) } // TODO: ensure that buf is completely read. diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index ed4fcc8a6..24e982725 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -67,10 +67,8 @@ func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) { return nil, nil } - r := bytes.NewReader(rawBytes) - var n int - var err error - txResult := wire.ReadBinary(&types.TxResult{}, r, 0, &n, &err).(*types.TxResult) + txResult := new(types.TxResult) + err := wire.UnmarshalBinary(rawBytes, &txResult) if err != nil { return nil, fmt.Errorf("Error reading TxResult: %v", err) } @@ -93,7 +91,10 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { } // index tx by hash - rawBytes := wire.BinaryBytes(result) + rawBytes, err := wire.MarshalBinary(result) + if err != nil { + return err + } storeBatch.Set(hash, rawBytes) } @@ -115,7 +116,10 @@ func (txi *TxIndex) Index(result *types.TxResult) error { } // index tx by hash - rawBytes := wire.BinaryBytes(result) + rawBytes, err := wire.MarshalBinary(result) + if err != nil { + return err + } b.Set(hash, rawBytes) b.Write() diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index ba2830e81..7ffc36e50 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -20,7 +20,7 @@ func TestTxIndex(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) tx := types.Tx("HELLO WORLD") - txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} + txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}}} hash := tx.Hash() batch := txindex.NewBatch(1) @@ -35,7 +35,7 @@ func TestTxIndex(t *testing.T) { assert.Equal(t, txResult, loadedTxResult) tx2 := types.Tx("BYE BYE WORLD") - txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} + txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}}} hash2 := tx2.Hash() err = indexer.Index(txResult2) @@ -146,12 +146,12 @@ func TestIndexAllTags(t *testing.T) { func txResultWithTags(tags []cmn.KVPair) *types.TxResult { tx := types.Tx("HELLO WORLD") - return &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: tags}} + return &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: tags, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}}} } func benchmarkTxIndex(txsCount int, b *testing.B) { tx := types.Tx("HELLO WORLD") - txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} + txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}, Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0}}} dir, err := ioutil.TempDir("", "tx_index_db") if err != nil { diff --git a/test/README.md b/test/README.md index f53dc371d..d9f09fef7 100644 --- a/test/README.md +++ b/test/README.md @@ -9,13 +9,13 @@ and run the following tests in docker containers: - go tests, with --race - includes test coverage - app tests - - dummy app over socket + - kvstore app over socket - counter app over socket - counter app over grpc - persistence tests - crash tendermint at each of many predefined points, restart, and ensure it syncs properly with the app - p2p tests - - start a local dummy app testnet on a docker network (requires docker version 1.10+) + - start a local kvstore app testnet on a docker network (requires docker version 1.10+) - send a tx on each node and ensure the state root is updated on all of them - crash and restart nodes one at a time and ensure they can sync back up (via fastsync) - crash and restart all nodes at once and ensure they can sync back up @@ -23,8 +23,3 @@ and run the following tests in docker containers: If on a `release-x.x.x` branch, we also run - `go test` for all our dependency libs (test/test_libs.sh) -- network_testing - benchmark a mintnet based cloud deploy using netmon - - - - diff --git a/test/app/clean.sh b/test/app/clean.sh old mode 100644 new mode 100755 diff --git a/test/app/counter_test.sh b/test/app/counter_test.sh old mode 100644 new mode 100755 diff --git a/test/app/grpc_client b/test/app/grpc_client deleted file mode 100755 index ff72d1858..000000000 Binary files a/test/app/grpc_client and /dev/null differ diff --git a/test/app/grpc_client.go b/test/app/grpc_client.go old mode 100644 new mode 100755 diff --git a/test/app/dummy_test.sh b/test/app/kvstore_test.sh old mode 100644 new mode 100755 similarity index 98% rename from test/app/dummy_test.sh rename to test/app/kvstore_test.sh index b3db0b86b..f63ed9dcb --- a/test/app/dummy_test.sh +++ b/test/app/kvstore_test.sh @@ -7,7 +7,7 @@ function toHex() { } ##################### -# dummy with curl +# kvstore with curl ##################### TESTNAME=$1 diff --git a/test/app/test.sh b/test/app/test.sh old mode 100644 new mode 100755 index 355fab45e..0f77da04e --- a/test/app/test.sh +++ b/test/app/test.sh @@ -1,47 +1,48 @@ #! /bin/bash -set -e +set -ex -#- dummy over socket, curl +#- kvstore over socket, curl #- counter over socket, curl #- counter over grpc, curl #- counter over grpc, grpc # TODO: install everything +export PATH="$GOBIN:$PATH" export TMHOME=$HOME/.tendermint_app -function dummy_over_socket(){ +function kvstore_over_socket(){ rm -rf $TMHOME tendermint init - echo "Starting dummy_over_socket" - abci-cli dummy > /dev/null & - pid_dummy=$! + echo "Starting kvstore_over_socket" + abci-cli kvstore > /dev/null & + pid_kvstore=$! tendermint node > tendermint.log & pid_tendermint=$! sleep 5 echo "running test" - bash dummy_test.sh "Dummy over Socket" + bash kvstore_test.sh "KVStore over Socket" - kill -9 $pid_dummy $pid_tendermint + kill -9 $pid_kvstore $pid_tendermint } # start tendermint first -function dummy_over_socket_reorder(){ +function kvstore_over_socket_reorder(){ rm -rf $TMHOME tendermint init - echo "Starting dummy_over_socket_reorder (ie. start tendermint first)" + echo "Starting kvstore_over_socket_reorder (ie. start tendermint first)" tendermint node > tendermint.log & pid_tendermint=$! sleep 2 - abci-cli dummy > /dev/null & - pid_dummy=$! + abci-cli kvstore > /dev/null & + pid_kvstore=$! sleep 5 echo "running test" - bash dummy_test.sh "Dummy over Socket" + bash kvstore_test.sh "KVStore over Socket" - kill -9 $pid_dummy $pid_tendermint + kill -9 $pid_kvstore $pid_tendermint } @@ -98,11 +99,11 @@ function counter_over_grpc_grpc() { cd $GOPATH/src/github.com/tendermint/tendermint/test/app case "$1" in - "dummy_over_socket") - dummy_over_socket + "kvstore_over_socket") + kvstore_over_socket ;; -"dummy_over_socket_reorder") - dummy_over_socket_reorder +"kvstore_over_socket_reorder") + kvstore_over_socket_reorder ;; "counter_over_socket") counter_over_socket @@ -115,9 +116,9 @@ case "$1" in ;; *) echo "Running all" - dummy_over_socket + kvstore_over_socket echo "" - dummy_over_socket_reorder + kvstore_over_socket_reorder echo "" counter_over_socket echo "" diff --git a/test/circleci/p2p.sh b/test/circleci/p2p.sh new file mode 100644 index 000000000..19200afbe --- /dev/null +++ b/test/circleci/p2p.sh @@ -0,0 +1,35 @@ +#! /bin/bash +set -eux + +# Get the directory of where this script is. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +LOGS_DIR="$DIR/../logs" +echo +echo "* [$(date +"%T")] cleaning up $LOGS_DIR" +rm -rf "$LOGS_DIR" +mkdir -p "$LOGS_DIR" + +set +e +echo +echo "* [$(date +"%T")] removing run_test container" +docker rm -vf run_test +set -e + +echo +echo "* [$(date +"%T")] starting rsyslog container" +docker rm -f rsyslog || true +docker run -d -v "$LOGS_DIR:/var/log/" -p 127.0.0.1:5514:514/udp --name rsyslog voxxit/rsyslog + +set +u +if [[ "$SKIP_BUILD" == "" ]]; then + echo + echo "* [$(date +"%T")] building docker image" + bash "$DIR/../docker/build.sh" +fi + +echo +echo "* [$(date +"%T")] running p2p tests on a local docker network" +bash "$DIR/../p2p/test.sh" tester diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index a168cfb1d..f26e60d56 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.9.2 +FROM golang:1.9.4 # Add testing deps for curl RUN echo 'deb http://httpredir.debian.org/debian testing main non-free contrib' >> /etc/apt/sources.list @@ -10,12 +10,13 @@ RUN apt-get update && \ # Setup tendermint repo ENV REPO $GOPATH/src/github.com/tendermint/tendermint +ENV GOBIN $GOPATH/bin WORKDIR $REPO # Install the vendored dependencies before copying code # docker caching prevents reinstall on code change! -ADD glide.yaml glide.yaml -ADD glide.lock glide.lock +ADD Gopkg.toml Gopkg.toml +ADD Gopkg.lock Gopkg.lock ADD Makefile Makefile RUN make get_tools RUN make get_vendor_deps @@ -25,8 +26,10 @@ ADD scripts scripts RUN bash scripts/install_abci_apps.sh # Now copy in the code +# NOTE: this will overwrite whatever is in vendor/ COPY . $REPO + RUN go install ./cmd/tendermint # expose the volume for debugging diff --git a/test/net/setup.sh b/test/net/setup.sh deleted file mode 100644 index 148a3c4b7..000000000 --- a/test/net/setup.sh +++ /dev/null @@ -1,26 +0,0 @@ -#! /bin/bash -set -eu - -# grab glide for dependency mgmt -go get github.com/Masterminds/glide - -# grab network monitor, install mintnet, netmon -# these might err -echo "... fetching repos. ignore go get errors" -set +e -go get github.com/tendermint/network_testing -go get github.com/tendermint/mintnet -go get github.com/tendermint/netmon -set -e - -# install vendored deps -echo "GOPATH $GOPATH" - -cd $GOPATH/src/github.com/tendermint/mintnet -echo "... install mintnet dir $(pwd)" -glide install -go install -cd $GOPATH/src/github.com/tendermint/netmon -echo "... install netmon dir $(pwd)" -glide install -go install diff --git a/test/net/start.sh b/test/net/start.sh deleted file mode 100644 index 9e9ea3a0d..000000000 --- a/test/net/start.sh +++ /dev/null @@ -1,29 +0,0 @@ -#! /bin/bash -set -eu - -# start a testnet and benchmark throughput using mintnet+netmon via the network_testing repo - -DATACENTER=single -VALSETSIZE=4 -BLOCKSIZE=8092 -TX_SIZE=200 -NTXS=$((BLOCKSIZE*4)) -RESULTSDIR=results -CLOUD_PROVIDER=digitalocean - -set +u -if [[ "$MACH_PREFIX" == "" ]]; then - MACH_PREFIX=mach -fi -set -u - -cd "$GOPATH/src/github.com/tendermint/network_testing" -echo "... running network test $(pwd)" -TMHEAD=$(git rev-parse --abbrev-ref HEAD) TM_IMAGE="tendermint/tendermint" bash experiments/exp_throughput.sh $DATACENTER $VALSETSIZE $BLOCKSIZE $TX_SIZE $NTXS $MACH_PREFIX $RESULTSDIR $CLOUD_PROVIDER - -# TODO: publish result! - -# cleanup - -echo "... destroying machines" -mintnet destroy --machines $MACH_PREFIX[1-$VALSETSIZE] diff --git a/test/net/test.sh b/test/net/test.sh deleted file mode 100644 index 19147eb83..000000000 --- a/test/net/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /bin/bash -set -eu - -# install mintnet, netmon, fetch network_testing -bash test/net/setup.sh - -# start the testnet -bash test/net/start.sh diff --git a/test/p2p/README.md b/test/p2p/README.md index b68f7a819..783e59405 100644 --- a/test/p2p/README.md +++ b/test/p2p/README.md @@ -38,7 +38,7 @@ for i in $(seq 1 4); do --name local_testnet_$i \ --entrypoint tendermint \ -e TMHOME=/go/src/github.com/tendermint/tendermint/test/p2p/data/mach$i/core \ - tendermint_tester node --p2p.persistent_peers 172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 --proxy_app=dummy + tendermint_tester node --p2p.persistent_peers 172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 --proxy_app=kvstore done ``` diff --git a/test/p2p/basic/test.sh b/test/p2p/basic/test.sh old mode 100644 new mode 100755 diff --git a/test/p2p/data/core/init.sh b/test/p2p/data/core/init.sh deleted file mode 100755 index 12a42ed40..000000000 --- a/test/p2p/data/core/init.sh +++ /dev/null @@ -1,20 +0,0 @@ -#! /bin/bash -# This is a sample bash script for tendermint core -# Edit this script before "mintnet start" to change -# the core blockchain engine. - -TMREPO="github.com/tendermint/tendermint" -BRANCH="master" - -go get -d $TMREPO/cmd/tendermint -### DEPENDENCIES (example) -# cd $GOPATH/src/github.com/tendermint/abci -# git fetch origin $BRANCH -# git checkout $BRANCH -### DEPENDENCIES END -cd $GOPATH/src/$TMREPO -git fetch origin $BRANCH -git checkout $BRANCH -make install - -tendermint node --p2p.seeds="$TMSEEDS" --moniker="$TMNAME" --proxy_app="$PROXYAPP" --rpc.unsafe diff --git a/test/p2p/data/mach1/core/config/node_key.json b/test/p2p/data/mach1/core/config/node_key.json new file mode 100644 index 000000000..7540cb5f4 --- /dev/null +++ b/test/p2p/data/mach1/core/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"ed25519","data":"06962D169F314ABB9D05AE5A04B46E48F0FBD8F1830149B47493910CBDCA7796096E5B94CD179F545AE3C281D9BF5C9E0E3D8FF719048B62F7849094CFFA8591"}} \ No newline at end of file diff --git a/test/p2p/data/mach2/core/config/node_key.json b/test/p2p/data/mach2/core/config/node_key.json new file mode 100644 index 000000000..efad2f0b3 --- /dev/null +++ b/test/p2p/data/mach2/core/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"ed25519","data":"B8CE8B0D5138C10208526ABDADCE91C735FCCC4186E06E0972EC35E64973428A45EBC61F24CE1B91B3D26AFBAB11C2789EF04CBAC28183619C01116B66A9C528"}} \ No newline at end of file diff --git a/test/p2p/data/mach3/core/config/node_key.json b/test/p2p/data/mach3/core/config/node_key.json new file mode 100644 index 000000000..58880f6f3 --- /dev/null +++ b/test/p2p/data/mach3/core/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"ed25519","data":"913DE8AC6D18922A53F6B0196EF023B4693FECFBB565E084F0B4941768F3DAE892B35ADD954562FE071C465BC244B2AFAED4A270EC849269341473CE192DE682"}} \ No newline at end of file diff --git a/test/p2p/data/mach4/core/config/node_key.json b/test/p2p/data/mach4/core/config/node_key.json new file mode 100644 index 000000000..72e7b25df --- /dev/null +++ b/test/p2p/data/mach4/core/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"ed25519","data":"408226F3F40411AC22262DD9A33BFE27D6FED42B9F084906B3797118C951CB82F81552170A85C94F0608AE8B59B70A0CA8B604A9057585B28A266140DC615E97"}} \ No newline at end of file diff --git a/test/p2p/fast_sync/test_peer.sh b/test/p2p/fast_sync/test_peer.sh index 1f341bf5d..8b24b0438 100644 --- a/test/p2p/fast_sync/test_peer.sh +++ b/test/p2p/fast_sync/test_peer.sh @@ -19,20 +19,20 @@ echo "Testing fastsync on node $ID" # kill peer set +e # circle sigh :( -docker rm -vf local_testnet_$ID -set -e - -# restart peer - should have an empty blockchain -PERSISTENT_PEERS="$(test/p2p/ip.sh 1):46656" -for j in `seq 2 $N`; do - PERSISTENT_PEERS="$PERSISTENT_PEERS,$(test/p2p/ip.sh $j):46656" -done -bash test/p2p/peer.sh $DOCKER_IMAGE $NETWORK_NAME $ID $PROXY_APP "--p2p.persistent_peers $PERSISTENT_PEERS --p2p.pex --rpc.unsafe" - -# wait for peer to sync and check the app hash -bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME fs_$ID "test/p2p/fast_sync/check_peer.sh $ID" - -echo "" -echo "PASS" -echo "" + docker rm -vf local_testnet_$ID + set -e + + # restart peer - should have an empty blockchain + PERSISTENT_PEERS="$(test/p2p/ip_plus_id.sh 1 $DOCKER_IMAGE):46656" + for j in `seq 2 $N`; do + PERSISTENT_PEERS="$PERSISTENT_PEERS,$(test/p2p/ip_plus_id.sh $j $DOCKER_IMAGE):46656" + done + bash test/p2p/peer.sh $DOCKER_IMAGE $NETWORK_NAME $ID $PROXY_APP "--p2p.persistent_peers $PERSISTENT_PEERS --p2p.pex --rpc.unsafe" + + # wait for peer to sync and check the app hash + bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME fs_$ID "test/p2p/fast_sync/check_peer.sh $ID" + + echo "" + echo "PASS" + echo "" diff --git a/test/p2p/ip.sh b/test/p2p/ip.sh index 33ea890db..77753f541 100755 --- a/test/p2p/ip.sh +++ b/test/p2p/ip.sh @@ -3,5 +3,3 @@ set -eu ID=$1 echo "172.57.0.$((100+$ID))" - - diff --git a/test/p2p/ip_plus_id.sh b/test/p2p/ip_plus_id.sh new file mode 100755 index 000000000..0d2248fe0 --- /dev/null +++ b/test/p2p/ip_plus_id.sh @@ -0,0 +1,7 @@ +#! /bin/bash +set -eu + +ID=$1 +DOCKER_IMAGE=$2 +NODEID="$(docker run --rm -e TMHOME=/go/src/github.com/tendermint/tendermint/test/p2p/data/mach$ID/core $DOCKER_IMAGE tendermint show_node_id)" +echo "$NODEID@172.57.0.$((100+$ID))" diff --git a/test/p2p/persistent_peers.sh b/test/p2p/persistent_peers.sh index 4ad55bc03..68eeebe25 100644 --- a/test/p2p/persistent_peers.sh +++ b/test/p2p/persistent_peers.sh @@ -2,11 +2,12 @@ set -eu N=$1 +DOCKER_IMAGE=$2 cd "$GOPATH/src/github.com/tendermint/tendermint" -persistent_peers="$(test/p2p/ip.sh 1):46656" +persistent_peers="$(test/p2p/ip_plus_id.sh 1 $DOCKER_IMAGE):46656" for i in $(seq 2 $N); do - persistent_peers="$persistent_peers,$(test/p2p/ip.sh $i):46656" + persistent_peers="$persistent_peers,$(test/p2p/ip_plus_id.sh $i $DOCKER_IMAGE):46656" done echo "$persistent_peers" diff --git a/test/p2p/pex/dial_peers.sh b/test/p2p/pex/dial_peers.sh index ddda7dbe7..43ebfab00 100644 --- a/test/p2p/pex/dial_peers.sh +++ b/test/p2p/pex/dial_peers.sh @@ -2,31 +2,22 @@ set -u N=$1 +PEERS=$2 -cd $GOPATH/src/github.com/tendermint/tendermint +cd "$GOPATH/src/github.com/tendermint/tendermint" echo "Waiting for nodes to come online" -for i in `seq 1 $N`; do - addr=$(test/p2p/ip.sh $i):46657 - curl -s $addr/status > /dev/null +for i in $(seq 1 "$N"); do + addr=$(test/p2p/ip.sh "$i"):46657 + curl -s "$addr/status" > /dev/null ERR=$? while [ "$ERR" != 0 ]; do sleep 1 - curl -s $addr/status > /dev/null + curl -s "$addr/status" > /dev/null ERR=$? done echo "... node $i is up" done -set -e -# peers need quotes -peers="\"$(test/p2p/ip.sh 1):46656\"" -for i in `seq 2 $N`; do - peers="$peers,\"$(test/p2p/ip.sh $i):46656\"" -done -echo $peers - -echo $peers IP=$(test/p2p/ip.sh 1) -curl "$IP:46657/dial_peers?persistent=true&peers=\[$peers\]" - +curl "$IP:46657/dial_peers?persistent=true&peers=\\[$PEERS\\]" diff --git a/test/p2p/pex/test_dial_peers.sh b/test/p2p/pex/test_dial_peers.sh index d0b042342..ec386d008 100644 --- a/test/p2p/pex/test_dial_peers.sh +++ b/test/p2p/pex/test_dial_peers.sh @@ -21,14 +21,17 @@ set -e # start the testnet on a local network # NOTE we re-use the same network for all tests -PERSISTENT_PEERS="" -bash test/p2p/local_testnet_start.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP $PERSISTENT_PEERS - +bash test/p2p/local_testnet_start.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP "" +PERSISTENT_PEERS="\"$(test/p2p/ip_plus_id.sh 1 $DOCKER_IMAGE):46656\"" +for i in $(seq 2 $N); do + PERSISTENT_PEERS="$PERSISTENT_PEERS,\"$(test/p2p/ip_plus_id.sh $i $DOCKER_IMAGE):46656\"" +done +echo "$PERSISTENT_PEERS" # dial peers from one node CLIENT_NAME="dial_peers" -bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/pex/dial_peers.sh $N" +bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/pex/dial_peers.sh $N $PERSISTENT_PEERS" # test basic connectivity and consensus # start client container and check the num peers and height for all nodes diff --git a/test/p2p/test.sh b/test/p2p/test.sh index c95f69733..abcf2ca07 100644 --- a/test/p2p/test.sh +++ b/test/p2p/test.sh @@ -4,7 +4,7 @@ set -eu DOCKER_IMAGE=$1 NETWORK_NAME=local_testnet N=4 -PROXY_APP=persistent_dummy +PROXY_APP=persistent_kvstore cd "$GOPATH/src/github.com/tendermint/tendermint" @@ -13,7 +13,7 @@ set +e bash test/p2p/local_testnet_stop.sh "$NETWORK_NAME" "$N" set -e -PERSISTENT_PEERS=$(bash test/p2p/persistent_peers.sh $N) +PERSISTENT_PEERS=$(bash test/p2p/persistent_peers.sh $N $DOCKER_IMAGE) # start the testnet on a local network # NOTE we re-use the same network for all tests diff --git a/test/persist/test.sh b/test/persist/test.sh deleted file mode 100644 index e24e81deb..000000000 --- a/test/persist/test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#! /bin/bash - -cd "$GOPATH/src/github.com/tendermint/tendermint" - -bash ./test/persist/test_failure_indices.sh diff --git a/test/persist/test_failure_indices.sh b/test/persist/test_failure_indices.sh index 6c40786d9..42739107f 100644 --- a/test/persist/test_failure_indices.sh +++ b/test/persist/test_failure_indices.sh @@ -1,5 +1,6 @@ #! /bin/bash +export PATH="$GOBIN:$PATH" export TMHOME=$HOME/.tendermint_persist rm -rf "$TMHOME" @@ -9,37 +10,37 @@ tendermint init RPC_ADDR="$(pwd)/rpc.sock" TM_CMD="tendermint node --log_level=debug --rpc.laddr=unix://$RPC_ADDR" # &> tendermint_${name}.log" -DUMMY_CMD="abci-cli dummy --persist $TMHOME/dummy" # &> dummy_${name}.log" +DUMMY_CMD="abci-cli kvstore --persist $TMHOME/kvstore" # &> kvstore_${name}.log" function start_procs(){ name=$1 indexToFail=$2 - echo "Starting persistent dummy and tendermint" + echo "Starting persistent kvstore and tendermint" if [[ "$CIRCLECI" == true ]]; then $DUMMY_CMD & else - $DUMMY_CMD &> "dummy_${name}.log" & + $DUMMY_CMD &> "kvstore_${name}.log" & fi PID_DUMMY=$! # before starting tendermint, remove the rpc socket - rm $RPC_ADDR + rm -f $RPC_ADDR if [[ "$indexToFail" == "" ]]; then # run in background, dont fail - if [[ "$CIRCLECI" == true ]]; then - $TM_CMD & - else + if [[ "$CIRCLECI" == true ]]; then + $TM_CMD & + else $TM_CMD &> "tendermint_${name}.log" & - fi + fi PID_TENDERMINT=$! else # run in foreground, fail - if [[ "$CIRCLECI" == true ]]; then - FAIL_TEST_INDEX=$indexToFail $TM_CMD - else + if [[ "$CIRCLECI" == true ]]; then + FAIL_TEST_INDEX=$indexToFail $TM_CMD + else FAIL_TEST_INDEX=$indexToFail $TM_CMD &> "tendermint_${name}.log" - fi + fi PID_TENDERMINT=$! fi } diff --git a/test/persist/test_simple.sh b/test/persist/test_simple.sh index 964862c44..be7852598 100644 --- a/test/persist/test_simple.sh +++ b/test/persist/test_simple.sh @@ -8,8 +8,8 @@ tendermint init function start_procs(){ name=$1 - echo "Starting persistent dummy and tendermint" - abci-cli dummy --persist $TMHOME/dummy &> "dummy_${name}.log" & + echo "Starting persistent kvstore and tendermint" + abci-cli kvstore --persist $TMHOME/kvstore &> "kvstore_${name}.log" & PID_DUMMY=$! tendermint node &> tendermint_${name}.log & PID_TENDERMINT=$! diff --git a/test/run_test.sh b/test/run_test.sh deleted file mode 100644 index b505126ea..000000000 --- a/test/run_test.sh +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash -set -e - -pwd - -BRANCH=$(git rev-parse --abbrev-ref HEAD) -echo "Current branch: $BRANCH" - -# run the go unit tests with coverage -bash test/test_cover.sh - -# run the app tests using bash -bash test/app/test.sh - -# run the persistence tests using bash -bash test/persist/test.sh - -if [[ "$BRANCH" == "master" || $(echo "$BRANCH" | grep "release-") != "" ]]; then - echo "" - echo "* branch $BRANCH; testing libs" - # checkout every github.com/tendermint dir and run its tests - bash test/test_libs.sh -fi diff --git a/test/test.sh b/test/test.sh deleted file mode 100755 index 64d7bfc71..000000000 --- a/test/test.sh +++ /dev/null @@ -1,60 +0,0 @@ -#! /bin/bash -set -eu - -# Get the directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - -LOGS_DIR="$DIR/logs" -echo -echo "* [$(date +"%T")] cleaning up $LOGS_DIR" -rm -rf "$LOGS_DIR" -mkdir -p "$LOGS_DIR" - -set +e -echo -echo "* [$(date +"%T")] removing run_test container" -docker rm -vf run_test -set -e - -echo -echo "* [$(date +"%T")] starting rsyslog container" -docker rm -f rsyslog || true -docker run -d -v "$LOGS_DIR:/var/log/" -p 127.0.0.1:5514:514/udp --name rsyslog voxxit/rsyslog - -set +u -if [[ "$SKIP_BUILD" == "" ]]; then - echo - echo "* [$(date +"%T")] building docker image" - bash "$DIR/docker/build.sh" -fi - -echo -echo "* [$(date +"%T")] running go tests and app tests in docker container" -# sometimes its helpful to mount the local test folder -# -v $DIR:/go/src/github.com/tendermint/tendermint/test -if [[ "$CIRCLECI" == true ]]; then - docker run --name run_test -e CIRCLECI=true -t tester bash test/run_test.sh -else - docker run --name run_test -t tester bash test/run_test.sh -fi - -# copy the coverage results out of docker container -docker cp run_test:/go/src/github.com/tendermint/tendermint/coverage.txt . - -# test basic network connectivity -# by starting a local testnet and checking peers connect and make blocks -echo -echo "* [$(date +"%T")] running p2p tests on a local docker network" -bash "$DIR/p2p/test.sh" tester - -# only run the cloud benchmark for releases -BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [[ $(echo "$BRANCH" | grep "release-") != "" ]]; then - echo - echo "TODO: run network tests" - #echo "* branch $BRANCH; running mintnet/netmon throughput benchmark" - # TODO: replace mintnet - #bash "$DIR/net/test.sh" -fi diff --git a/test/test_cover.sh b/test/test_cover.sh index b1b754ba4..59ce15b03 100644 --- a/test/test_cover.sh +++ b/test/test_cover.sh @@ -3,6 +3,7 @@ PKGS=$(go list github.com/tendermint/tendermint/... | grep -v /vendor/) set -e + echo "mode: atomic" > coverage.txt for pkg in ${PKGS[@]}; do go test -v -timeout 30m -race -coverprofile=profile.out -covermode=atomic "$pkg" diff --git a/test/test_libs.sh b/test/test_libs.sh index 0a8485340..893bf35dc 100644 --- a/test/test_libs.sh +++ b/test/test_libs.sh @@ -1,28 +1,26 @@ #! /bin/bash -set -e +set -ex -# set glide.lock path -if [[ "$GLIDE" == "" ]]; then - GLIDE=$GOPATH/src/github.com/tendermint/tendermint/glide.lock -fi +export PATH="$GOBIN:$PATH" -# get vendored commit for given lib +# Get the parent directory of where this script is. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" #################### # libs we depend on #################### # All libs should define `make test` and `make get_vendor_deps` -LIBS_TEST=(tmlibs go-wire go-crypto abci) - -DIR=$(pwd) -for lib in "${LIBS_MAKE_TEST[@]}"; do - +LIBS=(tmlibs go-wire go-crypto abci) +for lib in "${LIBS[@]}"; do # checkout vendored version of lib - bash scripts/glide/checkout.sh "$GLIDE" "$lib" + bash scripts/dep_utils/checkout.sh "$lib" echo "Testing $lib ..." cd "$GOPATH/src/github.com/tendermint/$lib" + make get_tools make get_vendor_deps make test if [[ "$?" != 0 ]]; then diff --git a/types/block.go b/types/block.go index 3e800e722..970ca36f4 100644 --- a/types/block.go +++ b/types/block.go @@ -4,11 +4,10 @@ import ( "bytes" "errors" "fmt" - "io" "strings" "time" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" "golang.org/x/crypto/ripemd160" @@ -96,7 +95,11 @@ func (b *Block) Hash() cmn.HexBytes { // MakePartSet returns a PartSet containing parts of a serialized block. // This is the form in which the block is gossipped to peers. func (b *Block) MakePartSet(partSize int) *PartSet { - return NewPartSetFromData(wire.BinaryBytes(b), partSize) + bz, err := wire.MarshalBinary(b) + if err != nil { + panic(err) + } + return NewPartSetFromData(bz, partSize) } // HashesTo is a convenience function that checks if a block hashes to the given argument. @@ -176,7 +179,7 @@ type Header struct { // Hash returns the hash of the header. // Returns nil if ValidatorHash is missing. func (h *Header) Hash() cmn.HexBytes { - if len(h.ValidatorsHash) == 0 { + if h == nil || len(h.ValidatorsHash) == 0 { return nil } return merkle.SimpleHashFromMap(map[string]merkle.Hasher{ @@ -249,7 +252,8 @@ type Commit struct { bitArray *cmn.BitArray } -// FirstPrecommit returns the first non-nil precommit in the commit +// FirstPrecommit returns the first non-nil precommit in the commit. +// If all precommits are nil, it returns an empty precommit with height 0. func (commit *Commit) FirstPrecommit() *Vote { if len(commit.Precommits) == 0 { return nil @@ -263,7 +267,9 @@ func (commit *Commit) FirstPrecommit() *Vote { return precommit } } - return nil + return &Vote{ + Type: VoteTypePrecommit, + } } // Height returns the height of the commit @@ -407,6 +413,9 @@ type Data struct { // Hash returns the hash of the data func (data *Data) Hash() cmn.HexBytes { + if data == nil { + return (Txs{}).Hash() + } if data.hash == nil { data.hash = data.Txs.Hash() // NOTE: leaves of merkle tree are TxIDs } @@ -493,17 +502,11 @@ func (blockID BlockID) Equals(other BlockID) bool { // Key returns a machine-readable string representation of the BlockID func (blockID BlockID) Key() string { - return string(blockID.Hash) + string(wire.BinaryBytes(blockID.PartsHeader)) -} - -// WriteSignBytes writes the canonical bytes of the BlockID to the given writer for digital signing -func (blockID BlockID) WriteSignBytes(w io.Writer, n *int, err *error) { - if blockID.IsZero() { - wire.WriteTo([]byte("null"), w, n, err) - } else { - wire.WriteJSON(CanonicalBlockID(blockID), w, n, err) + bz, err := wire.MarshalBinary(blockID.PartsHeader) + if err != nil { + panic(err) } - + return string(blockID.Hash) + string(bz) } // String returns a human readable string representation of the BlockID @@ -518,15 +521,24 @@ type hasher struct { } func (h hasher) Hash() []byte { - hasher, n, err := ripemd160.New(), new(int), new(error) - wire.WriteBinary(h.item, hasher, n, err) - if *err != nil { + hasher := ripemd160.New() + bz, err := wire.MarshalBinary(h.item) + if err != nil { + panic(err) + } + _, err = hasher.Write(bz) + if err != nil { panic(err) } return hasher.Sum(nil) } +func tmHash(item interface{}) []byte { + h := hasher{item} + return h.Hash() +} + func wireHasher(item interface{}) merkle.Hasher { return hasher{item} } diff --git a/types/block_test.go b/types/block_test.go index 1fcfa469b..e3e22743c 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -3,7 +3,9 @@ package types import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" ) @@ -73,3 +75,15 @@ func makeBlockID(hash string, partSetSize int, partSetHash string) BlockID { } } + +var nilBytes []byte + +func TestNilHeaderHashDoesntCrash(t *testing.T) { + assert.Equal(t, []byte((*Header)(nil).Hash()), nilBytes) + assert.Equal(t, []byte((new(Header)).Hash()), nilBytes) +} + +func TestNilDataHashDoesntCrash(t *testing.T) { + assert.Equal(t, []byte((*Data)(nil).Hash()), nilBytes) + assert.Equal(t, []byte(new(Data).Hash()), nilBytes) +} diff --git a/types/canonical_json.go b/types/canonical_json.go index 45d12b45f..4eeeb2064 100644 --- a/types/canonical_json.go +++ b/types/canonical_json.go @@ -3,14 +3,14 @@ package types import ( "time" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" ) -// canonical json is go-wire's json for structs with fields in alphabetical order +// canonical json is wire's json for structs with fields in alphabetical order -// timeFormat is used for generating the sigs -const timeFormat = wire.RFC3339Millis +// TimeFormat is used for generating the sigs +const TimeFormat = wire.RFC3339Millis type CanonicalJSONBlockID struct { Hash cmn.HexBytes `json:"hash,omitempty"` @@ -114,8 +114,8 @@ func CanonicalHeartbeat(heartbeat *Heartbeat) CanonicalJSONHeartbeat { } func CanonicalTime(t time.Time) string { - // note that sending time over go-wire resets it to + // note that sending time over wire resets it to // local time, we need to force UTC here, so the // signatures match - return t.UTC().Format(timeFormat) + return t.UTC().Format(TimeFormat) } diff --git a/types/evidence.go b/types/evidence.go index 9973c62e6..9e1f6af0e 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" "github.com/tendermint/tmlibs/merkle" ) @@ -148,10 +148,10 @@ func (dve *DuplicateVoteEvidence) Verify(chainID string) error { } // Signatures must be valid - if !dve.PubKey.VerifyBytes(SignBytes(chainID, dve.VoteA), dve.VoteA.Signature) { + if !dve.PubKey.VerifyBytes(dve.VoteA.SignBytes(chainID), dve.VoteA.Signature) { return fmt.Errorf("DuplicateVoteEvidence Error verifying VoteA: %v", ErrVoteInvalidSignature) } - if !dve.PubKey.VerifyBytes(SignBytes(chainID, dve.VoteB), dve.VoteB.Signature) { + if !dve.PubKey.VerifyBytes(dve.VoteB.SignBytes(chainID), dve.VoteB.Signature) { return fmt.Errorf("DuplicateVoteEvidence Error verifying VoteB: %v", ErrVoteInvalidSignature) } diff --git a/types/evidence_test.go b/types/evidence_test.go index 876b68ad1..84811514a 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -22,7 +22,7 @@ func makeVote(val *PrivValidatorFS, chainID string, valIndex int, height int64, Type: byte(step), BlockID: blockID, } - sig := val.PrivKey.Sign(SignBytes(chainID, v)) + sig := val.PrivKey.Sign(v.SignBytes(chainID)) v.Signature = sig return v @@ -41,7 +41,7 @@ func TestEvidence(t *testing.T) { vote1 := makeVote(val, chainID, 0, 10, 2, 1, blockID) badVote := makeVote(val, chainID, 0, 10, 2, 1, blockID) - badVote.Signature = val2.PrivKey.Sign(SignBytes(chainID, badVote)) + badVote.Signature = val2.PrivKey.Sign(badVote.SignBytes(chainID)) cases := []voteData{ {vote1, makeVote(val, chainID, 0, 10, 2, 1, blockID2), true}, // different block ids diff --git a/types/genesis.go b/types/genesis.go index b7e4f6452..a760caff9 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -28,7 +28,18 @@ type GenesisDoc struct { ConsensusParams *ConsensusParams `json:"consensus_params,omitempty"` Validators []GenesisValidator `json:"validators"` AppHash cmn.HexBytes `json:"app_hash"` - AppOptions interface{} `json:"app_options,omitempty"` + _AppState json.RawMessage `json:"app_state,omitempty"` + AppOptions json.RawMessage `json:"app_options,omitempty"` // DEPRECATED +} + +// AppState returns raw application state. +// TODO: replace with AppState field during next breaking release (0.18) +func (genDoc *GenesisDoc) AppState() json.RawMessage { + if len(genDoc.AppOptions) > 0 { + return genDoc.AppOptions + } else { + return genDoc._AppState + } } // SaveAs is a utility method for saving GenensisDoc as a JSON file. diff --git a/types/genesis_test.go b/types/genesis_test.go index bdef3b925..aa713289f 100644 --- a/types/genesis_test.go +++ b/types/genesis_test.go @@ -10,7 +10,6 @@ import ( ) func TestGenesis(t *testing.T) { - // test some bad ones from raw json testCases := [][]byte{ []byte{}, // empty @@ -30,7 +29,7 @@ func TestGenesis(t *testing.T) { } // test a good one by raw json - genDocBytes := []byte(`{"genesis_time":"0001-01-01T00:00:00Z","chain_id":"test-chain-QDKdJr","consensus_params":null,"validators":[{"pub_key":{"type":"ed25519","data":"961EAB8752E51A03618502F55C2B6E09C38C65635C64CCF3173ED452CF86C957"},"power":10,"name":""}],"app_hash":"","app_options":{"account_owner": "Bob"}}`) + genDocBytes := []byte(`{"genesis_time":"0001-01-01T00:00:00Z","chain_id":"test-chain-QDKdJr","consensus_params":null,"validators":[{"pub_key":{"type":"ed25519","data":"961EAB8752E51A03618502F55C2B6E09C38C65635C64CCF3173ED452CF86C957"},"power":10,"name":""}],"app_hash":"","app_state":{"account_owner": "Bob"}}`) _, err := GenesisDocFromJSON(genDocBytes) assert.NoError(t, err, "expected no error for good genDoc json") diff --git a/types/heartbeat.go b/types/heartbeat.go index a4ff0d217..8b86a15ba 100644 --- a/types/heartbeat.go +++ b/types/heartbeat.go @@ -2,10 +2,9 @@ package types import ( "fmt" - "io" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" ) @@ -15,7 +14,7 @@ import ( // json field tags because we always want the JSON // representation to be in its canonical form. type Heartbeat struct { - ValidatorAddress Address `json:"validator_address"` + ValidatorAddress Address `json:"validator_address"` ValidatorIndex int `json:"validator_index"` Height int64 `json:"height"` Round int `json:"round"` @@ -23,13 +22,17 @@ type Heartbeat struct { Signature crypto.Signature `json:"signature"` } -// WriteSignBytes writes the Heartbeat for signing. +// SignBytes returns the Heartbeat bytes for signing. // It panics if the Heartbeat is nil. -func (heartbeat *Heartbeat) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) { - wire.WriteJSON(CanonicalJSONOnceHeartbeat{ +func (heartbeat *Heartbeat) SignBytes(chainID string) []byte { + bz, err := wire.MarshalJSON(CanonicalJSONOnceHeartbeat{ chainID, CanonicalHeartbeat(heartbeat), - }, w, n, err) + }) + if err != nil { + panic(err) + } + return bz } // Copy makes a copy of the Heartbeat. diff --git a/types/heartbeat_test.go b/types/heartbeat_test.go index 660ccd0f9..206636166 100644 --- a/types/heartbeat_test.go +++ b/types/heartbeat_test.go @@ -1,7 +1,6 @@ package types import ( - "bytes" "testing" "github.com/stretchr/testify/require" @@ -34,23 +33,18 @@ func TestHeartbeatString(t *testing.T) { } func TestHeartbeatWriteSignBytes(t *testing.T) { - var n int - var err error - buf := new(bytes.Buffer) hb := &Heartbeat{ValidatorIndex: 1, Height: 10, Round: 1} - hb.WriteSignBytes("0xdeadbeef", buf, &n, &err) - require.Equal(t, buf.String(), `{"chain_id":"0xdeadbeef","heartbeat":{"height":10,"round":1,"sequence":0,"validator_address":"","validator_index":1}}`) + bz := hb.SignBytes("0xdeadbeef") + require.Equal(t, string(bz), `{"chain_id":"0xdeadbeef","heartbeat":{"height":10,"round":1,"sequence":0,"validator_address":"","validator_index":1}}`) - buf.Reset() plainHb := &Heartbeat{} - plainHb.WriteSignBytes("0xdeadbeef", buf, &n, &err) - require.Equal(t, buf.String(), `{"chain_id":"0xdeadbeef","heartbeat":{"height":0,"round":0,"sequence":0,"validator_address":"","validator_index":0}}`) + bz = plainHb.SignBytes("0xdeadbeef") + require.Equal(t, string(bz), `{"chain_id":"0xdeadbeef","heartbeat":{"height":0,"round":0,"sequence":0,"validator_address":"","validator_index":0}}`) require.Panics(t, func() { - buf.Reset() var nilHb *Heartbeat - nilHb.WriteSignBytes("0xdeadbeef", buf, &n, &err) - require.Equal(t, buf.String(), "null") + bz := nilHb.SignBytes("0xdeadbeef") + require.Equal(t, string(bz), "null") }) } diff --git a/types/part_set.go b/types/part_set.go index ff11f7d35..5c43b1518 100644 --- a/types/part_set.go +++ b/types/part_set.go @@ -9,7 +9,6 @@ import ( "golang.org/x/crypto/ripemd160" - "github.com/tendermint/go-wire" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" ) @@ -73,10 +72,6 @@ func (psh PartSetHeader) Equals(other PartSetHeader) bool { return psh.Total == other.Total && bytes.Equal(psh.Hash, other.Hash) } -func (psh PartSetHeader) WriteSignBytes(w io.Writer, n *int, err *error) { - wire.WriteJSON(CanonicalPartSetHeader(psh), w, n, err) -} - //------------------------------------- type PartSet struct { diff --git a/types/priv_validator.go b/types/priv_validator.go index 062fe09d2..daa456bc0 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io/ioutil" - "os" "sync" "time" @@ -34,6 +33,57 @@ func voteToStep(vote *Vote) int8 { } } +//-------------------------------------------------------------- +// PrivValidator is being upgraded! See types/priv_validator/ + +// ValidatorID contains the identity of the validator. +type ValidatorID struct { + Address cmn.HexBytes `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` +} + +// PrivValidator defines the functionality of a local Tendermint validator +// that signs votes, proposals, and heartbeats, and never double signs. +type PrivValidator2 interface { + Address() (Address, error) // redundant since .PubKey().Address() + PubKey() (crypto.PubKey, error) + + SignVote(chainID string, vote *Vote) error + SignProposal(chainID string, proposal *Proposal) error + SignHeartbeat(chainID string, heartbeat *Heartbeat) error +} + +type TestSigner interface { + Address() cmn.HexBytes + PubKey() crypto.PubKey + Sign([]byte) (crypto.Signature, error) +} + +func GenSigner() TestSigner { + return &DefaultTestSigner{ + crypto.GenPrivKeyEd25519().Wrap(), + } +} + +type DefaultTestSigner struct { + crypto.PrivKey +} + +func (ds *DefaultTestSigner) Address() cmn.HexBytes { + return ds.PubKey().Address() +} + +func (ds *DefaultTestSigner) PubKey() crypto.PubKey { + return ds.PrivKey.PubKey() +} + +func (ds *DefaultTestSigner) Sign(msg []byte) (crypto.Signature, error) { + return ds.PrivKey.Sign(msg), nil +} + +//-------------------------------------------------------------- +// TODO: Deprecate! + // PrivValidator defines the functionality of a local Tendermint validator // that signs votes, proposals, and heartbeats, and never double signs. type PrivValidator interface { @@ -48,6 +98,7 @@ type PrivValidator interface { // PrivValidatorFS implements PrivValidator using data persisted to disk // to prevent double signing. The Signer itself can be mutated to use // something besides the default, for instance a hardware signer. +// NOTE: the directory containing the privVal.filePath must already exist. type PrivValidatorFS struct { Address Address `json:"address"` PubKey crypto.PubKey `json:"pub_key"` @@ -130,7 +181,7 @@ func LoadPrivValidatorFS(filePath string) *PrivValidatorFS { // or else generates a new one and saves it to the filePath. func LoadOrGenPrivValidatorFS(filePath string) *PrivValidatorFS { var privVal *PrivValidatorFS - if _, err := os.Stat(filePath); err == nil { + if cmn.FileExists(filePath) { privVal = LoadPrivValidatorFS(filePath) } else { privVal = GenPrivValidatorFS(filePath) @@ -167,28 +218,28 @@ func (privVal *PrivValidatorFS) Save() { } func (privVal *PrivValidatorFS) save() { - if privVal.filePath == "" { - cmn.PanicSanity("Cannot save PrivValidator: filePath not set") + outFile := privVal.filePath + if outFile == "" { + panic("Cannot save PrivValidator: filePath not set") } jsonBytes, err := json.Marshal(privVal) if err != nil { - // `@; BOOM!!! - cmn.PanicCrisis(err) + panic(err) } - err = cmn.WriteFileAtomic(privVal.filePath, jsonBytes, 0600) + err = cmn.WriteFileAtomic(outFile, jsonBytes, 0600) if err != nil { - // `@; BOOM!!! - cmn.PanicCrisis(err) + panic(err) } } // Reset resets all fields in the PrivValidatorFS. // NOTE: Unsafe! func (privVal *PrivValidatorFS) Reset() { + var sig crypto.Signature privVal.LastHeight = 0 privVal.LastRound = 0 privVal.LastStep = 0 - privVal.LastSignature = crypto.Signature{} + privVal.LastSignature = sig privVal.LastSignBytes = nil privVal.Save() } @@ -248,7 +299,7 @@ func (privVal *PrivValidatorFS) checkHRS(height int64, round int, step int8) (bo // a previously signed vote (ie. we crashed after signing but before the vote hit the WAL). func (privVal *PrivValidatorFS) signVote(chainID string, vote *Vote) error { height, round, step := vote.Height, vote.Round, voteToStep(vote) - signBytes := SignBytes(chainID, vote) + signBytes := vote.SignBytes(chainID) sameHRS, err := privVal.checkHRS(height, round, step) if err != nil { @@ -287,7 +338,7 @@ func (privVal *PrivValidatorFS) signVote(chainID string, vote *Vote) error { // a previously signed proposal ie. we crashed after signing but before the proposal hit the WAL). func (privVal *PrivValidatorFS) signProposal(chainID string, proposal *Proposal) error { height, round, step := proposal.Height, proposal.Round, stepPropose - signBytes := SignBytes(chainID, proposal) + signBytes := proposal.SignBytes(chainID) sameHRS, err := privVal.checkHRS(height, round, step) if err != nil { @@ -339,7 +390,7 @@ func (privVal *PrivValidatorFS) SignHeartbeat(chainID string, heartbeat *Heartbe privVal.mtx.Lock() defer privVal.mtx.Unlock() var err error - heartbeat.Signature, err = privVal.Sign(SignBytes(chainID, heartbeat)) + heartbeat.Signature, err = privVal.Sign(heartbeat.SignBytes(chainID)) return err } @@ -379,7 +430,7 @@ func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.T panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err)) } - lastTime, err := time.Parse(timeFormat, lastVote.Vote.Timestamp) + lastTime, err := time.Parse(TimeFormat, lastVote.Vote.Timestamp) if err != nil { panic(err) } @@ -405,7 +456,7 @@ func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (ti panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err)) } - lastTime, err := time.Parse(timeFormat, lastProposal.Proposal.Timestamp) + lastTime, err := time.Parse(TimeFormat, lastProposal.Proposal.Timestamp) if err != nil { panic(err) } diff --git a/types/priv_validator/json.go b/types/priv_validator/json.go new file mode 100644 index 000000000..5c0849ebd --- /dev/null +++ b/types/priv_validator/json.go @@ -0,0 +1,197 @@ +package types + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" +) + +// PrivValidator aliases types.PrivValidator +type PrivValidator = types.PrivValidator2 + +//----------------------------------------------------- + +// PrivKey implements Signer +type PrivKey crypto.PrivKey + +// Sign - Implements Signer +func (pk PrivKey) Sign(msg []byte) (crypto.Signature, error) { + return crypto.PrivKey(pk).Sign(msg), nil +} + +// MarshalJSON satisfies json.Marshaler. +func (pk PrivKey) MarshalJSON() ([]byte, error) { + return crypto.PrivKey(pk).MarshalJSON() +} + +// UnmarshalJSON satisfies json.Unmarshaler. +func (pk *PrivKey) UnmarshalJSON(b []byte) error { + cpk := new(crypto.PrivKey) + if err := cpk.UnmarshalJSON(b); err != nil { + return err + } + *pk = (PrivKey)(*cpk) + return nil +} + +//----------------------------------------------------- + +var _ types.PrivValidator2 = (*PrivValidatorJSON)(nil) + +// PrivValidatorJSON wraps PrivValidatorUnencrypted +// and persists it to disk after every SignVote and SignProposal. +type PrivValidatorJSON struct { + *PrivValidatorUnencrypted + + filePath string +} + +// SignVote implements PrivValidator. It persists to disk. +func (pvj *PrivValidatorJSON) SignVote(chainID string, vote *types.Vote) error { + err := pvj.PrivValidatorUnencrypted.SignVote(chainID, vote) + if err != nil { + return err + } + pvj.Save() + return nil +} + +// SignProposal implements PrivValidator. It persists to disk. +func (pvj *PrivValidatorJSON) SignProposal(chainID string, proposal *types.Proposal) error { + err := pvj.PrivValidatorUnencrypted.SignProposal(chainID, proposal) + if err != nil { + return err + } + pvj.Save() + return nil +} + +//------------------------------------------------------- + +// String returns a string representation of the PrivValidatorJSON. +func (pvj *PrivValidatorJSON) String() string { + addr, err := pvj.Address() + if err != nil { + panic(err) + } + + return fmt.Sprintf("PrivValidator{%v %v}", addr, pvj.PrivValidatorUnencrypted.String()) +} + +// Save persists the PrivValidatorJSON to disk. +func (pvj *PrivValidatorJSON) Save() { + pvj.save() +} + +func (pvj *PrivValidatorJSON) save() { + if pvj.filePath == "" { + panic("Cannot save PrivValidator: filePath not set") + } + jsonBytes, err := json.Marshal(pvj) + if err != nil { + // ; BOOM!!! + panic(err) + } + err = cmn.WriteFileAtomic(pvj.filePath, jsonBytes, 0600) + if err != nil { + // ; BOOM!!! + panic(err) + } +} + +// Reset resets the PrivValidatorUnencrypted. Panics if the Signer is the wrong type. +// NOTE: Unsafe! +func (pvj *PrivValidatorJSON) Reset() { + pvj.PrivValidatorUnencrypted.LastSignedInfo.Reset() + pvj.Save() +} + +//---------------------------------------------------------------- + +// GenPrivValidatorJSON generates a new validator with randomly generated private key +// and the given filePath. It does not persist to file. +func GenPrivValidatorJSON(filePath string) *PrivValidatorJSON { + privKey := crypto.GenPrivKeyEd25519().Wrap() + return &PrivValidatorJSON{ + PrivValidatorUnencrypted: NewPrivValidatorUnencrypted(privKey), + filePath: filePath, + } +} + +// LoadPrivValidatorJSON loads a PrivValidatorJSON from the filePath. +func LoadPrivValidatorJSON(filePath string) *PrivValidatorJSON { + pvJSONBytes, err := ioutil.ReadFile(filePath) + if err != nil { + cmn.Exit(err.Error()) + } + pvj := PrivValidatorJSON{} + err = json.Unmarshal(pvJSONBytes, &pvj) + if err != nil { + cmn.Exit(cmn.Fmt("Error reading PrivValidatorJSON from %v: %v\n", filePath, err)) + } + + // enable persistence + pvj.filePath = filePath + return &pvj +} + +// LoadOrGenPrivValidatorJSON loads a PrivValidatorJSON from the given filePath +// or else generates a new one and saves it to the filePath. +func LoadOrGenPrivValidatorJSON(filePath string) *PrivValidatorJSON { + var pvj *PrivValidatorJSON + if cmn.FileExists(filePath) { + pvj = LoadPrivValidatorJSON(filePath) + } else { + pvj = GenPrivValidatorJSON(filePath) + pvj.Save() + } + return pvj +} + +//-------------------------------------------------------------- + +// NewTestPrivValidator returns a PrivValidatorJSON with a tempfile +// for the file path. +func NewTestPrivValidator(signer types.TestSigner) *PrivValidatorJSON { + _, tempFilePath := cmn.Tempfile("priv_validator_") + pv := &PrivValidatorJSON{ + PrivValidatorUnencrypted: NewPrivValidatorUnencrypted(signer.(*types.DefaultTestSigner).PrivKey), + filePath: tempFilePath, + } + return pv +} + +//------------------------------------------------------ + +// PrivValidatorsByAddress is a list of PrivValidatorJSON ordered by their +// addresses. +type PrivValidatorsByAddress []*PrivValidatorJSON + +func (pvs PrivValidatorsByAddress) Len() int { + return len(pvs) +} + +func (pvs PrivValidatorsByAddress) Less(i, j int) bool { + iaddr, err := pvs[j].Address() + if err != nil { + panic(err) + } + + jaddr, err := pvs[i].Address() + if err != nil { + panic(err) + } + + return bytes.Compare(iaddr, jaddr) == -1 +} + +func (pvs PrivValidatorsByAddress) Swap(i, j int) { + it := pvs[i] + pvs[i] = pvs[j] + pvs[j] = it +} diff --git a/types/priv_validator/priv_validator_test.go b/types/priv_validator/priv_validator_test.go new file mode 100644 index 000000000..120a0c86e --- /dev/null +++ b/types/priv_validator/priv_validator_test.go @@ -0,0 +1,282 @@ +package types + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/tendermint/types" +) + +func TestGenLoadValidator(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + _, tempFilePath := cmn.Tempfile("priv_validator_") + privVal := GenPrivValidatorJSON(tempFilePath) + + height := int64(100) + privVal.LastSignedInfo.Height = height + privVal.Save() + addr, err := privVal.Address() + require.Nil(err) + + privVal = LoadPrivValidatorJSON(tempFilePath) + pAddr, err := privVal.Address() + require.Nil(err) + + assert.Equal(addr, pAddr, "expected privval addr to be the same") + assert.Equal(height, privVal.LastSignedInfo.Height, "expected privval.LastHeight to have been saved") +} + +func TestLoadOrGenValidator(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + _, tempFilePath := cmn.Tempfile("priv_validator_") + if err := os.Remove(tempFilePath); err != nil { + t.Error(err) + } + privVal := LoadOrGenPrivValidatorJSON(tempFilePath) + addr, err := privVal.Address() + require.Nil(err) + + privVal = LoadOrGenPrivValidatorJSON(tempFilePath) + pAddr, err := privVal.Address() + require.Nil(err) + + assert.Equal(addr, pAddr, "expected privval addr to be the same") +} + +func TestUnmarshalValidator(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // create some fixed values + addrStr := "D028C9981F7A87F3093672BF0D5B0E2A1B3ED456" + pubStr := "3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + privStr := "27F82582AEFAE7AB151CFB01C48BB6C1A0DA78F9BDDA979A9F70A84D074EB07D3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8" + addrBytes, _ := hex.DecodeString(addrStr) + pubBytes, _ := hex.DecodeString(pubStr) + privBytes, _ := hex.DecodeString(privStr) + + // prepend type byte + pubKey, err := crypto.PubKeyFromBytes(append([]byte{1}, pubBytes...)) + require.Nil(err, "%+v", err) + privKey, err := crypto.PrivKeyFromBytes(append([]byte{1}, privBytes...)) + require.Nil(err, "%+v", err) + + serialized := fmt.Sprintf(`{ + "id": { + "address": "%s", + "pub_key": { + "type": "ed25519", + "data": "%s" + } + }, + "priv_key": { + "type": "ed25519", + "data": "%s" + }, + "last_signed_info": { + "height": 0, + "round": 0, + "step": 0, + "signature": null + } +}`, addrStr, pubStr, privStr) + + val := PrivValidatorJSON{} + err = json.Unmarshal([]byte(serialized), &val) + require.Nil(err, "%+v", err) + + // make sure the values match + vAddr, err := val.Address() + require.Nil(err) + + pKey, err := val.PubKey() + require.Nil(err) + + assert.EqualValues(addrBytes, vAddr) + assert.EqualValues(pubKey, pKey) + assert.EqualValues(privKey, val.PrivKey) + + // export it and make sure it is the same + out, err := json.Marshal(val) + require.Nil(err, "%+v", err) + assert.JSONEq(serialized, string(out)) +} + +func TestSignVote(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + _, tempFilePath := cmn.Tempfile("priv_validator_") + privVal := GenPrivValidatorJSON(tempFilePath) + + block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{}} + height, round := int64(10), 1 + voteType := types.VoteTypePrevote + + // sign a vote for first time + addr, err := privVal.Address() + require.Nil(err) + + vote := newVote(addr, 0, height, round, voteType, block1) + err = privVal.SignVote("mychainid", vote) + assert.NoError(err, "expected no error signing vote") + + // try to sign the same vote again; should be fine + err = privVal.SignVote("mychainid", vote) + assert.NoError(err, "expected no error on signing same vote") + + // now try some bad votes + cases := []*types.Vote{ + newVote(addr, 0, height, round-1, voteType, block1), // round regression + newVote(addr, 0, height-1, round, voteType, block1), // height regression + newVote(addr, 0, height-2, round+4, voteType, block1), // height regression and different round + newVote(addr, 0, height, round, voteType, block2), // different block + } + + for _, c := range cases { + err = privVal.SignVote("mychainid", c) + assert.Error(err, "expected error on signing conflicting vote") + } + + // try signing a vote with a different time stamp + sig := vote.Signature + vote.Timestamp = vote.Timestamp.Add(time.Duration(1000)) + err = privVal.SignVote("mychainid", vote) + assert.NoError(err) + assert.Equal(sig, vote.Signature) +} + +func TestSignProposal(t *testing.T) { + assert := assert.New(t) + + _, tempFilePath := cmn.Tempfile("priv_validator_") + privVal := GenPrivValidatorJSON(tempFilePath) + + block1 := types.PartSetHeader{5, []byte{1, 2, 3}} + block2 := types.PartSetHeader{10, []byte{3, 2, 1}} + height, round := int64(10), 1 + + // sign a proposal for first time + proposal := newProposal(height, round, block1) + err := privVal.SignProposal("mychainid", proposal) + assert.NoError(err, "expected no error signing proposal") + + // try to sign the same proposal again; should be fine + err = privVal.SignProposal("mychainid", proposal) + assert.NoError(err, "expected no error on signing same proposal") + + // now try some bad Proposals + cases := []*types.Proposal{ + newProposal(height, round-1, block1), // round regression + newProposal(height-1, round, block1), // height regression + newProposal(height-2, round+4, block1), // height regression and different round + newProposal(height, round, block2), // different block + } + + for _, c := range cases { + err = privVal.SignProposal("mychainid", c) + assert.Error(err, "expected error on signing conflicting proposal") + } + + // try signing a proposal with a different time stamp + sig := proposal.Signature + proposal.Timestamp = proposal.Timestamp.Add(time.Duration(1000)) + err = privVal.SignProposal("mychainid", proposal) + assert.NoError(err) + assert.Equal(sig, proposal.Signature) +} + +func TestDifferByTimestamp(t *testing.T) { + require := require.New(t) + + _, tempFilePath := cmn.Tempfile("priv_validator_") + privVal := GenPrivValidatorJSON(tempFilePath) + + block1 := types.PartSetHeader{5, []byte{1, 2, 3}} + height, round := int64(10), 1 + chainID := "mychainid" + + // test proposal + { + proposal := newProposal(height, round, block1) + err := privVal.SignProposal(chainID, proposal) + assert.NoError(t, err, "expected no error signing proposal") + signBytes := proposal.SignBytes(chainID) + sig := proposal.Signature + timeStamp := clipToMS(proposal.Timestamp) + + // manipulate the timestamp. should get changed back + proposal.Timestamp = proposal.Timestamp.Add(time.Millisecond) + proposal.Signature = crypto.Signature{} + err = privVal.SignProposal("mychainid", proposal) + assert.NoError(t, err, "expected no error on signing same proposal") + + assert.Equal(t, timeStamp, proposal.Timestamp) + assert.Equal(t, signBytes, proposal.SignBytes(chainID)) + assert.Equal(t, sig, proposal.Signature) + } + + // test vote + { + addr, err := privVal.Address() + require.Nil(err) + + voteType := types.VoteTypePrevote + blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + vote := newVote(addr, 0, height, round, voteType, blockID) + err = privVal.SignVote("mychainid", vote) + assert.NoError(t, err, "expected no error signing vote") + + signBytes := vote.SignBytes(chainID) + sig := vote.Signature + timeStamp := clipToMS(vote.Timestamp) + + // manipulate the timestamp. should get changed back + vote.Timestamp = vote.Timestamp.Add(time.Millisecond) + vote.Signature = crypto.Signature{} + err = privVal.SignVote("mychainid", vote) + assert.NoError(t, err, "expected no error on signing same vote") + + assert.Equal(t, timeStamp, vote.Timestamp) + assert.Equal(t, signBytes, vote.SignBytes(chainID)) + assert.Equal(t, sig, vote.Signature) + } +} + +func newVote(addr cmn.HexBytes, idx int, height int64, round int, typ byte, blockID types.BlockID) *types.Vote { + return &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: round, + Type: typ, + Timestamp: time.Now().UTC(), + BlockID: blockID, + } +} + +func newProposal(height int64, round int, partsHeader types.PartSetHeader) *types.Proposal { + return &types.Proposal{ + Height: height, + Round: round, + BlockPartsHeader: partsHeader, + Timestamp: time.Now().UTC(), + } +} + +func clipToMS(t time.Time) time.Time { + nano := t.UnixNano() + million := int64(1000000) + nano = (nano / million) * million + return time.Unix(0, nano).UTC() +} diff --git a/types/priv_validator/sign_info.go b/types/priv_validator/sign_info.go new file mode 100644 index 000000000..746131a96 --- /dev/null +++ b/types/priv_validator/sign_info.go @@ -0,0 +1,238 @@ +package types + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "time" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" +) + +// TODO: type ? +const ( + stepNone int8 = 0 // Used to distinguish the initial state + stepPropose int8 = 1 + stepPrevote int8 = 2 + stepPrecommit int8 = 3 +) + +func voteToStep(vote *types.Vote) int8 { + switch vote.Type { + case types.VoteTypePrevote: + return stepPrevote + case types.VoteTypePrecommit: + return stepPrecommit + default: + panic("Unknown vote type") + } +} + +//------------------------------------- + +// LastSignedInfo contains information about the latest +// data signed by a validator to help prevent double signing. +type LastSignedInfo struct { + Height int64 `json:"height"` + Round int `json:"round"` + Step int8 `json:"step"` + Signature crypto.Signature `json:"signature,omitempty"` // so we dont lose signatures + SignBytes cmn.HexBytes `json:"signbytes,omitempty"` // so we dont lose signatures +} + +func NewLastSignedInfo() *LastSignedInfo { + return &LastSignedInfo{ + Step: stepNone, + } +} + +func (info *LastSignedInfo) String() string { + return fmt.Sprintf("LH:%v, LR:%v, LS:%v", info.Height, info.Round, info.Step) +} + +// Verify returns an error if there is a height/round/step regression +// or if the HRS matches but there are no LastSignBytes. +// It returns true if HRS matches exactly and the LastSignature exists. +// It panics if the HRS matches, the LastSignBytes are not empty, but the LastSignature is empty. +func (info LastSignedInfo) Verify(height int64, round int, step int8) (bool, error) { + if info.Height > height { + return false, errors.New("Height regression") + } + + if info.Height == height { + if info.Round > round { + return false, errors.New("Round regression") + } + + if info.Round == round { + if info.Step > step { + return false, errors.New("Step regression") + } else if info.Step == step { + if info.SignBytes != nil { + if info.Signature.Empty() { + panic("info: LastSignature is nil but LastSignBytes is not!") + } + return true, nil + } + return false, errors.New("No LastSignature found") + } + } + } + return false, nil +} + +// Set height/round/step and signature on the info +func (info *LastSignedInfo) Set(height int64, round int, step int8, + signBytes []byte, sig crypto.Signature) { + + info.Height = height + info.Round = round + info.Step = step + info.Signature = sig + info.SignBytes = signBytes +} + +// Reset resets all the values. +// XXX: Unsafe. +func (info *LastSignedInfo) Reset() { + info.Height = 0 + info.Round = 0 + info.Step = 0 + info.Signature = crypto.Signature{} + info.SignBytes = nil +} + +// SignVote checks the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. +// If so, it signs the vote, updates the LastSignedInfo, and sets the signature on the vote. +// If the HRS are equal and the only thing changed is the timestamp, it sets the vote.Timestamp to the previous +// value and the Signature to the LastSignedInfo.Signature. +// Else it returns an error. +func (lsi *LastSignedInfo) SignVote(signer types.Signer, chainID string, vote *types.Vote) error { + height, round, step := vote.Height, vote.Round, voteToStep(vote) + signBytes := vote.SignBytes(chainID) + + sameHRS, err := lsi.Verify(height, round, step) + if err != nil { + return err + } + + // We might crash before writing to the wal, + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error + if sameHRS { + if bytes.Equal(signBytes, lsi.SignBytes) { + vote.Signature = lsi.Signature + } else if timestamp, ok := checkVotesOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { + vote.Timestamp = timestamp + vote.Signature = lsi.Signature + } else { + err = fmt.Errorf("Conflicting data") + } + return err + } + sig, err := signer.Sign(signBytes) + if err != nil { + return err + } + lsi.Set(height, round, step, signBytes, sig) + vote.Signature = sig + return nil +} + +// SignProposal checks if the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. +// If so, it signs the proposal, updates the LastSignedInfo, and sets the signature on the proposal. +// If the HRS are equal and the only thing changed is the timestamp, it sets the timestamp to the previous +// value and the Signature to the LastSignedInfo.Signature. +// Else it returns an error. +func (lsi *LastSignedInfo) SignProposal(signer types.Signer, chainID string, proposal *types.Proposal) error { + height, round, step := proposal.Height, proposal.Round, stepPropose + signBytes := proposal.SignBytes(chainID) + + sameHRS, err := lsi.Verify(height, round, step) + if err != nil { + return err + } + + // We might crash before writing to the wal, + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error + if sameHRS { + if bytes.Equal(signBytes, lsi.SignBytes) { + proposal.Signature = lsi.Signature + } else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { + proposal.Timestamp = timestamp + proposal.Signature = lsi.Signature + } else { + err = fmt.Errorf("Conflicting data") + } + return err + } + sig, err := signer.Sign(signBytes) + if err != nil { + return err + } + lsi.Set(height, round, step, signBytes, sig) + proposal.Signature = sig + return nil +} + +//------------------------------------- + +// returns the timestamp from the lastSignBytes. +// returns true if the only difference in the votes is their timestamp. +func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { + var lastVote, newVote types.CanonicalJSONOnceVote + if err := json.Unmarshal(lastSignBytes, &lastVote); err != nil { + panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err)) + } + if err := json.Unmarshal(newSignBytes, &newVote); err != nil { + panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err)) + } + + lastTime, err := time.Parse(types.TimeFormat, lastVote.Vote.Timestamp) + if err != nil { + panic(err) + } + + // set the times to the same value and check equality + now := types.CanonicalTime(time.Now()) + lastVote.Vote.Timestamp = now + newVote.Vote.Timestamp = now + lastVoteBytes, _ := json.Marshal(lastVote) + newVoteBytes, _ := json.Marshal(newVote) + + return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes) +} + +// returns the timestamp from the lastSignBytes. +// returns true if the only difference in the proposals is their timestamp +func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { + var lastProposal, newProposal types.CanonicalJSONOnceProposal + if err := json.Unmarshal(lastSignBytes, &lastProposal); err != nil { + panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err)) + } + if err := json.Unmarshal(newSignBytes, &newProposal); err != nil { + panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err)) + } + + lastTime, err := time.Parse(types.TimeFormat, lastProposal.Proposal.Timestamp) + if err != nil { + panic(err) + } + + // set the times to the same value and check equality + now := types.CanonicalTime(time.Now()) + lastProposal.Proposal.Timestamp = now + newProposal.Proposal.Timestamp = now + lastProposalBytes, _ := json.Marshal(lastProposal) + newProposalBytes, _ := json.Marshal(newProposal) + + return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes) +} diff --git a/types/priv_validator/socket.go b/types/priv_validator/socket.go new file mode 100644 index 000000000..26cab72b9 --- /dev/null +++ b/types/priv_validator/socket.go @@ -0,0 +1,564 @@ +package types + +import ( + "fmt" + "io" + "net" + "time" + + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + p2pconn "github.com/tendermint/tendermint/p2p/conn" + "github.com/tendermint/tendermint/types" +) + +const ( + defaultAcceptDeadlineSeconds = 3 + defaultConnDeadlineSeconds = 3 + defaultConnHeartBeatSeconds = 30 + defaultConnWaitSeconds = 60 + defaultDialRetries = 10 +) + +// Socket errors. +var ( + ErrDialRetryMax = errors.New("dialed maximum retries") + ErrConnWaitTimeout = errors.New("waited for remote signer for too long") + ErrConnTimeout = errors.New("remote signer timed out") +) + +var ( + acceptDeadline = time.Second + defaultAcceptDeadlineSeconds + connDeadline = time.Second * defaultConnDeadlineSeconds + connHeartbeat = time.Second * defaultConnHeartBeatSeconds +) + +// SocketClientOption sets an optional parameter on the SocketClient. +type SocketClientOption func(*SocketClient) + +// SocketClientAcceptDeadline sets the deadline for the SocketClient listener. +// A zero time value disables the deadline. +func SocketClientAcceptDeadline(deadline time.Duration) SocketClientOption { + return func(sc *SocketClient) { sc.acceptDeadline = deadline } +} + +// SocketClientConnDeadline sets the read and write deadline for connections +// from external signing processes. +func SocketClientConnDeadline(deadline time.Duration) SocketClientOption { + return func(sc *SocketClient) { sc.connDeadline = deadline } +} + +// SocketClientHeartbeat sets the period on which to check the liveness of the +// connected Signer connections. +func SocketClientHeartbeat(period time.Duration) SocketClientOption { + return func(sc *SocketClient) { sc.connHeartbeat = period } +} + +// SocketClientConnWait sets the timeout duration before connection of external +// signing processes are considered to be unsuccessful. +func SocketClientConnWait(timeout time.Duration) SocketClientOption { + return func(sc *SocketClient) { sc.connWaitTimeout = timeout } +} + +// SocketClient implements PrivValidator, it uses a socket to request signatures +// from an external process. +type SocketClient struct { + cmn.BaseService + + addr string + acceptDeadline time.Duration + connDeadline time.Duration + connHeartbeat time.Duration + connWaitTimeout time.Duration + privKey crypto.PrivKeyEd25519 + + conn net.Conn + listener net.Listener +} + +// Check that SocketClient implements PrivValidator2. +var _ types.PrivValidator2 = (*SocketClient)(nil) + +// NewSocketClient returns an instance of SocketClient. +func NewSocketClient( + logger log.Logger, + socketAddr string, + privKey crypto.PrivKeyEd25519, +) *SocketClient { + sc := &SocketClient{ + addr: socketAddr, + acceptDeadline: acceptDeadline, + connDeadline: connDeadline, + connHeartbeat: connHeartbeat, + connWaitTimeout: time.Second * defaultConnWaitSeconds, + privKey: privKey, + } + + sc.BaseService = *cmn.NewBaseService(logger, "SocketClient", sc) + + return sc +} + +// GetAddress implements PrivValidator. +// TODO(xla): Remove when PrivValidator2 replaced PrivValidator. +func (sc *SocketClient) GetAddress() types.Address { + addr, err := sc.Address() + if err != nil { + panic(err) + } + + return addr +} + +// Address is an alias for PubKey().Address(). +func (sc *SocketClient) Address() (cmn.HexBytes, error) { + p, err := sc.PubKey() + if err != nil { + return nil, err + } + + return p.Address(), nil +} + +// GetPubKey implements PrivValidator. +// TODO(xla): Remove when PrivValidator2 replaced PrivValidator. +func (sc *SocketClient) GetPubKey() crypto.PubKey { + pubKey, err := sc.PubKey() + if err != nil { + panic(err) + } + + return pubKey +} + +// PubKey implements PrivValidator2. +func (sc *SocketClient) PubKey() (crypto.PubKey, error) { + err := writeMsg(sc.conn, &PubKeyMsg{}) + if err != nil { + return crypto.PubKey{}, err + } + + res, err := readMsg(sc.conn) + if err != nil { + return crypto.PubKey{}, err + } + + return res.(*PubKeyMsg).PubKey, nil +} + +// SignVote implements PrivValidator2. +func (sc *SocketClient) SignVote(chainID string, vote *types.Vote) error { + err := writeMsg(sc.conn, &SignVoteMsg{Vote: vote}) + if err != nil { + return err + } + + res, err := readMsg(sc.conn) + if err != nil { + return err + } + + *vote = *res.(*SignVoteMsg).Vote + + return nil +} + +// SignProposal implements PrivValidator2. +func (sc *SocketClient) SignProposal( + chainID string, + proposal *types.Proposal, +) error { + err := writeMsg(sc.conn, &SignProposalMsg{Proposal: proposal}) + if err != nil { + return err + } + + res, err := readMsg(sc.conn) + if err != nil { + return err + } + + *proposal = *res.(*SignProposalMsg).Proposal + + return nil +} + +// SignHeartbeat implements PrivValidator2. +func (sc *SocketClient) SignHeartbeat( + chainID string, + heartbeat *types.Heartbeat, +) error { + err := writeMsg(sc.conn, &SignHeartbeatMsg{Heartbeat: heartbeat}) + if err != nil { + return err + } + + res, err := readMsg(sc.conn) + if err != nil { + return err + } + + *heartbeat = *res.(*SignHeartbeatMsg).Heartbeat + + return nil +} + +// OnStart implements cmn.Service. +func (sc *SocketClient) OnStart() error { + if err := sc.listen(); err != nil { + sc.Logger.Error( + "OnStart", + "err", errors.Wrap(err, "failed to listen"), + ) + + return err + } + + conn, err := sc.waitConnection() + if err != nil { + sc.Logger.Error( + "OnStart", + "err", errors.Wrap(err, "failed to accept connection"), + ) + + return err + } + + sc.conn = conn + + return nil +} + +// OnStop implements cmn.Service. +func (sc *SocketClient) OnStop() { + if sc.conn != nil { + if err := sc.conn.Close(); err != nil { + sc.Logger.Error( + "OnStop", + "err", errors.Wrap(err, "failed to close connection"), + ) + } + } + + if sc.listener != nil { + if err := sc.listener.Close(); err != nil { + sc.Logger.Error( + "OnStop", + "err", errors.Wrap(err, "failed to close listener"), + ) + } + } +} + +func (sc *SocketClient) acceptConnection() (net.Conn, error) { + conn, err := sc.listener.Accept() + if err != nil { + if !sc.IsRunning() { + return nil, nil // Ignore error from listener closing. + } + return nil, err + + } + + conn, err = p2pconn.MakeSecretConnection(conn, sc.privKey.Wrap()) + if err != nil { + return nil, err + } + + return conn, nil +} + +func (sc *SocketClient) listen() error { + ln, err := net.Listen(cmn.ProtocolAndAddress(sc.addr)) + if err != nil { + return err + } + + sc.listener = newTCPTimeoutListener( + ln, + sc.acceptDeadline, + sc.connDeadline, + sc.connHeartbeat, + ) + + return nil +} + +// waitConnection uses the configured wait timeout to error if no external +// process connects in the time period. +func (sc *SocketClient) waitConnection() (net.Conn, error) { + var ( + connc = make(chan net.Conn, 1) + errc = make(chan error, 1) + ) + + go func(connc chan<- net.Conn, errc chan<- error) { + conn, err := sc.acceptConnection() + if err != nil { + errc <- err + return + } + + connc <- conn + }(connc, errc) + + select { + case conn := <-connc: + return conn, nil + case err := <-errc: + if _, ok := err.(timeoutError); ok { + return nil, errors.Wrap(ErrConnWaitTimeout, err.Error()) + } + return nil, err + case <-time.After(sc.connWaitTimeout): + return nil, ErrConnWaitTimeout + } +} + +//--------------------------------------------------------- + +// RemoteSignerOption sets an optional parameter on the RemoteSigner. +type RemoteSignerOption func(*RemoteSigner) + +// RemoteSignerConnDeadline sets the read and write deadline for connections +// from external signing processes. +func RemoteSignerConnDeadline(deadline time.Duration) RemoteSignerOption { + return func(ss *RemoteSigner) { ss.connDeadline = deadline } +} + +// RemoteSignerConnRetries sets the amount of attempted retries to connect. +func RemoteSignerConnRetries(retries int) RemoteSignerOption { + return func(ss *RemoteSigner) { ss.connRetries = retries } +} + +// RemoteSigner implements PrivValidator by dialing to a socket. +type RemoteSigner struct { + cmn.BaseService + + addr string + chainID string + connDeadline time.Duration + connRetries int + privKey crypto.PrivKeyEd25519 + privVal PrivValidator + + conn net.Conn +} + +// NewRemoteSigner returns an instance of RemoteSigner. +func NewRemoteSigner( + logger log.Logger, + chainID, socketAddr string, + privVal PrivValidator, + privKey crypto.PrivKeyEd25519, +) *RemoteSigner { + rs := &RemoteSigner{ + addr: socketAddr, + chainID: chainID, + connDeadline: time.Second * defaultConnDeadlineSeconds, + connRetries: defaultDialRetries, + privKey: privKey, + privVal: privVal, + } + + rs.BaseService = *cmn.NewBaseService(logger, "RemoteSigner", rs) + + return rs +} + +// OnStart implements cmn.Service. +func (rs *RemoteSigner) OnStart() error { + conn, err := rs.connect() + if err != nil { + rs.Logger.Error("OnStart", "err", errors.Wrap(err, "connect")) + + return err + } + + go rs.handleConnection(conn) + + return nil +} + +// OnStop implements cmn.Service. +func (rs *RemoteSigner) OnStop() { + if rs.conn == nil { + return + } + + if err := rs.conn.Close(); err != nil { + rs.Logger.Error("OnStop", "err", errors.Wrap(err, "closing listener failed")) + } +} + +func (rs *RemoteSigner) connect() (net.Conn, error) { + for retries := rs.connRetries; retries > 0; retries-- { + // Don't sleep if it is the first retry. + if retries != rs.connRetries { + time.Sleep(rs.connDeadline) + } + + conn, err := cmn.Connect(rs.addr) + if err != nil { + rs.Logger.Error( + "connect", + "addr", rs.addr, + "err", errors.Wrap(err, "connection failed"), + ) + + continue + } + + if err := conn.SetDeadline(time.Now().Add(connDeadline)); err != nil { + rs.Logger.Error( + "connect", + "err", errors.Wrap(err, "setting connection timeout failed"), + ) + continue + } + + conn, err = p2pconn.MakeSecretConnection(conn, rs.privKey.Wrap()) + if err != nil { + rs.Logger.Error( + "connect", + "err", errors.Wrap(err, "encrypting connection failed"), + ) + + continue + } + + return conn, nil + } + + return nil, ErrDialRetryMax +} + +func (rs *RemoteSigner) handleConnection(conn net.Conn) { + for { + if !rs.IsRunning() { + return // Ignore error from listener closing. + } + + req, err := readMsg(conn) + if err != nil { + if err != io.EOF { + rs.Logger.Error("handleConnection", "err", err) + } + return + } + + var res PrivValMsg + + switch r := req.(type) { + case *PubKeyMsg: + var p crypto.PubKey + + p, err = rs.privVal.PubKey() + res = &PubKeyMsg{p} + case *SignVoteMsg: + err = rs.privVal.SignVote(rs.chainID, r.Vote) + res = &SignVoteMsg{r.Vote} + case *SignProposalMsg: + err = rs.privVal.SignProposal(rs.chainID, r.Proposal) + res = &SignProposalMsg{r.Proposal} + case *SignHeartbeatMsg: + err = rs.privVal.SignHeartbeat(rs.chainID, r.Heartbeat) + res = &SignHeartbeatMsg{r.Heartbeat} + default: + err = fmt.Errorf("unknown msg: %v", r) + } + + if err != nil { + rs.Logger.Error("handleConnection", "err", err) + return + } + + err = writeMsg(conn, res) + if err != nil { + rs.Logger.Error("handleConnection", "err", err) + return + } + } +} + +//--------------------------------------------------------- + +const ( + msgTypePubKey = byte(0x01) + msgTypeSignVote = byte(0x10) + msgTypeSignProposal = byte(0x11) + msgTypeSignHeartbeat = byte(0x12) +) + +// PrivValMsg is sent between RemoteSigner and SocketClient. +type PrivValMsg interface{} + +var _ = wire.RegisterInterface( + struct{ PrivValMsg }{}, + wire.ConcreteType{&PubKeyMsg{}, msgTypePubKey}, + wire.ConcreteType{&SignVoteMsg{}, msgTypeSignVote}, + wire.ConcreteType{&SignProposalMsg{}, msgTypeSignProposal}, + wire.ConcreteType{&SignHeartbeatMsg{}, msgTypeSignHeartbeat}, +) + +// PubKeyMsg is a PrivValidatorSocket message containing the public key. +type PubKeyMsg struct { + PubKey crypto.PubKey +} + +// SignVoteMsg is a PrivValidatorSocket message containing a vote. +type SignVoteMsg struct { + Vote *types.Vote +} + +// SignProposalMsg is a PrivValidatorSocket message containing a Proposal. +type SignProposalMsg struct { + Proposal *types.Proposal +} + +// SignHeartbeatMsg is a PrivValidatorSocket message containing a Heartbeat. +type SignHeartbeatMsg struct { + Heartbeat *types.Heartbeat +} + +func readMsg(r io.Reader) (PrivValMsg, error) { + var ( + n int + err error + ) + + read := wire.ReadBinary(struct{ PrivValMsg }{}, r, 0, &n, &err) + if err != nil { + if _, ok := err.(timeoutError); ok { + return nil, errors.Wrap(ErrConnTimeout, err.Error()) + } + + return nil, err + } + + w, ok := read.(struct{ PrivValMsg }) + if !ok { + return nil, errors.New("unknown type") + } + + return w.PrivValMsg, nil +} + +func writeMsg(w io.Writer, msg interface{}) error { + var ( + err error + n int + ) + + // TODO(xla): This extra wrap should be gone with the sdk-2 update. + wire.WriteBinary(struct{ PrivValMsg }{msg}, w, &n, &err) + if _, ok := err.(timeoutError); ok { + return errors.Wrap(ErrConnTimeout, err.Error()) + } + + return err +} diff --git a/types/priv_validator/socket_tcp.go b/types/priv_validator/socket_tcp.go new file mode 100644 index 000000000..2421eb9f4 --- /dev/null +++ b/types/priv_validator/socket_tcp.go @@ -0,0 +1,66 @@ +package types + +import ( + "net" + "time" +) + +// timeoutError can be used to check if an error returned from the netp package +// was due to a timeout. +type timeoutError interface { + Timeout() bool +} + +// tcpTimeoutListener implements net.Listener. +var _ net.Listener = (*tcpTimeoutListener)(nil) + +// tcpTimeoutListener wraps a *net.TCPListener to standardise protocol timeouts +// and potentially other tuning parameters. +type tcpTimeoutListener struct { + *net.TCPListener + + acceptDeadline time.Duration + connDeadline time.Duration + period time.Duration +} + +// newTCPTimeoutListener returns an instance of tcpTimeoutListener. +func newTCPTimeoutListener( + ln net.Listener, + acceptDeadline, connDeadline time.Duration, + period time.Duration, +) tcpTimeoutListener { + return tcpTimeoutListener{ + TCPListener: ln.(*net.TCPListener), + acceptDeadline: acceptDeadline, + connDeadline: connDeadline, + period: period, + } +} + +// Accept implements net.Listener. +func (ln tcpTimeoutListener) Accept() (net.Conn, error) { + err := ln.SetDeadline(time.Now().Add(ln.acceptDeadline)) + if err != nil { + return nil, err + } + + tc, err := ln.AcceptTCP() + if err != nil { + return nil, err + } + + if err := tc.SetDeadline(time.Now().Add(ln.connDeadline)); err != nil { + return nil, err + } + + if err := tc.SetKeepAlive(true); err != nil { + return nil, err + } + + if err := tc.SetKeepAlivePeriod(ln.period); err != nil { + return nil, err + } + + return tc, nil +} diff --git a/types/priv_validator/socket_tcp_test.go b/types/priv_validator/socket_tcp_test.go new file mode 100644 index 000000000..cd95ab0b9 --- /dev/null +++ b/types/priv_validator/socket_tcp_test.go @@ -0,0 +1,64 @@ +package types + +import ( + "net" + "testing" + "time" +) + +func TestTCPTimeoutListenerAcceptDeadline(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + ln = newTCPTimeoutListener(ln, time.Millisecond, time.Second, time.Second) + + _, err = ln.Accept() + opErr, ok := err.(*net.OpError) + if !ok { + t.Fatalf("have %v, want *net.OpError", err) + } + + if have, want := opErr.Op, "accept"; have != want { + t.Errorf("have %v, want %v", have, want) + } +} + +func TestTCPTimeoutListenerConnDeadline(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + ln = newTCPTimeoutListener(ln, time.Second, time.Millisecond, time.Second) + + donec := make(chan struct{}) + go func(ln net.Listener) { + defer close(donec) + + c, err := ln.Accept() + if err != nil { + t.Fatal(err) + } + + time.Sleep(2 * time.Millisecond) + + _, err = c.Write([]byte("foo")) + opErr, ok := err.(*net.OpError) + if !ok { + t.Fatalf("have %v, want *net.OpError", err) + } + + if have, want := opErr.Op, "write"; have != want { + t.Errorf("have %v, want %v", have, want) + } + }(ln) + + _, err = net.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + + <-donec +} diff --git a/types/priv_validator/socket_test.go b/types/priv_validator/socket_test.go new file mode 100644 index 000000000..2859c9452 --- /dev/null +++ b/types/priv_validator/socket_test.go @@ -0,0 +1,286 @@ +package types + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + p2pconn "github.com/tendermint/tendermint/p2p/conn" + "github.com/tendermint/tendermint/types" +) + +func TestSocketClientAddress(t *testing.T) { + var ( + chainID = cmn.RandStr(12) + sc, rs = testSetupSocketPair(t, chainID) + ) + defer sc.Stop() + defer rs.Stop() + + serverAddr, err := rs.privVal.Address() + require.NoError(t, err) + + clientAddr, err := sc.Address() + require.NoError(t, err) + + assert.Equal(t, serverAddr, clientAddr) + + // TODO(xla): Remove when PrivValidator2 replaced PrivValidator. + assert.Equal(t, serverAddr, sc.GetAddress()) + +} + +func TestSocketClientPubKey(t *testing.T) { + var ( + chainID = cmn.RandStr(12) + sc, rs = testSetupSocketPair(t, chainID) + ) + defer sc.Stop() + defer rs.Stop() + + clientKey, err := sc.PubKey() + require.NoError(t, err) + + privKey, err := rs.privVal.PubKey() + require.NoError(t, err) + + assert.Equal(t, privKey, clientKey) + + // TODO(xla): Remove when PrivValidator2 replaced PrivValidator. + assert.Equal(t, privKey, sc.GetPubKey()) +} + +func TestSocketClientProposal(t *testing.T) { + var ( + chainID = cmn.RandStr(12) + sc, rs = testSetupSocketPair(t, chainID) + + ts = time.Now() + privProposal = &types.Proposal{Timestamp: ts} + clientProposal = &types.Proposal{Timestamp: ts} + ) + defer sc.Stop() + defer rs.Stop() + + require.NoError(t, rs.privVal.SignProposal(chainID, privProposal)) + require.NoError(t, sc.SignProposal(chainID, clientProposal)) + assert.Equal(t, privProposal.Signature, clientProposal.Signature) +} + +func TestSocketClientVote(t *testing.T) { + var ( + chainID = cmn.RandStr(12) + sc, rs = testSetupSocketPair(t, chainID) + + ts = time.Now() + vType = types.VoteTypePrecommit + want = &types.Vote{Timestamp: ts, Type: vType} + have = &types.Vote{Timestamp: ts, Type: vType} + ) + defer sc.Stop() + defer rs.Stop() + + require.NoError(t, rs.privVal.SignVote(chainID, want)) + require.NoError(t, sc.SignVote(chainID, have)) + assert.Equal(t, want.Signature, have.Signature) +} + +func TestSocketClientHeartbeat(t *testing.T) { + var ( + chainID = cmn.RandStr(12) + sc, rs = testSetupSocketPair(t, chainID) + + want = &types.Heartbeat{} + have = &types.Heartbeat{} + ) + defer sc.Stop() + defer rs.Stop() + + require.NoError(t, rs.privVal.SignHeartbeat(chainID, want)) + require.NoError(t, sc.SignHeartbeat(chainID, have)) + assert.Equal(t, want.Signature, have.Signature) +} + +func TestSocketClientAcceptDeadline(t *testing.T) { + var ( + sc = NewSocketClient( + log.TestingLogger(), + "127.0.0.1:0", + crypto.GenPrivKeyEd25519(), + ) + ) + defer sc.Stop() + + SocketClientAcceptDeadline(time.Millisecond)(sc) + + assert.Equal(t, errors.Cause(sc.Start()), ErrConnWaitTimeout) +} + +func TestSocketClientDeadline(t *testing.T) { + var ( + addr = testFreeAddr(t) + listenc = make(chan struct{}) + sc = NewSocketClient( + log.TestingLogger(), + addr, + crypto.GenPrivKeyEd25519(), + ) + ) + + SocketClientConnDeadline(10 * time.Millisecond)(sc) + SocketClientConnWait(500 * time.Millisecond)(sc) + + go func(sc *SocketClient) { + defer close(listenc) + + require.NoError(t, sc.Start()) + + assert.True(t, sc.IsRunning()) + }(sc) + + for { + conn, err := cmn.Connect(addr) + if err != nil { + continue + } + + _, err = p2pconn.MakeSecretConnection( + conn, + crypto.GenPrivKeyEd25519().Wrap(), + ) + if err == nil { + break + } + } + + <-listenc + + // Sleep to guarantee deadline has been hit. + time.Sleep(20 * time.Microsecond) + + _, err := sc.PubKey() + assert.Equal(t, errors.Cause(err), ErrConnTimeout) +} + +func TestSocketClientWait(t *testing.T) { + sc := NewSocketClient( + log.TestingLogger(), + "127.0.0.1:0", + crypto.GenPrivKeyEd25519(), + ) + defer sc.Stop() + + SocketClientConnWait(time.Millisecond)(sc) + + assert.Equal(t, errors.Cause(sc.Start()), ErrConnWaitTimeout) +} + +func TestRemoteSignerRetry(t *testing.T) { + var ( + attemptc = make(chan int) + retries = 2 + ) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go func(ln net.Listener, attemptc chan<- int) { + attempts := 0 + + for { + conn, err := ln.Accept() + require.NoError(t, err) + + err = conn.Close() + require.NoError(t, err) + + attempts++ + + if attempts == retries { + attemptc <- attempts + break + } + } + }(ln, attemptc) + + rs := NewRemoteSigner( + log.TestingLogger(), + cmn.RandStr(12), + ln.Addr().String(), + NewTestPrivValidator(types.GenSigner()), + crypto.GenPrivKeyEd25519(), + ) + defer rs.Stop() + + RemoteSignerConnDeadline(time.Millisecond)(rs) + RemoteSignerConnRetries(retries)(rs) + + assert.Equal(t, errors.Cause(rs.Start()), ErrDialRetryMax) + + select { + case attempts := <-attemptc: + assert.Equal(t, retries, attempts) + case <-time.After(100 * time.Millisecond): + t.Error("expected remote to observe connection attempts") + } +} + +func testSetupSocketPair( + t *testing.T, + chainID string, +) (*SocketClient, *RemoteSigner) { + var ( + addr = testFreeAddr(t) + logger = log.TestingLogger() + signer = types.GenSigner() + privVal = NewTestPrivValidator(signer) + readyc = make(chan struct{}) + rs = NewRemoteSigner( + logger, + chainID, + addr, + privVal, + crypto.GenPrivKeyEd25519(), + ) + sc = NewSocketClient( + logger, + addr, + crypto.GenPrivKeyEd25519(), + ) + ) + + go func(sc *SocketClient) { + require.NoError(t, sc.Start()) + assert.True(t, sc.IsRunning()) + + readyc <- struct{}{} + }(sc) + + RemoteSignerConnDeadline(time.Millisecond)(rs) + RemoteSignerConnRetries(1e6)(rs) + + require.NoError(t, rs.Start()) + assert.True(t, rs.IsRunning()) + + <-readyc + + return sc, rs +} + +// testFreeAddr claims a free port so we don't block on listener being ready. +func testFreeAddr(t *testing.T) string { + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) +} diff --git a/types/priv_validator/unencrypted.go b/types/priv_validator/unencrypted.go new file mode 100644 index 000000000..10a304d9e --- /dev/null +++ b/types/priv_validator/unencrypted.go @@ -0,0 +1,66 @@ +package types + +import ( + "fmt" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" +) + +//----------------------------------------------------------------- + +var _ types.PrivValidator2 = (*PrivValidatorUnencrypted)(nil) + +// PrivValidatorUnencrypted implements PrivValidator. +// It uses an in-memory crypto.PrivKey that is +// persisted to disk unencrypted. +type PrivValidatorUnencrypted struct { + ID types.ValidatorID `json:"id"` + PrivKey PrivKey `json:"priv_key"` + LastSignedInfo *LastSignedInfo `json:"last_signed_info"` +} + +// NewPrivValidatorUnencrypted returns an instance of PrivValidatorUnencrypted. +func NewPrivValidatorUnencrypted(priv crypto.PrivKey) *PrivValidatorUnencrypted { + return &PrivValidatorUnencrypted{ + ID: types.ValidatorID{ + Address: priv.PubKey().Address(), + PubKey: priv.PubKey(), + }, + PrivKey: PrivKey(priv), + LastSignedInfo: NewLastSignedInfo(), + } +} + +// String returns a string representation of the PrivValidatorUnencrypted +func (upv *PrivValidatorUnencrypted) String() string { + addr, err := upv.Address() + if err != nil { + panic(err) + } + + return fmt.Sprintf("PrivValidator{%v %v}", addr, upv.LastSignedInfo.String()) +} + +func (upv *PrivValidatorUnencrypted) Address() (cmn.HexBytes, error) { + return upv.PrivKey.PubKey().Address(), nil +} + +func (upv *PrivValidatorUnencrypted) PubKey() (crypto.PubKey, error) { + return upv.PrivKey.PubKey(), nil +} + +func (upv *PrivValidatorUnencrypted) SignVote(chainID string, vote *types.Vote) error { + return upv.LastSignedInfo.SignVote(upv.PrivKey, chainID, vote) +} + +func (upv *PrivValidatorUnencrypted) SignProposal(chainID string, proposal *types.Proposal) error { + return upv.LastSignedInfo.SignProposal(upv.PrivKey, chainID, proposal) +} + +func (upv *PrivValidatorUnencrypted) SignHeartbeat(chainID string, heartbeat *types.Heartbeat) error { + var err error + heartbeat.Signature, err = upv.PrivKey.Sign(heartbeat.SignBytes(chainID)) + return err +} diff --git a/types/priv_validator/upgrade.go b/types/priv_validator/upgrade.go new file mode 100644 index 000000000..063655421 --- /dev/null +++ b/types/priv_validator/upgrade.go @@ -0,0 +1,59 @@ +package types + +import ( + "encoding/json" + "io/ioutil" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" +) + +type PrivValidatorV1 struct { + Address cmn.HexBytes `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` + LastHeight int64 `json:"last_height"` + LastRound int `json:"last_round"` + LastStep int8 `json:"last_step"` + LastSignature crypto.Signature `json:"last_signature,omitempty"` // so we dont lose signatures + LastSignBytes cmn.HexBytes `json:"last_signbytes,omitempty"` // so we dont lose signatures + PrivKey crypto.PrivKey `json:"priv_key"` +} + +func UpgradePrivValidator(filePath string) (*PrivValidatorJSON, error) { + b, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + + pv := new(PrivValidatorV1) + err = json.Unmarshal(b, pv) + if err != nil { + return nil, err + } + + pvNew := &PrivValidatorJSON{ + PrivValidatorUnencrypted: &PrivValidatorUnencrypted{ + ID: types.ValidatorID{ + Address: pv.Address, + PubKey: pv.PubKey, + }, + PrivKey: PrivKey(pv.PrivKey), + LastSignedInfo: &LastSignedInfo{ + Height: pv.LastHeight, + Round: pv.LastRound, + Step: pv.LastStep, + SignBytes: pv.LastSignBytes, + Signature: pv.LastSignature, + }, + }, + } + + b, err = json.MarshalIndent(pvNew, "", " ") + if err != nil { + return nil, err + } + + err = ioutil.WriteFile(filePath, b, 0600) + return pvNew, err +} diff --git a/types/priv_validator/upgrade_pv/main.go b/types/priv_validator/upgrade_pv/main.go new file mode 100644 index 000000000..5a0d4f263 --- /dev/null +++ b/types/priv_validator/upgrade_pv/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + + priv_val "github.com/tendermint/tendermint/types/priv_validator" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("USAGE: priv_val_converter ") + os.Exit(1) + } + file := os.Args[1] + _, err := priv_val.UpgradePrivValidator(file) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/types/priv_validator_test.go b/types/priv_validator_test.go index 08b58273a..edfcdf58c 100644 --- a/types/priv_validator_test.go +++ b/types/priv_validator_test.go @@ -185,18 +185,19 @@ func TestDifferByTimestamp(t *testing.T) { proposal := newProposal(height, round, block1) err := privVal.SignProposal(chainID, proposal) assert.NoError(t, err, "expected no error signing proposal") - signBytes := SignBytes(chainID, proposal) + signBytes := proposal.SignBytes(chainID) sig := proposal.Signature timeStamp := clipToMS(proposal.Timestamp) // manipulate the timestamp. should get changed back proposal.Timestamp = proposal.Timestamp.Add(time.Millisecond) - proposal.Signature = crypto.Signature{} + var emptySig crypto.Signature + proposal.Signature = emptySig err = privVal.SignProposal("mychainid", proposal) assert.NoError(t, err, "expected no error on signing same proposal") assert.Equal(t, timeStamp, proposal.Timestamp) - assert.Equal(t, signBytes, SignBytes(chainID, proposal)) + assert.Equal(t, signBytes, proposal.SignBytes(chainID)) assert.Equal(t, sig, proposal.Signature) } @@ -208,18 +209,19 @@ func TestDifferByTimestamp(t *testing.T) { err := privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") - signBytes := SignBytes(chainID, vote) + signBytes := vote.SignBytes(chainID) sig := vote.Signature timeStamp := clipToMS(vote.Timestamp) // manipulate the timestamp. should get changed back vote.Timestamp = vote.Timestamp.Add(time.Millisecond) - vote.Signature = crypto.Signature{} + var emptySig crypto.Signature + vote.Signature = emptySig err = privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error on signing same vote") assert.Equal(t, timeStamp, vote.Timestamp) - assert.Equal(t, signBytes, SignBytes(chainID, vote)) + assert.Equal(t, signBytes, vote.SignBytes(chainID)) assert.Equal(t, sig, vote.Signature) } } diff --git a/types/proposal.go b/types/proposal.go index 98600681a..c240756bb 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -3,11 +3,10 @@ package types import ( "errors" "fmt" - "io" "time" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + "github.com/tendermint/tendermint/wire" ) var ( @@ -50,10 +49,14 @@ func (p *Proposal) String() string { p.POLBlockID, p.Signature, CanonicalTime(p.Timestamp)) } -// WriteSignBytes writes the Proposal bytes for signing -func (p *Proposal) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) { - wire.WriteJSON(CanonicalJSONOnceProposal{ +// SignBytes returns the Proposal bytes for signing +func (p *Proposal) SignBytes(chainID string) []byte { + bz, err := wire.MarshalJSON(CanonicalJSONOnceProposal{ ChainID: chainID, Proposal: CanonicalProposal(p), - }, w, n, err) + }) + if err != nil { + panic(err) + } + return bz } diff --git a/types/proposal_test.go b/types/proposal_test.go index 0d2af71e2..610f76855 100644 --- a/types/proposal_test.go +++ b/types/proposal_test.go @@ -6,13 +6,13 @@ import ( "github.com/stretchr/testify/require" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" ) var testProposal *Proposal func init() { - var stamp, err = time.Parse(timeFormat, "2018-02-11T07:09:22.765Z") + var stamp, err = time.Parse(TimeFormat, "2018-02-11T07:09:22.765Z") if err != nil { panic(err) } @@ -26,7 +26,7 @@ func init() { } func TestProposalSignable(t *testing.T) { - signBytes := SignBytes("test_chain_id", testProposal) + signBytes := testProposal.SignBytes("test_chain_id") signStr := string(signBytes) expected := `{"chain_id":"test_chain_id","proposal":{"block_parts_header":{"hash":"626C6F636B7061727473","total":111},"height":12345,"pol_block_id":{},"pol_round":-1,"round":23456,"timestamp":"2018-02-11T07:09:22.765Z"}}` @@ -48,24 +48,25 @@ func TestProposalVerifySignature(t *testing.T) { pubKey := privVal.GetPubKey() prop := NewProposal(4, 2, PartSetHeader{777, []byte("proper")}, 2, BlockID{}) - signBytes := SignBytes("test_chain_id", prop) + signBytes := prop.SignBytes("test_chain_id") // sign it signature, err := privVal.Signer.Sign(signBytes) require.NoError(t, err) // verify the same proposal - valid := pubKey.VerifyBytes(SignBytes("test_chain_id", prop), signature) + valid := pubKey.VerifyBytes(prop.SignBytes("test_chain_id"), signature) require.True(t, valid) // serialize, deserialize and verify again.... newProp := new(Proposal) - bs := wire.BinaryBytes(prop) - err = wire.ReadBinaryBytes(bs, &newProp) + bs, err := wire.MarshalBinary(prop) + require.NoError(t, err) + err = wire.UnmarshalBinary(bs, &newProp) require.NoError(t, err) // verify the transmitted proposal - newSignBytes := SignBytes("test_chain_id", newProp) + newSignBytes := newProp.SignBytes("test_chain_id") require.Equal(t, string(signBytes), string(newSignBytes)) valid = pubKey.VerifyBytes(newSignBytes, signature) require.True(t, valid) @@ -73,14 +74,14 @@ func TestProposalVerifySignature(t *testing.T) { func BenchmarkProposalWriteSignBytes(b *testing.B) { for i := 0; i < b.N; i++ { - SignBytes("test_chain_id", testProposal) + testProposal.SignBytes("test_chain_id") } } func BenchmarkProposalSign(b *testing.B) { privVal := GenPrivValidatorFS("") for i := 0; i < b.N; i++ { - _, err := privVal.Signer.Sign(SignBytes("test_chain_id", testProposal)) + _, err := privVal.Signer.Sign(testProposal.SignBytes("test_chain_id")) if err != nil { b.Error(err) } @@ -88,12 +89,12 @@ func BenchmarkProposalSign(b *testing.B) { } func BenchmarkProposalVerifySignature(b *testing.B) { - signBytes := SignBytes("test_chain_id", testProposal) + signBytes := testProposal.SignBytes("test_chain_id") privVal := GenPrivValidatorFS("") signature, _ := privVal.Signer.Sign(signBytes) pubKey := privVal.GetPubKey() for i := 0; i < b.N; i++ { - pubKey.VerifyBytes(SignBytes("test_chain_id", testProposal), signature) + pubKey.VerifyBytes(testProposal.SignBytes("test_chain_id"), signature) } } diff --git a/types/results.go b/types/results.go index 4a02f2857..71834664d 100644 --- a/types/results.go +++ b/types/results.go @@ -2,7 +2,7 @@ package types import ( abci "github.com/tendermint/abci/types" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" ) @@ -18,7 +18,7 @@ type ABCIResult struct { // Hash returns the canonical hash of the ABCIResult func (a ABCIResult) Hash() []byte { - return wire.BinaryRipemd160(a) + return tmHash(a) } // ABCIResults wraps the deliver tx results to return a proof @@ -40,9 +40,13 @@ func NewResultFromResponse(response *abci.ResponseDeliverTx) ABCIResult { } } -// Bytes serializes the ABCIResponse using go-wire +// Bytes serializes the ABCIResponse using wire func (a ABCIResults) Bytes() []byte { - return wire.BinaryBytes(a) + bz, err := wire.MarshalBinary(a) + if err != nil { + panic(err) + } + return bz } // Hash returns a merkle hash of all results diff --git a/types/signable.go b/types/signable.go index bfdf9faa1..19829ede7 100644 --- a/types/signable.go +++ b/types/signable.go @@ -1,24 +1,16 @@ package types -import ( - "bytes" - "io" - - cmn "github.com/tendermint/tmlibs/common" -) - // Signable is an interface for all signable things. // It typically removes signatures before serializing. +// SignBytes returns the bytes to be signed +// NOTE: chainIDs are part of the SignBytes but not +// necessarily the object themselves. +// NOTE: Expected to panic if there is an error marshalling. type Signable interface { - WriteSignBytes(chainID string, w io.Writer, n *int, err *error) + SignBytes(chainID string) []byte } -// SignBytes is a convenience method for getting the bytes to sign of a Signable. -func SignBytes(chainID string, o Signable) []byte { - buf, n, err := new(bytes.Buffer), new(int), new(error) - o.WriteSignBytes(chainID, buf, n, err) - if *err != nil { - cmn.PanicCrisis(err) - } - return buf.Bytes() +// HashSignBytes is a convenience method for getting the hash of the bytes of a signable +func HashSignBytes(chainID string, o Signable) []byte { + return tmHash(o.SignBytes(chainID)) } diff --git a/types/test_util.go b/types/test_util.go index d13de04e2..73e53eb19 100644 --- a/types/test_util.go +++ b/types/test_util.go @@ -29,7 +29,7 @@ func MakeCommit(blockID BlockID, height int64, round int, } func signAddVote(privVal *PrivValidatorFS, vote *Vote, voteSet *VoteSet) (signed bool, err error) { - vote.Signature, err = privVal.Signer.Sign(SignBytes(voteSet.ChainID(), vote)) + vote.Signature, err = privVal.Signer.Sign(vote.SignBytes(voteSet.ChainID())) if err != nil { return false, err } diff --git a/types/tx.go b/types/tx.go index d9e2e2ebd..fc1d27212 100644 --- a/types/tx.go +++ b/types/tx.go @@ -11,12 +11,12 @@ import ( ) // Tx is an arbitrary byte array. -// NOTE: Tx has no types at this level, so when go-wire encoded it's just length-prefixed. +// NOTE: Tx has no types at this level, so when wire encoded it's just length-prefixed. // Alternatively, it may make sense to add types here and let // []byte be type 0x1 so we can have versioned txs if need be in the future. type Tx []byte -// Hash computes the RIPEMD160 hash of the go-wire encoded transaction. +// Hash computes the RIPEMD160 hash of the wire encoded transaction. func (tx Tx) Hash() []byte { return wireHasher(tx).Hash() } diff --git a/types/tx_test.go b/types/tx_test.go index dc99509dd..340bd5a6b 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" ctest "github.com/tendermint/tmlibs/test" ) @@ -69,8 +69,9 @@ func TestValidTxProof(t *testing.T) { // read-write must also work var p2 TxProof - bin := wire.BinaryBytes(proof) - err := wire.ReadBinaryBytes(bin, &p2) + bin, err := wire.MarshalBinary(proof) + assert.Nil(err) + err = wire.UnmarshalBinary(bin, &p2) if assert.Nil(err, "%d: %d: %+v", h, i, err) { assert.Nil(p2.Validate(root), "%d: %d", h, i) } @@ -96,7 +97,8 @@ func testTxProofUnchangable(t *testing.T) { // make sure it is valid to start with assert.Nil(proof.Validate(root)) - bin := wire.BinaryBytes(proof) + bin, err := wire.MarshalBinary(proof) + assert.Nil(err) // try mutating the data and make sure nothing breaks for j := 0; j < 500; j++ { @@ -110,7 +112,7 @@ func testTxProofUnchangable(t *testing.T) { // this make sure the proof doesn't deserialize into something valid func assertBadProof(t *testing.T, root []byte, bad []byte, good TxProof) { var proof TxProof - err := wire.ReadBinaryBytes(bad, &proof) + err := wire.UnmarshalBinary(bad, &proof) if err == nil { err = proof.Validate(root) if err == nil { diff --git a/types/validator.go b/types/validator.go index dfe575515..a9b42fe8c 100644 --- a/types/validator.go +++ b/types/validator.go @@ -3,10 +3,8 @@ package types import ( "bytes" "fmt" - "io" "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" cmn "github.com/tendermint/tmlibs/common" ) @@ -14,9 +12,9 @@ import ( // NOTE: The Accum is not included in Validator.Hash(); // make sure to update that method if changes are made here type Validator struct { - Address Address `json:"address"` - PubKey crypto.PubKey `json:"pub_key"` - VotingPower int64 `json:"voting_power"` + Address Address `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` + VotingPower int64 `json:"voting_power"` Accum int64 `json:"accum"` } @@ -47,9 +45,10 @@ func (v *Validator) CompareAccum(other *Validator) *Validator { } else if v.Accum < other.Accum { return other } else { - if bytes.Compare(v.Address, other.Address) < 0 { + result := bytes.Compare(v.Address, other.Address) + if result < 0 { return v - } else if bytes.Compare(v.Address, other.Address) > 0 { + } else if result > 0 { return other } else { cmn.PanicSanity("Cannot compare identical validators") @@ -72,7 +71,7 @@ func (v *Validator) String() string { // Hash computes the unique ID of a validator with a given voting power. // It excludes the Accum value, which changes with every round. func (v *Validator) Hash() []byte { - return wire.BinaryRipemd160(struct { + return tmHash(struct { Address Address PubKey crypto.PubKey VotingPower int64 @@ -83,25 +82,6 @@ func (v *Validator) Hash() []byte { }) } -//------------------------------------- - -var ValidatorCodec = validatorCodec{} - -type validatorCodec struct{} - -func (vc validatorCodec) Encode(o interface{}, w io.Writer, n *int, err *error) { - wire.WriteBinary(o.(*Validator), w, n, err) -} - -func (vc validatorCodec) Decode(r io.Reader, n *int, err *error) interface{} { - return wire.ReadBinary(&Validator{}, r, 0, n, err) -} - -func (vc validatorCodec) Compare(o1 interface{}, o2 interface{}) int { - cmn.PanicSanity("ValidatorCodec.Compare not implemented") - return 0 -} - //-------------------------------------------------------------------------------- // For testing... diff --git a/types/validator_set.go b/types/validator_set.go index 0d6c3249e..83d066ec1 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -253,7 +253,7 @@ func (valSet *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height } _, val := valSet.GetByIndex(idx) // Validate signature - precommitSignBytes := SignBytes(chainID, precommit) + precommitSignBytes := precommit.SignBytes(chainID) if !val.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) { return fmt.Errorf("Invalid commit -- invalid signature: %v", precommit) } @@ -327,7 +327,7 @@ func (valSet *ValidatorSet) VerifyCommitAny(newSet *ValidatorSet, chainID string seen[vi] = true // Validate signature old school - precommitSignBytes := SignBytes(chainID, precommit) + precommitSignBytes := precommit.SignBytes(chainID) if !ov.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) { return errors.Errorf("Invalid commit -- invalid signature: %v", precommit) } diff --git a/types/validator_set_test.go b/types/validator_set_test.go index 9c7512378..b346be1be 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -6,33 +6,15 @@ import ( "strings" "testing" "testing/quick" + "time" "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" ) -func randPubKey() crypto.PubKey { - var pubKey [32]byte - copy(pubKey[:], cmn.RandBytes(32)) - return crypto.PubKeyEd25519(pubKey).Wrap() -} - -func randValidator_() *Validator { - val := NewValidator(randPubKey(), cmn.RandInt64()) - val.Accum = cmn.RandInt64() - return val -} - -func randValidatorSet(numValidators int) *ValidatorSet { - validators := make([]*Validator, numValidators) - for i := 0; i < numValidators; i++ { - validators[i] = randValidator_() - } - return NewValidatorSet(validators) -} - func TestCopy(t *testing.T) { vset := randValidatorSet(10) vsetHash := vset.Hash() @@ -48,6 +30,26 @@ func TestCopy(t *testing.T) { } } +func BenchmarkValidatorSetCopy(b *testing.B) { + b.StopTimer() + vset := NewValidatorSet([]*Validator{}) + for i := 0; i < 1000; i++ { + privKey := crypto.GenPrivKeyEd25519() + pubKey := privKey.PubKey() + val := NewValidator(pubKey, 0) + if !vset.Add(val) { + panic("Failed to add validator") + } + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + vset.Copy() + } +} + +//------------------------------------------------------------------- + func TestProposerSelection1(t *testing.T) { vset := NewValidatorSet([]*Validator{ newValidator([]byte("foo"), 1000), @@ -66,10 +68,6 @@ func TestProposerSelection1(t *testing.T) { } } -func newValidator(address []byte, power int64) *Validator { - return &Validator{Address: address, VotingPower: power} -} - func TestProposerSelection2(t *testing.T) { addr0 := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} addr1 := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} @@ -193,6 +191,48 @@ func TestProposerSelection3(t *testing.T) { } } +func newValidator(address []byte, power int64) *Validator { + return &Validator{Address: address, VotingPower: power} +} + +func randPubKey() crypto.PubKey { + var pubKey [32]byte + copy(pubKey[:], cmn.RandBytes(32)) + return crypto.PubKeyEd25519(pubKey).Wrap() +} + +func randValidator_() *Validator { + val := NewValidator(randPubKey(), cmn.RandInt64()) + val.Accum = cmn.RandInt64() + return val +} + +func randValidatorSet(numValidators int) *ValidatorSet { + validators := make([]*Validator, numValidators) + for i := 0; i < numValidators; i++ { + validators[i] = randValidator_() + } + return NewValidatorSet(validators) +} + +func (valSet *ValidatorSet) toBytes() []byte { + bz, err := wire.MarshalBinary(valSet) + if err != nil { + panic(err) + } + return bz +} + +func (valSet *ValidatorSet) fromBytes(b []byte) { + err := wire.UnmarshalBinary(b, &valSet) + if err != nil { + // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED + panic(err) + } +} + +//------------------------------------------------------------------- + func TestValidatorSetTotalVotingPowerOverflows(t *testing.T) { vset := NewValidatorSet([]*Validator{ {Address: []byte("a"), VotingPower: math.MaxInt64, Accum: 0}, @@ -272,38 +312,60 @@ func TestSafeSubClip(t *testing.T) { assert.EqualValues(t, math.MaxInt64, safeSubClip(math.MaxInt64, -10)) } -func BenchmarkValidatorSetCopy(b *testing.B) { - b.StopTimer() - vset := NewValidatorSet([]*Validator{}) - for i := 0; i < 1000; i++ { - privKey := crypto.GenPrivKeyEd25519() - pubKey := privKey.PubKey() - val := NewValidator(pubKey, 0) - if !vset.Add(val) { - panic("Failed to add validator") - } +//------------------------------------------------------------------- + +func TestValidatorSetVerifyCommit(t *testing.T) { + privKey := crypto.GenPrivKeyEd25519() + pubKey := privKey.PubKey() + v1 := NewValidator(pubKey, 1000) + vset := NewValidatorSet([]*Validator{v1}) + + chainID := "mychainID" + blockID := BlockID{Hash: []byte("hello")} + height := int64(5) + vote := &Vote{ + ValidatorAddress: v1.Address, + ValidatorIndex: 0, + Height: height, + Round: 0, + Timestamp: time.Now().UTC(), + Type: VoteTypePrecommit, + BlockID: blockID, + } + vote.Signature = privKey.Sign(vote.SignBytes(chainID)) + commit := &Commit{ + BlockID: blockID, + Precommits: []*Vote{vote}, } - b.StartTimer() - for i := 0; i < b.N; i++ { - vset.Copy() + badChainID := "notmychainID" + badBlockID := BlockID{Hash: []byte("goodbye")} + badHeight := height + 1 + badCommit := &Commit{ + BlockID: blockID, + Precommits: []*Vote{nil}, } -} -func (valSet *ValidatorSet) toBytes() []byte { - buf, n, err := new(bytes.Buffer), new(int), new(error) - wire.WriteBinary(valSet, buf, n, err) - if *err != nil { - cmn.PanicCrisis(*err) + // test some error cases + // TODO: test more cases! + cases := []struct { + chainID string + blockID BlockID + height int64 + commit *Commit + }{ + {badChainID, blockID, height, commit}, + {chainID, badBlockID, height, commit}, + {chainID, blockID, badHeight, commit}, + {chainID, blockID, height, badCommit}, } - return buf.Bytes() -} -func (valSet *ValidatorSet) fromBytes(b []byte) { - r, n, err := bytes.NewReader(b), new(int), new(error) - wire.ReadBinary(valSet, r, 0, n, err) - if *err != nil { - // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - cmn.PanicCrisis(*err) + for i, c := range cases { + err := vset.VerifyCommit(c.chainID, c.blockID, c.height, c.commit) + assert.NotNil(t, err, i) } + + // test a good one + err := vset.VerifyCommit(chainID, blockID, height, commit) + assert.Nil(t, err) } diff --git a/types/vote.go b/types/vote.go index 7b069f2f6..ceb6e985e 100644 --- a/types/vote.go +++ b/types/vote.go @@ -4,11 +4,10 @@ import ( "bytes" "errors" "fmt" - "io" "time" - "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/wire" cmn "github.com/tendermint/tmlibs/common" ) @@ -73,11 +72,15 @@ type Vote struct { Signature crypto.Signature `json:"signature"` } -func (vote *Vote) WriteSignBytes(chainID string, w io.Writer, n *int, err *error) { - wire.WriteJSON(CanonicalJSONOnceVote{ +func (vote *Vote) SignBytes(chainID string) []byte { + bz, err := wire.MarshalJSON(CanonicalJSONOnceVote{ chainID, CanonicalVote(vote), - }, w, n, err) + }) + if err != nil { + panic(err) + } + return bz } func (vote *Vote) Copy() *Vote { @@ -111,7 +114,7 @@ func (vote *Vote) Verify(chainID string, pubKey crypto.PubKey) error { return ErrVoteInvalidValidatorAddress } - if !pubKey.VerifyBytes(SignBytes(chainID, vote), vote.Signature) { + if !pubKey.VerifyBytes(vote.SignBytes(chainID), vote.Signature) { return ErrVoteInvalidSignature } return nil diff --git a/types/vote_set.go b/types/vote_set.go index 2b5ac6316..37f26f4a5 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -8,10 +8,15 @@ import ( "github.com/pkg/errors" - "github.com/tendermint/tendermint/p2p" cmn "github.com/tendermint/tmlibs/common" ) +// UNSTABLE +// XXX: duplicate of p2p.ID to avoid dependence between packages. +// Perhaps we can have a minimal types package containing this (and other things?) +// that both `types` and `p2p` import ? +type P2PID string + /* VoteSet helps collect signatures from validators at each height+round for a predefined vote type. @@ -59,7 +64,7 @@ type VoteSet struct { sum int64 // Sum of voting power for seen votes, discounting conflicts maj23 *BlockID // First 2/3 majority seen votesByBlock map[string]*blockVotes // string(blockHash|blockParts) -> blockVotes - peerMaj23s map[p2p.ID]BlockID // Maj23 for each peer + peerMaj23s map[P2PID]BlockID // Maj23 for each peer } // Constructs a new VoteSet struct used to accumulate votes for given height/round. @@ -78,7 +83,7 @@ func NewVoteSet(chainID string, height int64, round int, type_ byte, valSet *Val sum: 0, maj23: nil, votesByBlock: make(map[string]*blockVotes, valSet.Size()), - peerMaj23s: make(map[p2p.ID]BlockID), + peerMaj23s: make(map[P2PID]BlockID), } } @@ -291,7 +296,7 @@ func (voteSet *VoteSet) addVerifiedVote(vote *Vote, blockKey string, votingPower // this can cause memory issues. // TODO: implement ability to remove peers too // NOTE: VoteSet must not be nil -func (voteSet *VoteSet) SetPeerMaj23(peerID p2p.ID, blockID BlockID) error { +func (voteSet *VoteSet) SetPeerMaj23(peerID P2PID, blockID BlockID) error { if voteSet == nil { cmn.PanicSanity("SetPeerMaj23() on nil VoteSet") } diff --git a/types/vote_test.go b/types/vote_test.go index 51eca12de..a4a0f309f 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - wire "github.com/tendermint/go-wire" + wire "github.com/tendermint/tendermint/wire" ) func examplePrevote() *Vote { @@ -18,7 +18,7 @@ func examplePrecommit() *Vote { } func exampleVote(t byte) *Vote { - var stamp, err = time.Parse(timeFormat, "2017-12-25T03:00:01.234Z") + var stamp, err = time.Parse(TimeFormat, "2017-12-25T03:00:01.234Z") if err != nil { panic(err) } @@ -42,7 +42,7 @@ func exampleVote(t byte) *Vote { func TestVoteSignable(t *testing.T) { vote := examplePrecommit() - signBytes := SignBytes("test_chain_id", vote) + signBytes := vote.SignBytes("test_chain_id") signStr := string(signBytes) expected := `{"chain_id":"test_chain_id","vote":{"block_id":{"hash":"68617368","parts":{"hash":"70617274735F68617368","total":1000000}},"height":12345,"round":2,"timestamp":"2017-12-25T03:00:01.234Z","type":2}}` @@ -77,24 +77,25 @@ func TestVoteVerifySignature(t *testing.T) { pubKey := privVal.GetPubKey() vote := examplePrecommit() - signBytes := SignBytes("test_chain_id", vote) + signBytes := vote.SignBytes("test_chain_id") // sign it signature, err := privVal.Signer.Sign(signBytes) require.NoError(t, err) // verify the same vote - valid := pubKey.VerifyBytes(SignBytes("test_chain_id", vote), signature) + valid := pubKey.VerifyBytes(vote.SignBytes("test_chain_id"), signature) require.True(t, valid) // serialize, deserialize and verify again.... precommit := new(Vote) - bs := wire.BinaryBytes(vote) - err = wire.ReadBinaryBytes(bs, &precommit) + bs, err := wire.MarshalBinary(vote) + require.NoError(t, err) + err = wire.UnmarshalBinary(bs, &precommit) require.NoError(t, err) // verify the transmitted vote - newSignBytes := SignBytes("test_chain_id", precommit) + newSignBytes := precommit.SignBytes("test_chain_id") require.Equal(t, string(signBytes), string(newSignBytes)) valid = pubKey.VerifyBytes(newSignBytes, signature) require.True(t, valid) diff --git a/version/version.go b/version/version.go index a45095428..2b81e95cc 100644 --- a/version/version.go +++ b/version/version.go @@ -1,13 +1,13 @@ package version const Maj = "0" -const Min = "16" +const Min = "17" const Fix = "0" var ( // Version is the current version of Tendermint // Must be a string because scripts like dist.sh read this file. - Version = "0.16.0" + Version = "0.17.0" // GitCommit is the current HEAD set using ldflags. GitCommit string diff --git a/wire/wire.go b/wire/wire.go new file mode 100644 index 000000000..9d0d2c208 --- /dev/null +++ b/wire/wire.go @@ -0,0 +1,60 @@ +package wire + +import ( + "github.com/tendermint/go-wire" +) + +/* +// Expose access to a global wire codec +// TODO: maybe introduce some Context object +// containing logger, config, codec that can +// be threaded through everything to avoid this global +var cdc *wire.Codec + +func init() { + cdc = wire.NewCodec() + crypto.RegisterWire(cdc) +} +*/ + +// Just a flow through to go-wire. +// To be used later for the global codec + +func MarshalBinary(o interface{}) ([]byte, error) { + return wire.MarshalBinary(o) +} + +func UnmarshalBinary(bz []byte, ptr interface{}) error { + return wire.UnmarshalBinary(bz, ptr) +} + +func MarshalJSON(o interface{}) ([]byte, error) { + return wire.MarshalJSON(o) +} + +func UnmarshalJSON(jsonBz []byte, ptr interface{}) error { + return wire.UnmarshalJSON(jsonBz, ptr) +} + +type ConcreteType = wire.ConcreteType + +func RegisterInterface(o interface{}, ctypes ...ConcreteType) *wire.TypeInfo { + return wire.RegisterInterface(o, ctypes...) +} + +const RFC3339Millis = wire.RFC3339Millis + +/* + +func RegisterInterface(ptr interface{}, opts *wire.InterfaceOptions) { + cdc.RegisterInterface(ptr, opts) +} + +func RegisterConcrete(o interface{}, name string, opts *wire.ConcreteOptions) { + cdc.RegisterConcrete(o, name, opts) +} + +//------------------------------- + +const RFC3339Millis = wire.RFC3339Millis +*/