From 774aff5f7d60725062aed57cdf5733a8ab9b5b11 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 14 Feb 2020 11:22:17 +0100 Subject: [PATCH 01/74] docs: update Light Client Protocol page (#4405) * docs: update Light Client Protocol page Closes #4331 * one way to get hash & height --- cmd/tendermint/commands/lite.go | 18 ++---- docs/tendermint-core/light-client-protocol.md | 57 ++++++++++++++----- lite2/doc.go | 15 +++-- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index f27c34cf1..8477b46ed 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -25,13 +25,12 @@ import ( // LiteCmd represents the base command when called without any subcommands var LiteCmd = &cobra.Command{ Use: "lite", - Short: "Run lite-client proxy server, verifying tendermint rpc", - Long: `This node will run a secure proxy to a tendermint rpc server. + Short: "Run a light client proxy server, verifying Tendermint rpc", + Long: `Run a light client proxy server, verifying Tendermint rpc. All calls that can be tracked back to a block header by a proof -will be verified before passing them back to the caller. Other that -that it will present the same interface as a full tendermint node, -just with added trust and running locally.`, +will be verified before passing them back to the caller. Other than +that, it will present the same interface as a full Tendermint node.`, RunE: runProxy, SilenceUsage: true, } @@ -52,27 +51,20 @@ var ( func init() { LiteCmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888", "Serve the proxy on the given address") - + LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") LiteCmd.Flags().StringVar(&primaryAddr, "primary", "tcp://localhost:26657", "Connect to a Tendermint node at this address") - LiteCmd.Flags().StringVar(&witnessesAddrs, "witnesses", "", "Tendermint nodes to cross-check the primary node, comma-separated") - - LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") - LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") LiteCmd.Flags().IntVar( &maxOpenConnections, "max-open-connections", 900, "Maximum number of simultaneous connections (including WebSocket).") - LiteCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, "Trusting period. Should be significantly less than the unbonding period") - LiteCmd.Flags().Int64Var(&trustedHeight, "trusted-height", 1, "Trusted header's height") - LiteCmd.Flags().BytesHexVar(&trustedHash, "trusted-hash", []byte{}, "Trusted header's hash") } diff --git a/docs/tendermint-core/light-client-protocol.md b/docs/tendermint-core/light-client-protocol.md index 1296fa471..4537412c1 100644 --- a/docs/tendermint-core/light-client-protocol.md +++ b/docs/tendermint-core/light-client-protocol.md @@ -4,20 +4,19 @@ order: 9 # Light Client Protocol -Light clients are an important part of the complete blockchain system -for most applications. Tendermint provides unique speed and security -properties for light client applications. +Light clients are an important part of the complete blockchain system for most +applications. Tendermint provides unique speed and security properties for +light client applications. See our [lite -package](https://godoc.org/github.com/tendermint/tendermint/lite). +package](https://pkg.go.dev/github.com/tendermint/tendermint/lite2?tab=doc). ## Overview -The objective of the light client protocol is to get a -commit for a recent block -hash where the commit includes a -majority of signatures from the last known validator set. From there, -all the application state is verifiable with [merkle +The objective of the light client protocol is to get a commit for a recent +block hash where the commit includes a majority of signatures from the last +known validator set. From there, all the application state is verifiable with +[merkle proofs](https://github.com/tendermint/spec/blob/953523c3cb99fdb8c8f7a2d21e3a99094279e9de/spec/blockchain/encoding.md#iavl-tree). ## Properties @@ -27,8 +26,38 @@ proofs](https://github.com/tendermint/spec/blob/953523c3cb99fdb8c8f7a2d21e3a9909 - You get the full speed benefits of Tendermint; transactions commit instantly. - You can get the most recent version of the application state - non-interactively (without committing anything to the blockchain). - For example, this means that you can get the most recent value of a - name from the name-registry without worrying about fork censorship - attacks, without posting a commit and waiting for confirmations. - It's fast, secure, and free! + non-interactively (without committing anything to the blockchain). For + example, this means that you can get the most recent value of a name from the + name-registry without worrying about fork censorship attacks, without posting + a commit and waiting for confirmations. It's fast, secure, and free! + +## Where to obtain trusted height & hash? + +https://pkg.go.dev/github.com/tendermint/tendermint/lite2?tab=doc#TrustOptions + +One way to obtain semi-trusted hash & height is to query multiple full nodes +and compare their hashes: + +```sh +$ curl -s https://233.123.0.140:26657:26657/commit | jq "{height: .result.signed_header.header.height, hash: .result.signed_header.commit.block_id.hash}" +{ + "height": "273", + "hash": "188F4F36CBCD2C91B57509BBF231C777E79B52EE3E0D90D06B1A25EB16E6E23D" +} +``` + +## HTTP proxy + +Tendermint comes with a built-in `tendermint lite` command, which can be used +to run a light client proxy server, verifying Tendermint rpc. All calls that +can be tracked back to a block header by a proof will be verified before +passing them back to the caller. Other than that, it will present the same +interface as a full Tendermint node. + +```sh +$ tendermint lite --chain-id=supernova --primary=tcp://233.123.0.140:26657 \ + --witnesses=tcp://179.63.29.15:26657,tcp://144.165.223.135:26657 \ + --trusted-height=10 --trusted-hash=37E9A6DD3FA25E83B22C18835401E8E56088D0D7ABC6FD99FCDC920DD76C1C57 +``` + +For additional options, run `tendermint lite --help`. diff --git a/lite2/doc.go b/lite2/doc.go index 920032f36..aa280e64f 100644 --- a/lite2/doc.go +++ b/lite2/doc.go @@ -76,24 +76,29 @@ Example usage: Hash: header.Hash(), }, httpp.New(chainID, "tcp://localhost:26657"), + []provider.Provider{httpp.New(chainID, "tcp://witness1:26657")}, dbs.New(db, chainID), ) - err = c.VerifyHeaderAtHeight(101, time.Now()) + err = c.Start() if err != nil { - fmt.Println("retry?") + // return err + t.Fatal(err) } + defer c.Stop() h, err := c.TrustedHeader(101) if err != nil { - fmt.Println("retry?") + // handle error } fmt.Println("got header", h) +Check out other examples in example_test.go + ## 2. Pure functions to verify a new header (see verifier.go) Verify function verifies a new header against some trusted header. See -https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md +https://github.com/tendermint/spec/blob/master/spec/consensus/light-client/verification.md for details. ## 3. Secure RPC proxy @@ -105,7 +110,7 @@ as a wrapper, which verifies all the headers, using a light client connected to some other node. See -https://github.com/tendermint/tendermint/blob/master/cmd/tendermint/commands/lite.go +https://docs.tendermint.com/master/tendermint-core/light-client-protocol.html for usage example. */ package lite From 3b5794ffe97fd94538eee9e3d013d6046eb72057 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 14 Feb 2020 11:35:14 +0100 Subject: [PATCH 02/74] adr: light client implementation (#4397) * adr: light client implementation Closes #2133 * note on chain IDs * explain why witnesses are required * if chain forks maliciously, chain ID stays the same * add a note about min witnesses while cross-checking --- docs/architecture/README.md | 3 + .../adr-046-light-client-implementation.md | 146 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 docs/architecture/adr-046-light-client-implementation.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 7055a3676..9b3c2f661 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -64,5 +64,8 @@ Note the context/background should be written in the present tense. - [ADR-039-Peer-Behaviour](./adr-039-peer-behaviour.md) - [ADR-041-Proposer-Selection-via-ABCI](./adr-041-proposer-selection-via-abci.md) - [ADR-043-Blockchain-RiRi-Org](./adr-043-blockchain-riri-org.md) +- [ADR-044-Lite-Client-With-Weak-Subjectivity](./adr-044-lite-client-with-weak-subjectivity.md) +- [ADR-045-ABCI-Evidence](./adr-045-abci-evidence.md) +- [ADR-046-Light-Client-Implementation](./adr-046-light-client-implementation.md) - [ADR-051-Double-Signing-Risk-Reduction](./adr-051-double-signing-risk-reduction.md) - [ADR-052-Tendermint-Mode](./adr-052-tendermint-mode.md) diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md new file mode 100644 index 000000000..11dc79fb9 --- /dev/null +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -0,0 +1,146 @@ +# ADR 046: Lite Client Implementation + +## Changelog +* 13-02-2020: Initial draft + +## Context + +A `Client` struct represents a light client, connected to a single blockchain. +As soon as it's started (via `Start`), it tries to update to the latest header +(using bisection algorithm by default). + +Cleaning routine is also started to remove headers outside of trusting period. +NOTE: since it's periodic, we still need to check header is not expired in +`TrustedHeader`, `TrustedValidatorSet` methods (and others which are using the +latest trusted header). + +The user has an option to manually verify headers using `VerifyHeader` and +`VerifyHeaderAtHeight` methods. To avoid races, `UpdatePeriod(0)` needs to be +passed when initializing the light client (it turns off the auto update). + +```go +type Client interface { + // start and stop updating & cleaning goroutines + Start() error + Stop() + Cleanup() error + + // get trusted headers & validators + TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) + TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error) + LastTrustedHeight() (int64, error) + FirstTrustedHeight() (int64, error) + + // query configuration options + ChainID() string + Primary() provider.Provider + Witnesses() []provider.Provider + + // verify new headers + VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) + VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error +} +``` + +A new light client can either be created from scratch (via `NewClient`) or +using the trusted store (via `NewClientFromTrustedStore`). When there's some +data in the trusted store and `NewClient` is called, the light client will a) +check if stored header is more recent b) optionally ask the user whenever it +should rollback (no confirmation required by default). + +```go +func NewClient( + chainID string, + trustOptions TrustOptions, + primary provider.Provider, + witnesses []provider.Provider, + trustedStore store.Store, + options ...Option) (*Client, error) { +``` + +`witnesses` as argument (as opposite to `Option`) is an intentional choice, +made to increase security by default. At least one witness is required, +although, right now, the light client does not check that primary != witness. +When cross-checking a new header with witnesses, minimum number of witnesses +required to respond: 1. + +Due to bisection algorithm nature, some headers might be skipped. If the light +client does not have a header for height `X` and `TrustedHeader(X)` or +`TrustedValidatorSet(X)` methods are called, it will download the header from +primary provider and perform a backwards verification. + +```go +type Provider interface { + ChainID() string + + SignedHeader(height int64) (*types.SignedHeader, error) + ValidatorSet(height int64) (*types.ValidatorSet, error) +} +``` + +Provider is a full node usually, but can be another light client. The above +interface is thin and can accommodate many implementations. + +If provider (primary or witness) becomes unavailable for a prolonged period of +time, it will be removed to ensure smooth operation. + +Both `Client` and providers expose chain ID to track if there are on the same +chain. Note, when chain upgrades or intentionally forks, chain ID changes. + +The light client stores headers & validators in the trusted store: + +```go +type Store interface { + SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error + DeleteSignedHeaderAndNextValidatorSet(height int64) error + + SignedHeader(height int64) (*types.SignedHeader, error) + ValidatorSet(height int64) (*types.ValidatorSet, error) + + LastSignedHeaderHeight() (int64, error) + FirstSignedHeaderHeight() (int64, error) + + SignedHeaderAfter(height int64) (*types.SignedHeader, error) +} +``` + +At the moment, the only implementation is the `db` store (wrapper around the KV +database, used in Tendermint). In the future, remote adapters are possible +(e.g. `Postgresql`). + +```go +func Verify( + chainID string, + h1 *types.SignedHeader, + h1NextVals *types.ValidatorSet, + h2 *types.SignedHeader, + h2Vals *types.ValidatorSet, + trustingPeriod time.Duration, + now time.Time, + trustLevel tmmath.Fraction) error { +``` + +`Verify` pure function is exposed for a header verification. It handles both +cases of adjacent and non-adjacent headers. In the former case, it compares the +hashes directly (2/3+ signed transition). Otherwise, it verifies 1/3+ +(`trustLevel`) of trusted validators are still present in new validators. + +## Status + +Accepted. + +## Consequences + +### Positive + +* single `Client` struct, which is easy to use +* flexible interfaces for header providers and trusted storage + +### Negative + +* `Verify` needs to be aligned with the current spec + +### Neutral + +* `Verify` function might be misused (called with non-adjacent headers in + incorrectly implemented sequential verification) From 5f5e0e1d027d42d8ed3516d520ddaa10830ffc9e Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 14 Feb 2020 14:41:18 +0100 Subject: [PATCH 03/74] makefile: place phony markers after targets (#4408) The .PHONY targets in the Makefile are usually placed far away from the actual targets, and thus aren't always updated. Placing the .PHONY targets right next to the targets they cover make them more visible and thus more likely to be updated when necessary. --- Makefile | 39 ++++++++++++++++++++++++++++----------- tests.mk | 19 +++++++++++++++++-- tools.mk | 13 +++++++++++-- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 610a0c6b5..5557a87ae 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ LD_FLAGS = -X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse BUILD_FLAGS = -mod=readonly -ldflags "$(LD_FLAGS)" all: check build test install +.PHONY: all # The below include contains the tools. include tools.mk @@ -17,24 +18,30 @@ include tests.mk build: CGO_ENABLED=0 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint/ +.PHONY: build build_c: CGO_ENABLED=1 go build $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" -o $(OUTPUT) ./cmd/tendermint/ +.PHONY: build_c build_race: CGO_ENABLED=1 go build -race $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint +.PHONY: build_race install: CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) ./cmd/tendermint +.PHONY: install install_c: CGO_ENABLED=1 go install $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" ./cmd/tendermint +.PHONY: install_c ############################################################################### ### Protobuf ### ############################################################################### proto-all: proto-gen proto-lint proto-check-breaking +.PHONY: proto-all proto-gen: ## If you get the following error, @@ -43,14 +50,15 @@ proto-gen: ## Note the $< here is substituted for the %.proto ## Note the $@ here is substituted for the %.pb.go @sh scripts/protocgen.sh +.PHONY: proto-gen proto-lint: @buf check lint --error-format=json +.PHONY: proto-lint proto-check-breaking: @buf check breaking --against-input '.git#branch=master' - -.PHONY: proto-all proto-gen proto-lint proto-check-breaking +.PHONY: proto-check-breaking ############################################################################### ### Build ABCI ### @@ -58,9 +66,11 @@ proto-check-breaking: build_abci: @go build -mod=readonly -i ./abci/cmd/... +.PHONY: build_abci install_abci: @go install -mod=readonly ./abci/cmd/... +.PHONY: install_abci ############################################################################### ### Distribution ### @@ -70,6 +80,7 @@ install_abci: # TODO add abci to these scripts dist: @BUILD_TAGS=$(BUILD_TAGS) sh -c "'$(CURDIR)/scripts/dist.sh'" +.PHONY: dist go-mod-cache: go.sum @echo "--> Download go modules to local cache" @@ -85,12 +96,14 @@ 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 +.PHONY: draw_deps 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 $(OUTPUT) ./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" +.PHONY: get_deps_bin_size ############################################################################### ### Libs ### @@ -104,11 +117,13 @@ gen_certs: clean_certs mv out/server.crt rpc/lib/server/test.crt mv out/server.key rpc/lib/server/test.key rm -rf out +.PHONY: gen_certs # deletes generated certificates clean_certs: rm -f rpc/lib/server/test.crt rm -f rpc/lib/server/test.key +.PHONY: clean_certs ############################################################################### ### Formatting, linting, and vetting ### @@ -116,10 +131,12 @@ clean_certs: fmt: @go fmt ./... +.PHONY: fmt lint: @echo "--> Running linter" @golangci-lint run +.PHONY: lint DESTINATION = ./index.html.md @@ -135,6 +152,7 @@ build-docs: cp -r .vuepress/dist/* ~/output/$${p}/ ; \ cp ~/output/$${p}/index.html ~/output ; \ done < versions ; +.PHONY: build-docs sync-docs: cd ~/output && \ @@ -152,6 +170,7 @@ build-docker: cp $(OUTPUT) DOCKER/tendermint docker build --label=tendermint --tag="tendermint/tendermint" DOCKER rm -rf DOCKER/tendermint +.PHONY: build-docker ############################################################################### ### Local testnet using docker ### @@ -160,9 +179,11 @@ build-docker: # Build linux binary on other platforms build-linux: tools GOOS=linux GOARCH=amd64 $(MAKE) build +.PHONY: build-linux build-docker-localnode: @cd networks/local && make +.PHONY: build-docker-localnode # Runs `make build_c` from within an Amazon Linux (v2)-based Docker build # container in order to build an Amazon Linux-compatible binary. Produces a @@ -170,15 +191,18 @@ build-docker-localnode: build_c-amazonlinux: $(MAKE) -C ./DOCKER build_amazonlinux_buildimage docker run --rm -it -v `pwd`:/tendermint tendermint/tendermint:build_c-amazonlinux +.PHONY: build_c-amazonlinux # Run a 4-node testnet locally localnet-start: localnet-stop build-docker-localnode @if ! [ -f build/node0/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/tendermint:Z tendermint/localnode testnet --config /etc/tendermint/config-template.toml --v 4 --o . --populate-persistent-peers --starting-ip-address 192.167.10.2; fi docker-compose up +.PHONY: localnet-start # Stop testnet localnet-stop: docker-compose down +.PHONY: localnet-stop # Build hooks for dredd, to skip or add information on some steps build-contract-tests-hooks: @@ -187,6 +211,7 @@ ifeq ($(OS),Windows_NT) else go build -mod=readonly $(BUILD_FLAGS) -o build/contract_tests ./cmd/contract_tests endif +.PHONY: build-contract-tests-hooks # Run a nodejs tool to test endpoints against a localnet # The command takes care of starting and stopping the network @@ -195,12 +220,4 @@ endif # The binaries should be built beforehand contract-tests: dredd - -# 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 build_abci dist install install_abci check_tools tools update_tools draw_deps \ - protoc_abci protoc_libs gen_certs clean_certs grpc_dbserver fmt build-linux localnet-start \ - localnet-stop build-docker build-docker-localnode protoc_grpc protoc_all \ - build_c install_c test_with_deadlock cleanup_after_test_with_deadlock lint build-contract-tests-hooks contract-tests \ - build_c-amazonlinux +.PHONY: contract-tests diff --git a/tests.mk b/tests.mk index 72bba47e4..d7e305312 100644 --- a/tests.mk +++ b/tests.mk @@ -8,25 +8,30 @@ BINDIR ?= $(GOPATH)/bin ## required to be run first by most tests build_docker_test_image: docker build -t tester -f ./test/docker/Dockerfile . +.PHONY: build_docker_test_image ### coverage, app, persistence, and libs tests test_cover: # run the go unit tests with coverage bash test/test_cover.sh +.PHONY: test_cover test_apps: # run the app tests using bash # requires `abci-cli` and `tendermint` binaries installed bash test/app/test.sh +.PHONY: test_apps test_abci_apps: bash abci/tests/test_app/test.sh +.PHONY: test_abci_apps test_abci_cli: # test the cli against the examples in the tutorial at: # ./docs/abci-cli.md # if test fails, update the docs ^ @ bash abci/tests/test_cli/test.sh +.PHONY: test_abci_cli test_persistence: # run the persistence tests using bash @@ -35,6 +40,7 @@ test_persistence: # TODO undockerize # bash test/persist/test_failure_indices.sh +.PHONY: test_persistence test_p2p: docker rm -f rsyslog || true @@ -45,6 +51,7 @@ test_p2p: # the `docker cp` takes a really long time; uncomment for debugging # # mkdir -p test/p2p/logs && docker cp rsyslog:/var/log test/p2p/logs +.PHONY: test_p2p test_p2p_ipv6: # IPv6 tests require Docker daemon with IPv6 enabled, e.g. in daemon.json: @@ -63,6 +70,7 @@ test_p2p_ipv6: # the `docker cp` takes a really long time; uncomment for debugging # # mkdir -p test/p2p/logs && docker cp rsyslog:/var/log test/p2p/logs +.PHONY: test_p2p_ipv6 test_integrations: make build_docker_test_image @@ -77,31 +85,38 @@ test_integrations: make test_p2p # Disabled by default since it requires Docker daemon with IPv6 enabled #make test_p2p_ipv6 +.PHONY: test_integrations test_release: @go test -tags release $(PACKAGES) +.PHONY: test_release test100: @for i in {1..100}; do make test; done +.PHONY: test100 vagrant_test: vagrant up vagrant ssh -c 'make test_integrations' +.PHONY: vagrant_test ### go tests test: @echo "--> Running go test" @go test -p 1 $(PACKAGES) +.PHONY: test test_race: @echo "--> Running go test --race" @go test -p 1 -v -race $(PACKAGES) +.PHONY: test_race # uses https://github.com/sasha-s/go-deadlock/ to detect potential deadlocks test_with_deadlock: make set_with_deadlock make test make cleanup_after_test_with_deadlock +.PHONY: test_with_deadlock set_with_deadlock: @echo "Get Goid" @@ -111,6 +126,7 @@ set_with_deadlock: find . -name "*.go" | grep -v "vendor/" | xargs -n 1 sed -i.bak 's/sync.RWMutex/deadlock.RWMutex/' find . -name "*.go" | grep -v "vendor/" | xargs -n 1 sed -i.bak 's/sync.Mutex/deadlock.Mutex/' find . -name "*.go" | grep -v "vendor/" | xargs -n 1 goimports -w +.PHONY: set_with_deadlock # cleanes up after you ran test_with_deadlock cleanup_after_test_with_deadlock: @@ -119,5 +135,4 @@ cleanup_after_test_with_deadlock: find . -name "*.go" | grep -v "vendor/" | xargs -n 1 goimports -w # cleans up the deps to not include the need libs go mod tidy - -.PHONY: test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test +.PHONY: cleanup_after_test_with_deadlock diff --git a/tools.mk b/tools.mk index 7761311a9..66c6f2978 100644 --- a/tools.mk +++ b/tools.mk @@ -51,25 +51,31 @@ PROTOBUF = $(TOOLS_DESTDIR)/protoc GOODMAN = $(TOOLS_DESTDIR)/goodman all: tools +.PHONY: all tools: certstrap protobuf goodman +.PHONY: tools check: check_tools +.PHONY: check check_tools: @# https://stackoverflow.com/a/25668869 @echo "Found tools: $(foreach tool,$(notdir $(GOTOOLS)),\ $(if $(shell which $(tool)),$(tool),$(error "No $(tool) in PATH")))" +.PHONY: check_tools certstrap: $(CERTSTRAP) $(CERTSTRAP): @echo "Get Certstrap" @go get github.com/square/certstrap@v1.2.0 +.PHONY: certstrap protobuf: $(PROTOBUF) $(PROTOBUF): @echo "Get GoGo Protobuf" @go get github.com/gogo/protobuf/protoc-gen-gogo@v1.3.1 +.PHONY: protobuf buf: protoc-gen-buf-check-breaking protoc-gen-buf-check-lint @echo "Installing buf..." @@ -77,6 +83,7 @@ buf: protoc-gen-buf-check-breaking protoc-gen-buf-check-lint "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-${UNAME_S}-${UNAME_M}" \ -o "${BIN}/buf" && \ chmod +x "${BIN}/buf" +.PHONY: buf protoc-gen-buf-check-breaking: @echo "Installing protoc-gen-buf-check-breaking..." @@ -91,17 +98,20 @@ protoc-gen-buf-check-lint: "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/protoc-gen-buf-check-lint-${UNAME_S}-${UNAME_M}" \ -o "${BIN}/protoc-gen-buf-check-lint" && \ chmod +x "${BIN}/protoc-gen-buf-check-lint" +.PHONY: protoc-gen-buf-check-lint goodman: $(GOODMAN) $(GOODMAN): @echo "Get Goodman" @go get github.com/snikch/goodman/cmd/goodman@10e37e294daa3c9a90abded60ff9924bafab3888 +.PHONY: goodman tools-clean: rm -f $(CERTSTRAP) $(PROTOBUF) $(GOX) $(GOODMAN) rm -f tools-stamp rm -rf /usr/local/include/google/protobuf rm -f /usr/local/bin/protoc +.PHONY: tooks-clean ### # Non Go tools @@ -127,5 +137,4 @@ protoc: @unzip -o $(PROTOC_ZIP) -d /usr/local bin/protoc @unzip -o $(PROTOC_ZIP) -d /usr/local 'include/*' @rm -f $(PROTOC_ZIP) - -.PHONY: all tools tools-clean protoc +.PHONY: protoc From 58620af5e06af709dfce279dc67a4d9b397709de Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Fri, 14 Feb 2020 14:52:50 +0100 Subject: [PATCH 04/74] lite2: modified sequence method to match bisection (#4403) Currently the sequence function always starts from the trustedHeader and trustedNextVals stored in the lite client. Whereas the bisection one allows the method to be started from any combination of header and validator set. I opened up the sequence verification method to do the same --- lite2/client.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 668a46203..ff43581ef 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -617,7 +617,7 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali var err error switch c.verificationMode { case sequential: - err = c.sequence(newHeader, newVals, now) + err = c.sequence(c.trustedHeader, c.trustedNextVals, newHeader, newVals, now) case skipping: err = c.bisection(c.trustedHeader, c.trustedNextVals, newHeader, newVals, now) default: @@ -697,25 +697,30 @@ func (c *Client) cleanup(stopHeight int64) error { } // see VerifyHeader -func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { +func (c *Client) sequence( + trustedHeader *types.SignedHeader, + trustedNextVals *types.ValidatorSet, + newHeader *types.SignedHeader, + newVals *types.ValidatorSet, + now time.Time) error { // 1) Verify any intermediate headers. var ( - interimHeader *types.SignedHeader - nextVals *types.ValidatorSet - err error + interimHeader *types.SignedHeader + interimNextVals *types.ValidatorSet + err error ) - for height := c.trustedHeader.Height + 1; height < newHeader.Height; height++ { + for height := trustedHeader.Height + 1; height < newHeader.Height; height++ { interimHeader, err = c.signedHeaderFromPrimary(height) if err != nil { return errors.Wrapf(err, "failed to obtain the header #%d", height) } - c.logger.Debug("Verify newHeader against lastHeader", + c.logger.Debug("Verify newHeader against trustedHeader", "lastHeight", c.trustedHeader.Height, "lastHash", c.trustedHeader.Hash(), "newHeight", interimHeader.Height, "newHash", interimHeader.Hash()) - err = Verify(c.chainID, c.trustedHeader, c.trustedNextVals, interimHeader, c.trustedNextVals, + err = Verify(c.chainID, trustedHeader, trustedNextVals, interimHeader, trustedNextVals, c.trustingPeriod, now, c.trustLevel) if err != nil { return errors.Wrapf(err, "failed to verify the header #%d", height) @@ -723,17 +728,18 @@ func (c *Client) sequence(newHeader *types.SignedHeader, newVals *types.Validato // Update trusted header and vals. if height == newHeader.Height-1 { - nextVals = newVals + interimNextVals = newVals } else { - nextVals, err = c.validatorSetFromPrimary(height + 1) + interimNextVals, err = c.validatorSetFromPrimary(height + 1) if err != nil { return errors.Wrapf(err, "failed to obtain the vals #%d", height+1) } } - err = c.updateTrustedHeaderAndVals(interimHeader, nextVals) + err = c.updateTrustedHeaderAndVals(interimHeader, interimNextVals) if err != nil { return errors.Wrapf(err, "failed to update trusted state #%d", height) } + trustedHeader, trustedNextVals = interimHeader, interimNextVals } // 2) Verify the new header. From c56fd04ab44e42b9500ed181b483584bf0d5078f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 14 Feb 2020 17:04:56 +0100 Subject: [PATCH 05/74] lite2: disconnect from bad nodes (#4388) Closes #4385 * extract TrustOptions into its own file * print trusted hash before asking whenever to rollback or not so the user could reset the light client with the trusted header * do not return an error if rollback is aborted reason: we trust the old header presumably, so can continue from it. * add note about time of initial header * improve logging and add comments * cross-check newHeader after LC verified it * check if header is not nil so we don't crash on the next line * remove witness if it sends us incorrect header * require at least one witness * fix build and tests * rename tests and assert for specific error * wrote a test * fix linter errors * only check 1/3 if headers diverge --- lite2/client.go | 174 ++++++++++++++++++------------------ lite2/client_test.go | 81 +++++++++++------ lite2/provider/http/http.go | 5 ++ lite2/trust_options.go | 53 +++++++++++ 4 files changed, 201 insertions(+), 112 deletions(-) create mode 100644 lite2/trust_options.go diff --git a/lite2/client.go b/lite2/client.go index ff43581ef..717623cbb 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" - "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" "github.com/tendermint/tendermint/lite2/provider" @@ -17,50 +16,6 @@ import ( "github.com/tendermint/tendermint/types" ) -// TrustOptions are the trust parameters needed when a new light client -// connects to the network or when an existing light client that has been -// offline for longer than the trusting period connects to the network. -// -// The expectation is the user will get this information from a trusted source -// like a validator, a friend, or a secure website. A more user friendly -// solution with trust tradeoffs is that we establish an https based protocol -// with a default end point that populates this information. Also an on-chain -// registry of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the -// future. -type TrustOptions struct { - // tp: trusting period. - // - // Should be significantly less than the unbonding period (e.g. unbonding - // period = 3 weeks, trusting period = 2 weeks). - // - // More specifically, trusting period + time needed to check headers + time - // needed to report and punish misbehavior should be less than the unbonding - // period. - Period time.Duration - - // Header's Height and Hash must both be provided to force the trusting of a - // particular header. - Height int64 - Hash []byte -} - -// ValidateBasic performs basic validation. -func (opts TrustOptions) ValidateBasic() error { - if opts.Period <= 0 { - return errors.New("negative or zero period") - } - if opts.Height <= 0 { - return errors.New("negative or zero height") - } - if len(opts.Hash) != tmhash.Size { - return errors.Errorf("expected hash size to be %d bytes, got %d bytes", - tmhash.Size, - len(opts.Hash), - ) - } - return nil -} - type mode byte const ( @@ -215,7 +170,7 @@ func NewClient( } } - if c.trustedHeader == nil || c.trustedHeader.Height != trustOptions.Height { + if c.trustedHeader == nil || c.trustedHeader.Height < trustOptions.Height { if err := c.initializeWithTrustOptions(trustOptions); err != nil { return nil, err } @@ -339,20 +294,21 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { case options.Height < c.trustedHeader.Height: c.logger.Info("Client initialized with old header (trusted is more recent)", "old", options.Height, - "trusted", c.trustedHeader.Height) + "trusted", c.trustedHeader.Height, + "trusted-hash", hash2str(c.trustedHeader.Hash())) action := fmt.Sprintf( "Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)", options.Height, options.Hash, c.trustedHeader.Height, c.trustedHeader.Hash()) if c.confirmationFn(action) { - // remove all the headers ( options.Height, trustedHeader.Height ] + // remove all the headers (options.Height, trustedHeader.Height] c.cleanup(options.Height + 1) c.logger.Info("Rolled back to older header (newer headers were removed)", "old", options.Height) } else { - return errors.New("rollback aborted") + return nil } primaryHash = options.Hash @@ -386,7 +342,9 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { return err } - // NOTE: Verify func will check if it's expired or not. + // NOTE: - Verify func will check if it's expired or not. + // - h.Time is not being checked against time.Now() because we don't + // want to add yet another argument to NewClient* functions. if err := h.ValidateBasic(c.chainID); err != nil { return err } @@ -400,12 +358,14 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { if err != nil { return err } + if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { return errors.Errorf("expected header's validators (%X) to match those that were supplied (%X)", h.ValidatorsHash, vals.Hash(), ) } + // Ensure that +2/3 of validators signed correctly. err = vals.VerifyCommit(c.chainID, h.Commit.BlockID, h.Height, h.Commit) if err != nil { @@ -570,8 +530,6 @@ func (c *Client) ChainID() string { // If the header is not found by the primary provider, // provider.ErrSignedHeaderNotFound error is returned. func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { - c.logger.Info("VerifyHeaderAtHeight", "height", height) - if c.trustedHeader.Height >= height { return nil, errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) } @@ -603,17 +561,13 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // provider, provider.ErrSignedHeaderNotFound / // provider.ErrValidatorSetNotFound error is returned. func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { - c.logger.Info("VerifyHeader", "height", newHeader.Hash(), "newVals", fmt.Sprintf("%X", newVals.Hash())) + c.logger.Info("VerifyHeader", "height", newHeader.Height, "hash", hash2str(newHeader.Hash()), + "vals", hash2str(newVals.Hash())) if c.trustedHeader.Height >= newHeader.Height { return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) } - if err := c.compareNewHeaderWithWitnesses(newHeader); err != nil { - c.logger.Error("Error when comparing new header with one from a witness", "err", err) - return err - } - var err error switch c.verificationMode { case sequential: @@ -624,6 +578,12 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) } if err != nil { + c.logger.Error("Can't verify", "err", err) + return err + } + + if err := c.compareNewHeaderWithWitnesses(newHeader); err != nil { + c.logger.Error("Error when comparing new header with witnesses", "err", err) return err } @@ -660,7 +620,9 @@ func (c *Client) Cleanup() error { return c.cleanup(0) } -// cleanup deletes all headers & validator sets between +stopHeight+ and latest height included +// cleanup deletes all headers & validator sets between +stopHeight+ and latest +// height included. It also sets trustedHeader (vals) to the latest header +// (vals) if such exists. func (c *Client) cleanup(stopHeight int64) error { // 1) Get the oldest height. oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight() @@ -826,7 +788,8 @@ func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, nextVals *typ return nil } -// fetch header and validators for the given height from primary provider. +// fetch header and validators for the given height (0 - latest) from primary +// provider. func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, *types.ValidatorSet, error) { h, err := c.signedHeaderFromPrimary(height) if err != nil { @@ -916,49 +879,75 @@ func (c *Client) backwards(toHeight int64, fromHeader *types.SignedHeader, now t func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { c.providerMutex.Lock() defer c.providerMutex.Unlock() - // 0. Check witnesses exist - if len(c.witnesses) == 0 { - return errors.New("could not find any witnesses") - } - - matchedHeader := false + // 1. Make sure AT LEAST ONE witness returns the same header. + headerMatched := false + witnessesToRemove := make([]int, 0) for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { - // 1. Loop through all witnesses. - for _, witness := range c.witnesses { + if len(c.witnesses) == 0 { + return errors.New("could not find any witnesses. please reset the light client") + } - // 2. Fetch the header. + for i, witness := range c.witnesses { altH, err := witness.SignedHeader(h.Height) if err != nil { - c.logger.Info("No Response from witness ", "witness", witness) + c.logger.Error("Failed to get a header from witness", "height", h.Height, "witness", witness) + continue + } + + if err = altH.ValidateBasic(c.chainID); err != nil { + c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) + witnessesToRemove = append(witnessesToRemove, i) continue } - // 3. Compare hashes. if !bytes.Equal(h.Hash(), altH.Hash()) { - // TODO: One of the providers is lying. Send the evidence to fork - // accountability server. + if err = c.trustedNextVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, + altH.Height, altH.Commit, c.trustLevel); err != nil { + c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) + witnessesToRemove = append(witnessesToRemove, i) + continue + } + + // TODO: send the diverged headers to primary && all witnesses + return errors.Errorf( "header hash %X does not match one %X from the witness %v", h.Hash(), altH.Hash(), witness) } - matchedHeader = true - + headerMatched = true } - // 4. Check that one responding witness has returned a matching header - if matchedHeader { + for _, idx := range witnessesToRemove { + c.removeWitness(idx) + } + witnessesToRemove = make([]int, 0) + + if headerMatched { return nil } + // 2. Otherwise, sleep time.Sleep(backoffTimeout(attempt)) - } return errors.New("awaiting response from all witnesses exceeded dropout time") } +// NOTE: requires a providerMutex locked. +func (c *Client) removeWitness(idx int) { + switch len(c.witnesses) { + case 0: + panic(fmt.Sprintf("wanted to remove %d element from empty witnesses slice", idx)) + case 1: + c.witnesses = make([]provider.Provider, 0) + default: + c.witnesses[idx] = c.witnesses[len(c.witnesses)-1] + c.witnesses = c.witnesses[:len(c.witnesses)-1] + } +} + func (c *Client) removeNoLongerTrustedHeadersRoutine() { defer c.routinesWaitGroup.Done() @@ -1001,6 +990,10 @@ func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { } // 3) Remove all headers that are outside of the trusting period. + // + // NOTE: even the latest header can be removed. it's okay because + // c.trustedHeader will retain it in memory so other funcs like VerifyHeader + // don't crash. for height := oldestHeight; height <= latestHeight; height++ { h, err := c.trustedStore.SignedHeader(height) if err != nil { @@ -1067,27 +1060,31 @@ func (c *Client) Update(now time.Time) error { return err } - c.logger.Info("Advanced to new state", "height", latestHeader.Height, "hash", latestHeader.Hash()) + c.logger.Info("Advanced to new state", "height", latestHeader.Height, "hash", hash2str(latestHeader.Hash())) } return nil } -// replaceProvider takes the first alternative provider and promotes it as the primary provider +// replaceProvider takes the first alternative provider and promotes it as the +// primary provider. func (c *Client) replacePrimaryProvider() error { c.providerMutex.Lock() defer c.providerMutex.Unlock() - if len(c.witnesses) == 0 { - return errors.Errorf("no witnesses left") + + if len(c.witnesses) <= 1 { + return errors.Errorf("only one witness left. please reset the light client") } c.primary = c.witnesses[0] c.witnesses = c.witnesses[1:] c.logger.Info("New primary", "p", c.primary) + return nil } -// signedHeaderFromPrimary retrieves the SignedHeader from the primary provider at the specified height. -// Handles dropout by the primary provider by swapping with an alternative provider +// signedHeaderFromPrimary retrieves the SignedHeader from the primary provider +// at the specified height. Handles dropout by the primary provider by swapping +// with an alternative provider. func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, error) { for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { c.providerMutex.Lock() @@ -1115,8 +1112,9 @@ func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, err return c.signedHeaderFromPrimary(height) } -// validatorSetFromPrimary retrieves the ValidatorSet from the primary provider at the specified height. -// Handles dropout by the primary provider after 5 attempts by replacing it with an alternative provider +// validatorSetFromPrimary retrieves the ValidatorSet from the primary provider +// at the specified height. Handles dropout by the primary provider after 5 +// attempts by replacing it with an alternative provider. func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, error) { for attempt := uint16(1); attempt <= c.maxRetryAttempts; attempt++ { c.providerMutex.Lock() @@ -1142,3 +1140,7 @@ func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, err func backoffTimeout(attempt uint16) time.Duration { return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond } + +func hash2str(hash []byte) string { + return fmt.Sprintf("%X", hash) +} diff --git a/lite2/client_test.go b/lite2/client_test.go index b4823eaf9..e2931f323 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -55,7 +55,6 @@ var ( ) func TestClient_SequentialVerification(t *testing.T) { - testCases := []struct { otherHeaders map[int64]*types.SignedHeader // all except ^ vals map[int64]*types.ValidatorSet @@ -176,7 +175,6 @@ func TestClient_SequentialVerification(t *testing.T) { } func TestClient_SkippingVerification(t *testing.T) { - // required for 2nd test case newKeys := genPrivKeys(4) newVals := newKeys.ToValidators(10, 1) @@ -249,7 +247,6 @@ func TestClient_SkippingVerification(t *testing.T) { } func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { - c, err := NewClient( chainID, trustOptions, @@ -291,7 +288,6 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { } func TestClient_Cleanup(t *testing.T) { - c, err := NewClient( chainID, trustOptions, @@ -314,8 +310,7 @@ func TestClient_Cleanup(t *testing.T) { } // trustedHeader.Height == options.Height -func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { - +func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) @@ -388,8 +383,7 @@ func TestClientRestoreTrustedHeaderAfterStartup1(t *testing.T) { } // trustedHeader.Height < options.Height -func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { - +func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) @@ -472,8 +466,7 @@ func TestClientRestoreTrustedHeaderAfterStartup2(t *testing.T) { } // trustedHeader.Height > options.Height -func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { - +func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) @@ -581,7 +574,6 @@ func TestClientRestoreTrustedHeaderAfterStartup3(t *testing.T) { } func TestClient_Update(t *testing.T) { - c, err := NewClient( chainID, trustOptions, @@ -606,7 +598,6 @@ func TestClient_Update(t *testing.T) { } func TestClient_Concurrency(t *testing.T) { - c, err := NewClient( chainID, trustOptions, @@ -654,8 +645,7 @@ func TestClient_Concurrency(t *testing.T) { wg.Wait() } -func TestProvider_Replacement(t *testing.T) { - +func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { c, err := NewClient( chainID, trustOptions, @@ -666,6 +656,7 @@ func TestProvider_Replacement(t *testing.T) { Logger(log.TestingLogger()), MaxRetryAttempts(1), ) + require.NoError(t, err) err = c.Update(bTime.Add(2 * time.Hour)) require.NoError(t, err) @@ -674,8 +665,7 @@ func TestProvider_Replacement(t *testing.T) { assert.Equal(t, 1, len(c.Witnesses())) } -func TestProvider_TrustedHeaderFetchesMissingHeader(t *testing.T) { - +func TestClient_TrustedHeaderFetchesMissingHeader(t *testing.T) { c, err := NewClient( chainID, TrustOptions{ @@ -707,31 +697,29 @@ func TestProvider_TrustedHeaderFetchesMissingHeader(t *testing.T) { assert.Nil(t, h) } -func Test_NewClientFromTrustedStore(t *testing.T) { - +func TestClient_NewClientFromTrustedStore(t *testing.T) { // 1) Initiate DB and fill with a "trusted" header db := dbs.New(dbm.NewMemDB(), chainID) err := db.SaveSignedHeaderAndNextValidatorSet(h1, vals) require.NoError(t, err) - // 2) Initialize Lite Client from Trusted Store c, err := NewClientFromTrustedStore( chainID, trustPeriod, - fullNode, - []provider.Provider{fullNode}, + deadNode, + []provider.Provider{deadNode}, db, ) require.NoError(t, err) - // 3) Check header exists through the lite clients eyes + // 2) Check header exists (deadNode is being used to ensure we're not getting + // it from primary) h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) assert.NoError(t, err) assert.EqualValues(t, 1, h.Height) } -func TestCompareWithWitnesses(t *testing.T) { - +func TestClientUpdateErrorsIfAllWitnessesUnavailable(t *testing.T) { c, err := NewClient( chainID, trustOptions, @@ -743,7 +731,48 @@ func TestCompareWithWitnesses(t *testing.T) { MaxRetryAttempts(1), ) require.NoError(t, err) - err = c.Update(time.Now()) - assert.Error(t, err) + err = c.Update(bTime.Add(2 * time.Hour)) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "awaiting response from all witnesses exceeded dropout time") + } +} + +func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { + // straight invalid header + badProvider1 := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 3: {Header: nil, Commit: nil}, + }, + map[int64]*types.ValidatorSet{}, + ) + + // less than 1/3 signed + badProvider2 := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 3: keys.GenSignedHeaderLastBlockID(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash2"), []byte("cons_hash"), []byte("results_hash"), + len(keys), len(keys), types.BlockID{Hash: h2.Hash()}), + }, + map[int64]*types.ValidatorSet{}, + ) + + c, err := NewClient( + chainID, + trustOptions, + fullNode, + []provider.Provider{badProvider1, badProvider2}, + dbs.New(dbm.NewMemDB(), chainID), + UpdatePeriod(0), + Logger(log.TestingLogger()), + ) + require.NoError(t, err) + + err = c.Update(bTime.Add(2 * time.Hour)) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "could not find any witnesses") + } + assert.Zero(t, 0, len(c.Witnesses())) } diff --git a/lite2/provider/http/http.go b/lite2/provider/http/http.go index bbe6e92ae..130bf0a24 100644 --- a/lite2/provider/http/http.go +++ b/lite2/provider/http/http.go @@ -1,6 +1,7 @@ package http import ( + "errors" "fmt" "strings" @@ -68,6 +69,10 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) { return nil, err } + if commit.Header == nil { + return nil, errors.New("header is nil") + } + // Verify we're still on the same chain. if p.chainID != commit.Header.ChainID { return nil, fmt.Errorf("expected chainID %s, got %s", p.chainID, commit.Header.ChainID) diff --git a/lite2/trust_options.go b/lite2/trust_options.go new file mode 100644 index 000000000..7bd36fe5c --- /dev/null +++ b/lite2/trust_options.go @@ -0,0 +1,53 @@ +package lite + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/tendermint/tendermint/crypto/tmhash" +) + +// TrustOptions are the trust parameters needed when a new light client +// connects to the network or when an existing light client that has been +// offline for longer than the trusting period connects to the network. +// +// The expectation is the user will get this information from a trusted source +// like a validator, a friend, or a secure website. A more user friendly +// solution with trust tradeoffs is that we establish an https based protocol +// with a default end point that populates this information. Also an on-chain +// registry of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the +// future. +type TrustOptions struct { + // tp: trusting period. + // + // Should be significantly less than the unbonding period (e.g. unbonding + // period = 3 weeks, trusting period = 2 weeks). + // + // More specifically, trusting period + time needed to check headers + time + // needed to report and punish misbehavior should be less than the unbonding + // period. + Period time.Duration + + // Header's Height and Hash must both be provided to force the trusting of a + // particular header. + Height int64 + Hash []byte +} + +// ValidateBasic performs basic validation. +func (opts TrustOptions) ValidateBasic() error { + if opts.Period <= 0 { + return errors.New("negative or zero period") + } + if opts.Height <= 0 { + return errors.New("negative or zero height") + } + if len(opts.Hash) != tmhash.Size { + return errors.Errorf("expected hash size to be %d bytes, got %d bytes", + tmhash.Size, + len(opts.Hash), + ) + } + return nil +} From da8813071ba3cb9ecd3a78e5ff2e6ecd1468d4c7 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Sun, 16 Feb 2020 01:07:48 +0500 Subject: [PATCH 06/74] Docs staging (#4407) * update theme * Update version * Updated Questions section in the footer * Remove links to Riot chat * Typo * Add Discord link Co-authored-by: Marko --- docs/.vuepress/config.js | 16 +---- .../adr-051-double-signing-risk-reduction.md | 2 +- docs/architecture/adr-052-tendermint-mode.md | 2 +- docs/package-lock.json | 62 +++++++++---------- docs/package.json | 11 ++-- 5 files changed, 35 insertions(+), 58 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index dc7a200ce..55613252f 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -19,12 +19,6 @@ module.exports = { gutter: { title: "Help & Support", editLink: true, - chat: { - title: "Riot Chat", - text: "Chat with Tendermint developers on Riot Chat.", - url: "https://riot.im/app/#/room/#tendermint:matrix.org", - bg: "#222" - }, forum: { title: "Tendermint Forum", text: "Join the Tendermint forum to learn more", @@ -38,6 +32,7 @@ module.exports = { } }, footer: { + questionsText: "Chat with Cosmos developers in [Discord](https://discordapp.com/channels/669268347736686612) or reach out on the [SDK Developer Forum](https://forum.cosmos.network/c/tendermint) to learn more.", logo: "/logo-bw.svg", textLink: { text: "tendermint.com", @@ -95,10 +90,6 @@ module.exports = { { title: "Forum", url: "https://forum.cosmos.network/c/tendermint" - }, - { - title: "Chat", - url: "https://riot.im/app/#/room/#tendermint:matrix.org" } ] }, @@ -138,11 +129,6 @@ module.exports = { } ] }, - markdown: { - anchor: { - permalinkSymbol: "" - } - }, plugins: [ [ "@vuepress/google-analytics", diff --git a/docs/architecture/adr-051-double-signing-risk-reduction.md b/docs/architecture/adr-051-double-signing-risk-reduction.md index 04ee41a67..ae663e8b5 100644 --- a/docs/architecture/adr-051-double-signing-risk-reduction.md +++ b/docs/architecture/adr-051-double-signing-risk-reduction.md @@ -27,7 +27,7 @@ We would like to suggest a double signing risk reduction method. 2. If there exists votes from the validator's consensus key, exit state machine program - Configuration - We would like to suggest by introducing `double_sign_check_height` parameter in `config.toml` and cli, how many blocks state machine looks back to check votes - - `double_sign_check_height = {{ .Consensus.DoubleSignCheckHeight }}` in `config.toml` + - `double_sign_check_height = {{ .Consensus.DoubleSignCheckHeight }}` in `config.toml` - `tendermint node --double_sign_check_height` in cli - State machine ignore checking procedure when `vote-check-height == 0` diff --git a/docs/architecture/adr-052-tendermint-mode.md b/docs/architecture/adr-052-tendermint-mode.md index edf217a06..acd5028b4 100644 --- a/docs/architecture/adr-052-tendermint-mode.md +++ b/docs/architecture/adr-052-tendermint-mode.md @@ -42,7 +42,7 @@ We would like to suggest a simple Tendermint mode abstraction. These modes will - p2p/pex - Configuration, cli command - We would like to suggest by introducing `mode` parameter in `config.toml` and cli - - `mode = "{{ .BaseConfig.Mode }}"` in `config.toml` + - `mode = "{{ .BaseConfig.Mode }}"` in `config.toml` - `tendermint node --mode validator` in cli - fullnode | validator | seed (default: "fullnode") - RPC modification diff --git a/docs/package-lock.json b/docs/package-lock.json index e5e653713..a5e8c0880 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1157,12 +1157,6 @@ "lodash.debounce": "^4.0.8" } }, - "@vuepress/plugin-google-analytics": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.2.0.tgz", - "integrity": "sha512-0zol5D4Efb5GKel7ADO/s65MLtKSLnOEGkeWzuipkWomSQPzP7TJ3+/RcYBnGdyBFHd1BSpTUHGK0b/IGwM3UA==", - "dev": true - }, "@vuepress/plugin-last-updated": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@vuepress/plugin-last-updated/-/plugin-last-updated-1.3.0.tgz", @@ -2378,9 +2372,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001023", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz", - "integrity": "sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA==" + "version": "1.0.30001027", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz", + "integrity": "sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg==" }, "caseless": { "version": "0.12.0", @@ -3571,9 +3565,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.344.tgz", - "integrity": "sha512-tvbx2Wl8WBR+ym3u492D0L6/jH+8NoQXqe46+QhbWH3voVPauGuZYeb1QAXYoOAWuiP2dbSvlBx0kQ1F3hu/Mw==" + "version": "1.3.346", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.346.tgz", + "integrity": "sha512-Yy4jF5hJd57BWmGPt0KjaXc25AmWZeQK75kdr4zIzksWVtiT6DwaNtvTb9dt+LkQKwUpvBfCyyPsXXtbY/5GYw==" }, "elliptic": { "version": "6.5.2", @@ -6086,9 +6080,9 @@ "integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==" }, "markdown-it-attrs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-3.0.1.tgz", - "integrity": "sha512-fcpdmxdEsctDVJEunPyrirVtU/6zcTMxPxAu4Ofz51PKAa8vRMpmGQXsmXx1HTdIdUPoDonm/RhS/+jTNywj/Q==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-3.0.2.tgz", + "integrity": "sha512-q45vdXU9TSWaHgFkWEFM97YHEoCmOyG9csLLdv3oVC6ARjT77u4wfng9rRtSOMb5UpxzT7zTX5GBbwm15H40dw==" }, "markdown-it-chain": { "version": "1.3.0", @@ -6559,9 +6553,9 @@ } }, "node-releases": { - "version": "1.1.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.47.tgz", - "integrity": "sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA==", + "version": "1.1.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.48.tgz", + "integrity": "sha512-Hr8BbmUl1ujAST0K0snItzEA5zkJTQup8VNTKNfT6Zw8vTJkIiagUPNfxHmgDOyfFYNfKAul40sD0UEYTvwebw==", "requires": { "semver": "^6.3.0" } @@ -7630,11 +7624,11 @@ } }, "postcss-safe-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz", - "integrity": "sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", + "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", "requires": { - "postcss": "^7.0.0" + "postcss": "^7.0.26" } }, "postcss-selector-parser": { @@ -8276,9 +8270,9 @@ "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=" }, "resolve": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", - "integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", "requires": { "path-parse": "^1.0.6" } @@ -10096,9 +10090,9 @@ } }, "vuepress-theme-cosmos": { - "version": "1.0.148", - "resolved": "https://registry.npmjs.org/vuepress-theme-cosmos/-/vuepress-theme-cosmos-1.0.148.tgz", - "integrity": "sha512-IVzX339e9k25YNC62UZPfkLg50IGCctvlI/TQF6s63eCdh+ayHOPLTFq5zAiYTpP09Dbz4tSC74cQyO3fUyPOQ==", + "version": "1.0.150", + "resolved": "https://registry.npmjs.org/vuepress-theme-cosmos/-/vuepress-theme-cosmos-1.0.150.tgz", + "integrity": "sha512-f4McVndkB+CqJ6mWpOG4UZSR14LJyXqwcgwoDoDUx149g2PKU3qI/AF5AcrM25+4UKMCXFKcJloQCl/aWq+1ig==", "requires": { "@cosmos-ui/vue": "^0.5.20", "axios": "^0.19.0", @@ -10181,9 +10175,9 @@ } }, "webpack-chain": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.3.1.tgz", - "integrity": "sha512-PLWPY92p5Vj0hOylUGjRoRY7Kgrns5vmPFAQ9BpSHnBbVbh2akRQVUlbRb2mmGYRSY1FklTULtyVChNmcQjIVg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.4.0.tgz", + "integrity": "sha512-f97PYqxU+9/u0IUqp/ekAHRhBD1IQwhBv3wlJo2nvyELpr2vNnUqO3XQEk+qneg0uWGP54iciotszpjfnEExFA==", "requires": { "deepmerge": "^1.5.2", "javascript-stringify": "^2.0.1" @@ -10209,9 +10203,9 @@ } }, "webpack-dev-server": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.2.tgz", - "integrity": "sha512-pxZKPYb+n77UN8u9YxXT4IaIrGcNtijh/mi8TXbErHmczw0DtPnMTTjHj+eNjkqLOaAZM/qD7V59j/qJsEiaZA==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", + "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", "requires": { "ansi-html": "0.0.7", "bonjour": "^3.5.0", diff --git a/docs/package.json b/docs/package.json index 962531efd..8ce869057 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,19 +4,16 @@ "description": "Welcome to the Tendermint Core documentation!", "main": "index.js", "dependencies": { - "vuepress-theme-cosmos": "^1.0.148" - }, - "devDependencies": { - "@vuepress/plugin-google-analytics": "^1.2.0" + "vuepress-theme-cosmos": "^1.0.150" }, "scripts": { "preserve": "./pre.sh", - "serve": "trap 'exit 0' SIGINT; vuepress dev", + "serve": "trap 'exit 0' SIGINT; vuepress dev --no-cache", "postserve": "./post.sh", "prebuild": "./pre.sh", - "build": "trap 'exit 0' SIGINT; vuepress build", + "build": "trap 'exit 0' SIGINT; vuepress build --no-cache", "postbuild": "./post.sh" }, "author": "", "license": "ISC" -} +} \ No newline at end of file From 5ea1ff94d1317f9ea130a36333c27a7dc7a61186 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 17 Feb 2020 14:00:56 +0700 Subject: [PATCH 07/74] rpc: fix issue with multiple subscriptions (#4406) Using the WebSocket server, when the same client calls multiple time the subscribe method, only the last subscription receives all the events of the previous ones. example: subscription1 = tm.event = 'NewBlock' subscription2 = tm.event = 'Tx' In this case, subscription2 will receive the new blocks but subscription1 will not. This came from the WebSocket handler that had the declaration of the rpcrequest moved and so overridden for every request and given in the JSONReq client context (so the id of the subscription was not the right one). This fixes the issue by simply declaring the rpcrequest inside the loop so every request will create a new object without overwriting the previous one. --- CHANGELOG_PENDING.md | 2 ++ rpc/lib/server/ws_handler.go | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index bf41c1b5b..3eea4b2a0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,3 +20,5 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: ### BUG FIXES: + +- [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) diff --git a/rpc/lib/server/ws_handler.go b/rpc/lib/server/ws_handler.go index 548a244cb..07f424b48 100644 --- a/rpc/lib/server/ws_handler.go +++ b/rpc/lib/server/ws_handler.go @@ -299,8 +299,6 @@ func (wsc *wsConnection) Context() context.Context { // Read from the socket and subscribe to or unsubscribe from events func (wsc *wsConnection) readRoutine() { - var request types.RPCRequest - defer func() { if r := recover(); r != nil { err, ok := r.(error) @@ -308,7 +306,7 @@ func (wsc *wsConnection) readRoutine() { err = fmt.Errorf("WSJSONRPC: %v", r) } wsc.Logger.Error("Panic in WSJSONRPC handler", "err", err, "stack", string(debug.Stack())) - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err)) + wsc.WriteRPCResponse(types.RPCInternalError(types.JSONRPCIntID(-1), err)) go wsc.readRoutine() } }() @@ -339,6 +337,7 @@ func (wsc *wsConnection) readRoutine() { return } + var request types.RPCRequest err = json.Unmarshal(in, &request) if err != nil { wsc.WriteRPCResponse(types.RPCParseError(errors.Wrap(err, "error unmarshaling request"))) From f5901ea460e471d926c3f6875c19726526951a79 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 18 Feb 2020 16:10:04 +0100 Subject: [PATCH 08/74] lite2: divide verify functions (#4412) Closes #4398 * divided verify functions * extacted method * renamed functions. Created standard Verify function * checked non-adjacency. separated VerifyCommit * lint fixes * fix godoc documentation for VerifyAdjacent and VerifyNonAdjacent * add a comment about VerifyCommit being the last check Co-authored-by: Anton Kaliaev --- lite2/client.go | 6 +- lite2/verifier.go | 183 +++++++++++++++++++++++++++-------------- lite2/verifier_test.go | 11 +-- 3 files changed, 131 insertions(+), 69 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 717623cbb..3c0a299d8 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -682,8 +682,8 @@ func (c *Client) sequence( "lastHash", c.trustedHeader.Hash(), "newHeight", interimHeader.Height, "newHash", interimHeader.Hash()) - err = Verify(c.chainID, trustedHeader, trustedNextVals, interimHeader, trustedNextVals, - c.trustingPeriod, now, c.trustLevel) + err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, trustedNextVals, + c.trustingPeriod, now) if err != nil { return errors.Wrapf(err, "failed to verify the header #%d", height) } @@ -705,7 +705,7 @@ func (c *Client) sequence( } // 2) Verify the new header. - return Verify(c.chainID, c.trustedHeader, c.trustedNextVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel) + return VerifyAdjacent(c.chainID, c.trustedHeader, newHeader, newVals, c.trustingPeriod, now) } // see VerifyHeader diff --git a/lite2/verifier.go b/lite2/verifier.go index d2f656e41..5491c8d91 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -11,106 +11,167 @@ import ( ) var ( - // DefaultTrustLevel - new header can be trusted if at least one correct old + // DefaultTrustLevel - new header can be trusted if at least one correct // validator signed it. DefaultTrustLevel = tmmath.Fraction{Numerator: 1, Denominator: 3} ) -// Verify verifies the new header (h2) against the old header (h1). It ensures that: +// VerifyNonAdjacent verifies non-adjacent untrustedHeader against +// trustedHeader. It ensures that: // -// a) h1 can still be trusted (if not, ErrOldHeaderExpired is returned); -// b) h2 is valid; -// c) either h2.ValidatorsHash equals h1NextVals.Hash() -// OR trustLevel ([1/3, 1]) of last trusted validators (h1NextVals) signed -// correctly (if not, ErrNewValSetCantBeTrusted is returned); -// c) more than 2/3 of new validators (h2Vals) have signed h2 (if not, -// ErrNotEnoughVotingPowerSigned is returned). -func Verify( +// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); +// b) untrustedHeader is valid; +// c) trustLevel ([1/3, 1]) of trustedHeaderNextVals signed correctly +// (if not, ErrNewValSetCantBeTrusted is returned); +// d) more than 2/3 of untrustedVals have signed h2 (if not, +// ErrNotEnoughVotingPowerSigned is returned); +// e) headers are non-adjacent. +func VerifyNonAdjacent( chainID string, - h1 *types.SignedHeader, - h1NextVals *types.ValidatorSet, - h2 *types.SignedHeader, - h2Vals *types.ValidatorSet, + trustedHeader *types.SignedHeader, // height=X + trustedNextVals *types.ValidatorSet, // height=X+1 + untrustedHeader *types.SignedHeader, // height=Y + untrustedVals *types.ValidatorSet, // height=Y trustingPeriod time.Duration, now time.Time, trustLevel tmmath.Fraction) error { - if err := ValidateTrustLevel(trustLevel); err != nil { + if untrustedHeader.Height == trustedHeader.Height+1 { + return errors.New("headers must be non adjacent in height") + } + + if HeaderExpired(trustedHeader, trustingPeriod, now) { + return ErrOldHeaderExpired{trustedHeader.Time.Add(trustingPeriod), now} + } + + if err := verifyNewHeaderAndVals(chainID, untrustedHeader, untrustedVals, trustedHeader, now); err != nil { return err } - // Ensure last header can still be trusted. - if HeaderExpired(h1, trustingPeriod, now) { - return ErrOldHeaderExpired{h1.Time.Add(trustingPeriod), now} - } - - if err := verifyNewHeaderAndVals(chainID, h2, h2Vals, h1, now); err != nil { - return err - } - - if h2.Height == h1.Height+1 { - if !bytes.Equal(h2.ValidatorsHash, h1NextVals.Hash()) { - err := errors.Errorf("expected old header next validators (%X) to match those from new header (%X)", - h1NextVals.Hash(), - h2.ValidatorsHash, - ) - return err - } - } else { - // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. - err := h1NextVals.VerifyCommitTrusting(chainID, h2.Commit.BlockID, h2.Height, h2.Commit, trustLevel) - if err != nil { - switch e := err.(type) { - case types.ErrNotEnoughVotingPowerSigned: - return ErrNewValSetCantBeTrusted{e} - default: - return e - } + // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. + err := trustedNextVals.VerifyCommitTrusting(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, + untrustedHeader.Commit, trustLevel) + if err != nil { + switch e := err.(type) { + case types.ErrNotEnoughVotingPowerSigned: + return ErrNewValSetCantBeTrusted{e} + default: + return e } } // Ensure that +2/3 of new validators signed correctly. - err := h2Vals.VerifyCommit(chainID, h2.Commit.BlockID, h2.Height, h2.Commit) - if err != nil { + // + // NOTE: this should always be the last check because untrustedVals can be + // intentionaly made very large to DOS the light client. not the case for + // VerifyAdjacent, where validator set is known in advance. + if err := untrustedVals.VerifyCommit(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, + untrustedHeader.Commit); err != nil { return err } return nil } -func verifyNewHeaderAndVals( +// VerifyAdjacent verifies directly adjacent untrustedHeader against +// trustedHeader. It ensures that: +// +// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); +// b) untrustedHeader is valid; +// c) untrustedHeader.ValidatorsHash equals trustedHeaderNextVals.Hash() +// d) more than 2/3 of new validators (untrustedVals) have signed h2 (if not, +// ErrNotEnoughVotingPowerSigned is returned); +// e) headers are adjacent. +func VerifyAdjacent( chainID string, - h2 *types.SignedHeader, - h2Vals *types.ValidatorSet, - h1 *types.SignedHeader, + trustedHeader *types.SignedHeader, // height=X + untrustedHeader *types.SignedHeader, // height=X+1 + untrustedVals *types.ValidatorSet, // height=X+1 + trustingPeriod time.Duration, now time.Time) error { - if err := h2.ValidateBasic(chainID); err != nil { - return errors.Wrap(err, "h2.ValidateBasic failed") + if untrustedHeader.Height != trustedHeader.Height+1 { + return errors.New("headers must be adjacent in height") } - if h2.Height <= h1.Height { + if HeaderExpired(trustedHeader, trustingPeriod, now) { + return ErrOldHeaderExpired{trustedHeader.Time.Add(trustingPeriod), now} + } + + if err := verifyNewHeaderAndVals(chainID, untrustedHeader, untrustedVals, trustedHeader, now); err != nil { + return err + } + + // Check the validator hashes are the same + if !bytes.Equal(untrustedHeader.ValidatorsHash, trustedHeader.NextValidatorsHash) { + err := errors.Errorf("expected old header next validators (%X) to match those from new header (%X)", + trustedHeader.NextValidatorsHash, + untrustedHeader.ValidatorsHash, + ) + return err + } + + // Ensure that +2/3 of new validators signed correctly. + if err := untrustedVals.VerifyCommit(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, + untrustedHeader.Commit); err != nil { + return err + } + + return nil +} + +// Verify combines both VerifyAdjacent and VerifyNonAdjacent functions. +func Verify( + chainID string, + trustedHeader *types.SignedHeader, // height=X + trustedNextVals *types.ValidatorSet, // height=X+1 + untrustedHeader *types.SignedHeader, // height=Y + untrustedVals *types.ValidatorSet, // height=Y + trustingPeriod time.Duration, + now time.Time, + trustLevel tmmath.Fraction) error { + + if untrustedHeader.Height != trustedHeader.Height+1 { + return VerifyNonAdjacent(chainID, trustedHeader, trustedNextVals, untrustedHeader, untrustedVals, + trustingPeriod, now, trustLevel) + } + + return VerifyAdjacent(chainID, trustedHeader, untrustedHeader, untrustedVals, trustingPeriod, now) +} + +func verifyNewHeaderAndVals( + chainID string, + untrustedHeader *types.SignedHeader, + untrustedVals *types.ValidatorSet, + trustedHeader *types.SignedHeader, + now time.Time) error { + + if err := untrustedHeader.ValidateBasic(chainID); err != nil { + return errors.Wrap(err, "untrustedHeader.ValidateBasic failed") + } + + if untrustedHeader.Height <= trustedHeader.Height { return errors.Errorf("expected new header height %d to be greater than one of old header %d", - h2.Height, - h1.Height) + untrustedHeader.Height, + trustedHeader.Height) } - if !h2.Time.After(h1.Time) { + if !untrustedHeader.Time.After(trustedHeader.Time) { return errors.Errorf("expected new header time %v to be after old header time %v", - h2.Time, - h1.Time) + untrustedHeader.Time, + trustedHeader.Time) } - if !h2.Time.Before(now) { + if !untrustedHeader.Time.Before(now) { return errors.Errorf("new header has a time from the future %v (now: %v)", - h2.Time, + untrustedHeader.Time, now) } - if !bytes.Equal(h2.ValidatorsHash, h2Vals.Hash()) { + if !bytes.Equal(untrustedHeader.ValidatorsHash, untrustedVals.Hash()) { return errors.Errorf("expected new header validators (%X) to match those that were supplied (%X)", - h2.ValidatorsHash, - h2Vals.Hash(), + untrustedHeader.ValidatorsHash, + untrustedVals.Hash(), ) } diff --git a/lite2/verifier_test.go b/lite2/verifier_test.go index 1625213cc..6e118d7ae 100644 --- a/lite2/verifier_test.go +++ b/lite2/verifier_test.go @@ -42,7 +42,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { 3 * time.Hour, bTime.Add(2 * time.Hour), nil, - "expected new header height 1 to be greater than one of old header 1", + "headers must be adjacent in height", }, // different chainID -> error 1: { @@ -52,7 +52,8 @@ func TestVerifyAdjacentHeaders(t *testing.T) { 3 * time.Hour, bTime.Add(2 * time.Hour), nil, - "h2.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not 'TestVerifyAdjacentHeaders'", + "untrustedHeader.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not" + + " 'TestVerifyAdjacentHeaders'", }, // new header's time is before old header's time -> error 2: { @@ -139,8 +140,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { - err := Verify(chainID, header, vals, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now, DefaultTrustLevel) - + err := VerifyAdjacent(chainID, header, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now) switch { case tc.expErr != nil && assert.Error(t, err): assert.Equal(t, tc.expErr, err) @@ -254,7 +254,8 @@ func TestVerifyNonAdjacentHeaders(t *testing.T) { for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { - err := Verify(chainID, header, vals, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now, DefaultTrustLevel) + err := VerifyNonAdjacent(chainID, header, vals, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now, + DefaultTrustLevel) switch { case tc.expErr != nil && assert.Error(t, err): From 6c67e246c308a1606706d5cf1280ad6f538feecf Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 18 Feb 2020 16:20:02 +0100 Subject: [PATCH 09/74] dep: bump gokit dep (#4424) * dep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokitdep: bump gokit- Signedoff-by: Marko Baricevic * go mod tidy * test go version change * combination of multiple cmds Co-authored-by: Anton Kaliaev --- go.mod | 16 ++-- go.sum | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index b52f7ca4b..87cab15d0 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,32 @@ module github.com/tendermint/tendermint -go 1.12 +go 1.13 require ( github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f - github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/Workiva/go-datastructures v1.0.50 github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a github.com/fortytw2/leaktest v1.3.0 - github.com/go-kit/kit v0.9.0 + github.com/go-kit/kit v0.10.0 github.com/go-logfmt/logfmt v0.5.0 github.com/gogo/protobuf v1.3.1 github.com/golang/protobuf v1.3.3 - github.com/google/gofuzz v1.0.0 // indirect github.com/gorilla/websocket v1.4.1 github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/libp2p/go-buffer-pool v0.0.2 github.com/magiconair/properties v1.8.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v0.9.3 - github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 + github.com/prometheus/client_golang v1.3.0 + github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a github.com/rs/cors v1.7.0 github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa - github.com/spf13/cobra v0.0.1 + github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.6.2 github.com/stretchr/testify v1.4.0 github.com/tendermint/go-amino v0.14.1 github.com/tendermint/tm-db v0.4.0 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 - golang.org/x/net v0.0.0-20190628185345-da137c7871d7 - golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 google.golang.org/grpc v1.27.1 ) diff --git a/go.sum b/go.sum index 19cfd4cb8..eb2868125 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,40 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f h1:4O1om+UVU+Hfcihr1timk8YNXHxzZWgCo7ofnrZRApw= github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.50 h1:slDmfW6KCHcC7U+LP3DDBbm4fqTwZGn1beOFPfGaLvo= github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= @@ -26,22 +45,40 @@ github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVa github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= @@ -52,8 +89,11 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -61,22 +101,29 @@ github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -84,36 +131,75 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -131,56 +217,121 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.0 h1:J8lpUdobwIeCI7OiSxHqEwJUKvJwicL5+3v1oe2Yb4k= -github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= -github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -188,26 +339,30 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa h1:YJfZp12Z3AFhSBeXOlv4BO55RMwPn2NoQeDsrdWnBtY= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= -github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= -github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= @@ -220,76 +375,136 @@ github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6o github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= github.com/tendermint/tm-db v0.4.0 h1:iPbCcLbf4nwDFhS39Zo1lpdS1X/cT9CkTlUx17FHQgA= github.com/tendermint/tm-db v0.4.0/go.mod h1:+Cwhgowrf7NBGXmsqFMbwEtbo80XmyrlY5Jsk95JubQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -297,13 +512,17 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -311,5 +530,9 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From 68f37fff65102ef9d5ff51df2cd0f43489fc20c1 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Wed, 19 Feb 2020 12:28:34 +0100 Subject: [PATCH 10/74] Use uint64 for consensus.Reactor.SwitchToConsensus() blocksSynced (#4433) --- blockchain/v0/reactor.go | 4 ++-- blockchain/v1/reactor.go | 4 ++-- blockchain/v1/reactor_test.go | 2 +- consensus/reactor.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blockchain/v0/reactor.go b/blockchain/v0/reactor.go index c35a3e6a9..d47e892c2 100644 --- a/blockchain/v0/reactor.go +++ b/blockchain/v0/reactor.go @@ -41,7 +41,7 @@ const ( type consensusReactor interface { // for when we switch from blockchain reactor and fast sync to // the consensus machine - SwitchToConsensus(sm.State, int) + SwitchToConsensus(sm.State, uint64) } type peerError struct { @@ -214,7 +214,7 @@ func (bcR *BlockchainReactor) poolRoutine() { statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) switchToConsensusTicker := time.NewTicker(switchToConsensusIntervalSeconds * time.Second) - blocksSynced := 0 + blocksSynced := uint64(0) chainID := bcR.initialState.ChainID state := bcR.initialState diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go index c1932d406..b57901789 100644 --- a/blockchain/v1/reactor.go +++ b/blockchain/v1/reactor.go @@ -43,7 +43,7 @@ var ( type consensusReactor interface { // for when we switch from blockchain reactor and fast sync to // the consensus machine - SwitchToConsensus(sm.State, int) + SwitchToConsensus(sm.State, uint64) } // BlockchainReactor handles long-term catchup syncing. @@ -59,7 +59,7 @@ type BlockchainReactor struct { fastSync bool fsm *BcReactorFSM - blocksSynced int + blocksSynced uint64 // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. messagesForFSMCh chan bcReactorMessage diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go index 2add11df1..deb73ad6d 100644 --- a/blockchain/v1/reactor_test.go +++ b/blockchain/v1/reactor_test.go @@ -157,7 +157,7 @@ type consensusReactorTest struct { mtx sync.Mutex } -func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced int) { +func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced uint64) { conR.mtx.Lock() defer conR.mtx.Unlock() conR.switchedToConsensus = true diff --git a/consensus/reactor.go b/consensus/reactor.go index 40868ff9d..0f2dad743 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -98,7 +98,7 @@ func (conR *Reactor) OnStop() { // SwitchToConsensus switches from fast_sync mode to consensus mode. // It resets the state, turns off fast_sync, and starts the consensus state-machine -func (conR *Reactor) SwitchToConsensus(state sm.State, blocksSynced int) { +func (conR *Reactor) SwitchToConsensus(state sm.State, blocksSynced uint64) { conR.Logger.Info("SwitchToConsensus") conR.conS.reconstructLastCommit(state) // NOTE: The line below causes broadcastNewRoundStepRoutine() to From c680507bab100382ebab5b7d49d1f007b8f5c3cf Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 19 Feb 2020 13:11:17 +0100 Subject: [PATCH 11/74] dep: maunally bump dep (#4436) replaces #4434 Signed-off-by: Marko Baricevic --- go.mod | 2 +- go.sum | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 87cab15d0..7b669b1db 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/libp2p/go-buffer-pool v0.0.2 github.com/magiconair/properties v1.8.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.3.0 + github.com/prometheus/client_golang v1.4.1 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a github.com/rs/cors v1.7.0 github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa diff --git a/go.sum b/go.sum index eb2868125..e606cd3fa 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -200,6 +202,7 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -294,6 +297,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= +github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -303,6 +308,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= @@ -310,6 +317,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -462,6 +471,8 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -485,6 +496,8 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -512,6 +525,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= @@ -530,6 +545,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From b09cdaf1afc7b8ed9c9a1685b36ac27986da87e3 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Wed, 19 Feb 2020 14:01:42 +0100 Subject: [PATCH 12/74] rpc: fix tx_search pagination with ordered results (#4437) --- CHANGELOG_PENDING.md | 2 ++ rpc/client/rpc_test.go | 57 ++++++++++++++++++++++++++++-------- rpc/core/tx.go | 66 ++++++++++++++++++++---------------------- 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 3eea4b2a0..a2fc85ee0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -21,4 +21,6 @@ program](https://hackerone.com/tendermint). ### BUG FIXES: +- [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) + - [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 55c7755ac..cd27ee4d7 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -17,6 +17,7 @@ import ( "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/tmhash" + tmbytes "github.com/tendermint/tendermint/libs/bytes" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" mempl "github.com/tendermint/tendermint/mempool" @@ -414,14 +415,25 @@ func TestTx(t *testing.T) { } func TestTxSearch(t *testing.T) { - // first we broadcast a tx c := getHTTPClient() - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err) - txHeight := bres.Height - txHash := bres.Hash + // first we broadcast a few txs + var tx []byte + var txHeight int64 + var txHash tmbytes.HexBytes + for i := 0; i < 10; i++ { + _, _, tx = MakeTxKV() + res, err := c.BroadcastTxCommit(tx) + require.NoError(t, err) + txHeight = res.Height + txHash = res.Hash + } + + // Since we're not using an isolated test server, we'll have lingering transactions + // from other tests as well + result, err := c.TxSearch("tx.height >= 0", true, 1, 100, "asc") + require.NoError(t, err) + txCount := len(result.Txs) anotherTxHash := types.Tx("a different tx").Hash() @@ -433,6 +445,7 @@ func TestTxSearch(t *testing.T) { result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", txHash), true, 1, 30, "asc") require.Nil(t, err) require.Len(t, result.Txs, 1) + require.Equal(t, txHash, result.Txs[0].Hash) ptx := result.Txs[0] assert.EqualValues(t, txHeight, ptx.Height) @@ -476,12 +489,7 @@ func TestTxSearch(t *testing.T) { require.Nil(t, err) require.Len(t, result.Txs, 0) - // broadcast another transaction to make sure we have at least two. - _, _, tx2 := MakeTxKV() - _, err = c.BroadcastTxCommit(tx2) - require.Nil(t, err) - - // chech sorting + // check sorting result, err = c.TxSearch(fmt.Sprintf("tx.height >= 1"), false, 1, 30, "asc") require.Nil(t, err) for k := 0; k < len(result.Txs)-1; k++ { @@ -495,6 +503,31 @@ func TestTxSearch(t *testing.T) { require.GreaterOrEqual(t, result.Txs[k].Height, result.Txs[k+1].Height) require.GreaterOrEqual(t, result.Txs[k].Index, result.Txs[k+1].Index) } + + // check pagination + seen := map[int64]bool{} + maxHeight := int64(0) + perPage := 3 + pages := txCount/perPage + 1 + for page := 1; page <= pages; page++ { + result, err = c.TxSearch("tx.height >= 1", false, page, perPage, "asc") + require.NoError(t, err) + if page < pages { + require.Len(t, result.Txs, perPage) + } else { + require.LessOrEqual(t, len(result.Txs), perPage) + } + require.Equal(t, txCount, result.TotalCount) + for _, tx := range result.Txs { + require.False(t, seen[tx.Height], + "Found duplicate height %v in page %v", tx.Height, page) + require.Greater(t, tx.Height, maxHeight, + "Found decreasing height %v (max seen %v) in page %v", tx.Height, maxHeight, page) + seen[tx.Height] = true + maxHeight = tx.Height + } + } + require.Len(t, seen, txCount) } } diff --git a/rpc/core/tx.go b/rpc/core/tx.go index cd6306060..f894424b7 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -72,6 +72,27 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int return nil, err } + // sort results (must be done before pagination) + switch orderBy { + case "desc": + sort.Slice(results, func(i, j int) bool { + if results[i].Height == results[j].Height { + return results[i].Index > results[j].Index + } + return results[i].Height > results[j].Height + }) + case "asc", "": + sort.Slice(results, func(i, j int) bool { + if results[i].Height == results[j].Height { + return results[i].Index < results[j].Index + } + return results[i].Height < results[j].Height + }) + default: + return nil, errors.New("expected order_by to be either `asc` or `desc` or empty") + } + + // paginate results totalCount := len(results) perPage = validatePerPage(perPage) page, err = validatePage(page, perPage, totalCount) @@ -79,49 +100,26 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int return nil, err } skipCount := validateSkipCount(page, perPage) + pageSize := tmmath.MinInt(perPage, totalCount-skipCount) - apiResults := make([]*ctypes.ResultTx, tmmath.MinInt(perPage, totalCount-skipCount)) - var proof types.TxProof - // if there's no tx in the results array, we don't need to loop through the apiResults array - for i := 0; i < len(apiResults); i++ { - r := results[skipCount+i] - height := r.Height - index := r.Index + apiResults := make([]*ctypes.ResultTx, 0, pageSize) + for i := skipCount; i < skipCount+pageSize; i++ { + r := results[i] + var proof types.TxProof if prove { - block := blockStore.LoadBlock(height) - proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines + block := blockStore.LoadBlock(r.Height) + proof = block.Data.Txs.Proof(int(r.Index)) // XXX: overflow on 32-bit machines } - apiResults[i] = &ctypes.ResultTx{ + apiResults = append(apiResults, &ctypes.ResultTx{ Hash: r.Tx.Hash(), - Height: height, - Index: index, + Height: r.Height, + Index: r.Index, TxResult: r.Result, Tx: r.Tx, Proof: proof, - } - } - - if len(apiResults) > 1 { - switch orderBy { - case "desc": - sort.Slice(apiResults, func(i, j int) bool { - if apiResults[i].Height == apiResults[j].Height { - return apiResults[i].Index > apiResults[j].Index - } - return apiResults[i].Height > apiResults[j].Height - }) - case "asc", "": - sort.Slice(apiResults, func(i, j int) bool { - if apiResults[i].Height == apiResults[j].Height { - return apiResults[i].Index < apiResults[j].Index - } - return apiResults[i].Height < apiResults[j].Height - }) - default: - return nil, errors.New("expected order_by to be either `asc` or `desc` or empty") - } + }) } return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil From 1874a971706380275d86a53b337a51f55ad83c17 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Wed, 19 Feb 2020 15:18:52 +0100 Subject: [PATCH 13/74] lite: modified bisection to loop (#4400) refs #4329 As opposed to using recursion to implement the bisection method of verifying a header, which could have problems with memory allocation (especially for smaller devices), the bisection algorithm now uses a for loop. * modified bisection to loop * made lint changes * made lint changes * move note to VerifyHeader since it applies both for sequence and bisection * test bisection jumps to header signed by 1/3+ of old validator set * update labels in debug log calls * copy tc Co-authored-by: Anton Kaliaev --- lite2/client.go | 115 +++++++++++++-------------- lite2/client_test.go | 179 +++++++++++++++++++++++++++++------------- lite2/test_helpers.go | 10 +-- 3 files changed, 184 insertions(+), 120 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 3c0a299d8..2c487f9c9 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -294,8 +294,8 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { case options.Height < c.trustedHeader.Height: c.logger.Info("Client initialized with old header (trusted is more recent)", "old", options.Height, - "trusted", c.trustedHeader.Height, - "trusted-hash", hash2str(c.trustedHeader.Hash())) + "trustedHeight", c.trustedHeader.Height, + "trustedHash", hash2str(c.trustedHeader.Hash())) action := fmt.Sprintf( "Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)", @@ -373,14 +373,14 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { } // 3) Fetch and verify the next vals (verification happens in - // updateTrustedHeaderAndVals). + // updateTrustedHeaderAndNextVals). nextVals, err := c.validatorSetFromPrimary(options.Height + 1) if err != nil { return err } // 4) Persist both of them and continue. - return c.updateTrustedHeaderAndVals(h, nextVals) + return c.updateTrustedHeaderAndNextVals(h, nextVals) } // Start starts two processes: 1) auto updating 2) removing outdated headers. @@ -560,6 +560,10 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // If, at any moment, SignedHeader or ValidatorSet are not found by the primary // provider, provider.ErrSignedHeaderNotFound / // provider.ErrValidatorSetNotFound error is returned. +// +// NOTE: although newVals is entered as input, trustedStore will only store the +// validator set at height newHeader.Height+1 (i.e. +// newHeader.NextValidatorsHash). func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { c.logger.Info("VerifyHeader", "height", newHeader.Height, "hash", hash2str(newHeader.Hash()), "vals", hash2str(newVals.Hash())) @@ -592,7 +596,7 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali if err != nil { return err } - return c.updateTrustedHeaderAndVals(newHeader, nextVals) + return c.updateTrustedHeaderAndNextVals(newHeader, nextVals) } // Primary returns the primary provider. @@ -678,10 +682,10 @@ func (c *Client) sequence( } c.logger.Debug("Verify newHeader against trustedHeader", - "lastHeight", c.trustedHeader.Height, - "lastHash", c.trustedHeader.Hash(), + "trustedHeight", c.trustedHeader.Height, + "trustedHash", hash2str(c.trustedHeader.Hash()), "newHeight", interimHeader.Height, - "newHash", interimHeader.Hash()) + "newHash", hash2str(interimHeader.Hash())) err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, trustedNextVals, c.trustingPeriod, now) if err != nil { @@ -697,7 +701,7 @@ func (c *Client) sequence( return errors.Wrapf(err, "failed to obtain the vals #%d", height+1) } } - err = c.updateTrustedHeaderAndVals(interimHeader, interimNextVals) + err = c.updateTrustedHeaderAndNextVals(interimHeader, interimNextVals) if err != nil { return errors.Wrapf(err, "failed to update trusted state #%d", height) } @@ -710,62 +714,55 @@ func (c *Client) sequence( // see VerifyHeader func (c *Client) bisection( - lastHeader *types.SignedHeader, - lastVals *types.ValidatorSet, - newHeader *types.SignedHeader, - newVals *types.ValidatorSet, + trustedHeader *types.SignedHeader, // height h + trustedNextVals *types.ValidatorSet, // height h + 1 + newHeader *types.SignedHeader, // height g + newVals *types.ValidatorSet, // height g now time.Time) error { - c.logger.Debug("Verify newHeader against lastHeader", - "lastHeight", lastHeader.Height, - "lastHash", lastHeader.Hash(), - "newHeight", newHeader.Height, - "newHash", newHeader.Hash()) - err := Verify(c.chainID, lastHeader, lastVals, newHeader, newVals, c.trustingPeriod, now, c.trustLevel) - switch err.(type) { - case nil: - return nil - case ErrNewValSetCantBeTrusted: - // continue bisection - default: - return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) - } + interimVals := newVals + interimHeader := newHeader - pivot := (c.trustedHeader.Height + newHeader.Header.Height) / 2 - pivotHeader, pivotVals, err := c.fetchHeaderAndValsAtHeight(pivot) - if err != nil { - return err - } + for trustedHeader.Height < newHeader.Height { + c.logger.Debug("Verify newHeader against trustedHeader", + "trustedHeight", trustedHeader.Height, + "trustedHash", hash2str(trustedHeader.Hash()), + "newHeight", newHeader.Height, + "newHash", hash2str(newHeader.Hash())) + err := Verify(c.chainID, trustedHeader, trustedNextVals, interimHeader, interimVals, c.trustingPeriod, now, + c.trustLevel) + switch err.(type) { + case nil: + // Update the lower bound to the previous upper bound + trustedHeader = interimHeader + trustedNextVals, err = c.validatorSetFromPrimary(interimHeader.Height + 1) + if err != nil { + return err + } + if !bytes.Equal(trustedHeader.NextValidatorsHash, trustedNextVals.Hash()) { + return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", + trustedHeader.NextValidatorsHash, + trustedNextVals.Hash(), + trustedHeader.Height) + } - // left branch - { - err := c.bisection(lastHeader, lastVals, pivotHeader, pivotVals, now) - if err != nil { - return errors.Wrapf(err, "bisection of #%d and #%d", lastHeader.Height, pivot) - } - } + err = c.updateTrustedHeaderAndNextVals(trustedHeader, trustedNextVals) + if err != nil { + return err + } - // right branch - { - nextVals, err := c.validatorSetFromPrimary(pivot + 1) - if err != nil { - return errors.Wrapf(err, "failed to obtain the vals #%d", pivot+1) - } - if !bytes.Equal(pivotHeader.NextValidatorsHash, nextVals.Hash()) { - return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", - pivotHeader.NextValidatorsHash, - nextVals.Hash(), - pivot) - } + // Update the upper bound to the untrustedHeader + interimHeader, interimVals = newHeader, newVals - err = c.updateTrustedHeaderAndVals(pivotHeader, nextVals) - if err != nil { - return errors.Wrapf(err, "failed to update trusted state #%d", pivot) - } + case ErrNewValSetCantBeTrusted: + pivotHeight := (interimHeader.Height + trustedHeader.Height) / 2 + interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(pivotHeight) + if err != nil { + return err + } - err = c.bisection(pivotHeader, nextVals, newHeader, newVals, now) - if err != nil { - return errors.Wrapf(err, "bisection of #%d and #%d", pivot, newHeader.Height) + default: + return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) } } @@ -773,7 +770,7 @@ func (c *Client) bisection( } // persist header and next validators to trustedStore. -func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error { +func (c *Client) updateTrustedHeaderAndNextVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error { if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) { return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, nextVals.Hash()) } diff --git a/lite2/client_test.go b/lite2/client_test.go index e2931f323..d8f44adc6 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -27,8 +27,10 @@ var ( bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") h1 = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) + // 3/3 signed h2 = keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys), types.BlockID{Hash: h1.Hash()}) + // 3/3 signed h3 = keys.GenSignedHeaderLastBlockID(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys), types.BlockID{Hash: h2.Hash()}) trustPeriod = 4 * time.Hour @@ -56,13 +58,14 @@ var ( func TestClient_SequentialVerification(t *testing.T) { testCases := []struct { + name string otherHeaders map[int64]*types.SignedHeader // all except ^ vals map[int64]*types.ValidatorSet initErr bool verifyErr bool }{ - // good { + "good", map[int64]*types.SignedHeader{ // trusted header 1: h1, @@ -80,8 +83,8 @@ func TestClient_SequentialVerification(t *testing.T) { false, false, }, - // bad: different first header { + "bad: different first header", map[int64]*types.SignedHeader{ // different header 1: keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals, @@ -93,8 +96,8 @@ func TestClient_SequentialVerification(t *testing.T) { true, false, }, - // bad: 1/3 signed interim header { + "bad: 1/3 signed interim header", map[int64]*types.SignedHeader{ // trusted header 1: h1, @@ -114,8 +117,8 @@ func TestClient_SequentialVerification(t *testing.T) { false, true, }, - // bad: 1/3 signed last header { + "bad: 1/3 signed last header", map[int64]*types.SignedHeader{ // trusted header 1: h1, @@ -138,39 +141,42 @@ func TestClient_SequentialVerification(t *testing.T) { } for _, tc := range testCases { - c, err := NewClient( - chainID, - trustOptions, - mockp.New( + tc := tc + t.Run(tc.name, func(t *testing.T) { + c, err := NewClient( chainID, - tc.otherHeaders, - tc.vals, - ), - []provider.Provider{mockp.New( - chainID, - tc.otherHeaders, - tc.vals, - )}, - dbs.New(dbm.NewMemDB(), chainID), - SequentialVerification(), - ) + trustOptions, + mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + ), + []provider.Provider{mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + )}, + dbs.New(dbm.NewMemDB(), chainID), + SequentialVerification(), + ) + + if tc.initErr { + require.Error(t, err) + return + } - if tc.initErr { - require.Error(t, err) - continue - } else { require.NoError(t, err) - } - err = c.Start() - require.NoError(t, err) - defer c.Stop() + err = c.Start() + require.NoError(t, err) + defer c.Stop() - _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) - if tc.verifyErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) + if tc.verifyErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } } @@ -179,12 +185,19 @@ func TestClient_SkippingVerification(t *testing.T) { newKeys := genPrivKeys(4) newVals := newKeys.ToValidators(10, 1) + // 1/3+ of vals, 2/3- of newVals + transitKeys := keys.Extend(3) + transitVals := transitKeys.ToValidators(10, 1) + testCases := []struct { + name string otherHeaders map[int64]*types.SignedHeader // all except ^ vals map[int64]*types.ValidatorSet + initErr bool + verifyErr bool }{ - // good { + "good", map[int64]*types.SignedHeader{ // trusted header 1: h1, @@ -197,9 +210,28 @@ func TestClient_SkippingVerification(t *testing.T) { 3: vals, 4: vals, }, + false, + false, }, - // good, val set changes 100% at height 2 { + "good, but val set changes by 2/3 (1/3 of vals is still present)", + map[int64]*types.SignedHeader{ + // trusted header + 1: h1, + 3: transitKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, transitVals, transitVals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(transitKeys)), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: transitVals, + 4: transitVals, + }, + false, + false, + }, + { + "good, but val set changes 100% at height 2", map[int64]*types.SignedHeader{ // trusted header 1: h1, @@ -216,33 +248,68 @@ func TestClient_SkippingVerification(t *testing.T) { 3: newVals, 4: newVals, }, + false, + false, + }, + { + "bad: last header signed by newVals, interim header has no signers", + map[int64]*types.SignedHeader{ + // trusted header + 1: h1, + // last header (0/4 of the original val set signed) + 2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, newVals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, 0), + // last header (0/4 of the original val set signed) + 3: newKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, newVals, newVals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(newKeys)), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: newVals, + 4: newVals, + }, + false, + true, }, } for _, tc := range testCases { - c, err := NewClient( - chainID, - trustOptions, - mockp.New( + tc := tc + t.Run(tc.name, func(t *testing.T) { + c, err := NewClient( chainID, - tc.otherHeaders, - tc.vals, - ), - []provider.Provider{mockp.New( - chainID, - tc.otherHeaders, - tc.vals, - )}, - dbs.New(dbm.NewMemDB(), chainID), - SkippingVerification(DefaultTrustLevel), - ) - require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() + trustOptions, + mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + ), + []provider.Provider{mockp.New( + chainID, + tc.otherHeaders, + tc.vals, + )}, + dbs.New(dbm.NewMemDB(), chainID), + SkippingVerification(DefaultTrustLevel), + ) + if tc.initErr { + require.Error(t, err) + return + } - _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) - assert.NoError(t, err) + require.NoError(t, err) + err = c.Start() + require.NoError(t, err) + defer c.Stop() + + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) + if tc.verifyErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } } diff --git a/lite2/test_helpers.go b/lite2/test_helpers.go index 11074f8c0..cc1bf4eb9 100644 --- a/lite2/test_helpers.go +++ b/lite2/test_helpers.go @@ -36,11 +36,11 @@ func genPrivKeys(n int) privKeys { // return res // } -// // Extend adds n more keys (to remove, just take a slice). -// func (pkz privKeys) Extend(n int) privKeys { -// extra := genPrivKeys(n) -// return append(pkz, extra...) -// } +// Extend adds n more keys (to remove, just take a slice). +func (pkz privKeys) Extend(n int) privKeys { + extra := genPrivKeys(n) + return append(pkz, extra...) +} // // GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. // func GenSecpPrivKeys(n int) privKeys { From 403f43a4770b74546664f2c77720c64234300717 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Wed, 19 Feb 2020 15:36:15 +0100 Subject: [PATCH 14/74] rpc: fix txsearch tests (#4438) --- rpc/client/rpc_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index cd27ee4d7..c1418c816 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -3,6 +3,7 @@ package client_test import ( "bytes" "fmt" + "math" "math/rand" "net/http" "strings" @@ -508,7 +509,7 @@ func TestTxSearch(t *testing.T) { seen := map[int64]bool{} maxHeight := int64(0) perPage := 3 - pages := txCount/perPage + 1 + pages := int(math.Ceil(float64(txCount) / float64(perPage))) for page := 1; page <= pages; page++ { result, err = c.TxSearch("tx.height >= 1", false, page, perPage, "asc") require.NoError(t, err) From ee993ba8ff9893ab4c46b269987a3138ead223ff Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Wed, 19 Feb 2020 16:00:15 +0100 Subject: [PATCH 15/74] blockchain: add v2 reactor (#4361) The work includes the reactor which ties together all the seperate routines involved in the design of the blockchain v2 refactor. This PR replaces #4067 which got far too large and messy after a failed attempt to rebase. ## Commits: * Blockchainv 2 reactor: + I cleaner copy of the work done in #4067 which fell too far behind and was a nightmare to rebase. + The work includes the reactor which ties together all the seperate routines involved in the design of the blockchain v2 refactor. * fixes after merge * reorder iIO interface methodset * change iO -> IO * panic before send nil block * rename switchToConsensus -> trySwitchToConsensus * rename tdState -> tmState * Update blockchain/v2/reactor.go Co-Authored-By: Bot from GolangCI <42910462+golangcibot@users.noreply.github.com> * remove peer when it sends a block unsolicited * check for not ready in markReceived * fix error * fix the pcFinished event * typo fix * add documentation for processor fields * simplify time.Since * try and make the linter happy * some doc updates * fix channel diagram * Update adr-043-blockchain-riri-org.md * panic on nil switch * liting fixes * account for nil block in bBlockResponseMessage * panic on duplicate block enqueued by processor * linting * goimport reactor_test.go Co-authored-by: Bot from GolangCI <42910462+golangcibot@users.noreply.github.com> Co-authored-by: Anca Zamfir Co-authored-by: Marko Co-authored-by: Anton Kaliaev --- behaviour/reporter.go | 2 +- blockchain/v1/reactor.go | 4 +- blockchain/v2/codec.go | 13 + blockchain/v2/io.go | 111 ++++ blockchain/v2/metrics.go | 1 + blockchain/v2/processor.go | 126 ++--- blockchain/v2/processor_context.go | 51 +- blockchain/v2/processor_test.go | 179 +++--- blockchain/v2/reactor.go | 521 ++++++++++++++++-- blockchain/v2/reactor_test.go | 513 ++++++++++++++++- blockchain/v2/routine.go | 5 +- blockchain/v2/scheduler.go | 203 +++---- blockchain/v2/scheduler_test.go | 519 +++++++---------- blockchain/v2/types.go | 1 + .../adr-043-blockchain-riri-org.md | 11 +- .../img/blockchain-reactor-v2.png | Bin 120617 -> 171024 bytes .../img/blockchain-v2-channels.png | Bin 0 -> 109340 bytes 17 files changed, 1546 insertions(+), 714 deletions(-) create mode 100644 blockchain/v2/codec.go create mode 100644 blockchain/v2/io.go create mode 100644 docs/architecture/img/blockchain-v2-channels.png diff --git a/behaviour/reporter.go b/behaviour/reporter.go index 96ce32994..1f16b9bb3 100644 --- a/behaviour/reporter.go +++ b/behaviour/reporter.go @@ -19,7 +19,7 @@ type SwitchReporter struct { } // NewSwitchReporter return a new SwitchReporter instance which wraps the Switch. -func NewSwitcReporter(sw *p2p.Switch) *SwitchReporter { +func NewSwitchReporter(sw *p2p.Switch) *SwitchReporter { return &SwitchReporter{ sw: sw, } diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go index b57901789..1aba26b35 100644 --- a/blockchain/v1/reactor.go +++ b/blockchain/v1/reactor.go @@ -103,7 +103,7 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *st fsm := NewFSM(startHeight, bcR) bcR.fsm = fsm bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) - //bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + //bcR.swReporter = behaviour.NewSwitchReporter(bcR.BaseReactor.Switch) return bcR } @@ -141,7 +141,7 @@ func (bcR *BlockchainReactor) SetLogger(l log.Logger) { // OnStart implements service.Service. func (bcR *BlockchainReactor) OnStart() error { - bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + bcR.swReporter = behaviour.NewSwitchReporter(bcR.BaseReactor.Switch) if bcR.fastSync { go bcR.poolRoutine() } diff --git a/blockchain/v2/codec.go b/blockchain/v2/codec.go new file mode 100644 index 000000000..f970d115f --- /dev/null +++ b/blockchain/v2/codec.go @@ -0,0 +1,13 @@ +package v2 + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + RegisterBlockchainMessages(cdc) + types.RegisterBlockAmino(cdc) +} diff --git a/blockchain/v2/io.go b/blockchain/v2/io.go new file mode 100644 index 000000000..3db48c8c0 --- /dev/null +++ b/blockchain/v2/io.go @@ -0,0 +1,111 @@ +package v2 + +import ( + "fmt" + + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" +) + +type iIO interface { + sendBlockRequest(peerID p2p.ID, height int64) error + sendBlockToPeer(block *types.Block, peerID p2p.ID) error + sendBlockNotFound(height int64, peerID p2p.ID) error + sendStatusResponse(height int64, peerID p2p.ID) error + + broadcastStatusRequest(height int64) + + trySwitchToConsensus(state state.State, blocksSynced int) +} + +type switchIO struct { + sw *p2p.Switch +} + +func newSwitchIo(sw *p2p.Switch) *switchIO { + return &switchIO{ + sw: sw, + } +} + +const ( + // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) + BlockchainChannel = byte(0x40) +) + +type consensusReactor interface { + // for when we switch from blockchain reactor and fast sync to + // the consensus machine + SwitchToConsensus(state.State, int) +} + +func (sio *switchIO) sendBlockRequest(peerID p2p.ID, height int64) error { + peer := sio.sw.Peers().Get(peerID) + if peer == nil { + return fmt.Errorf("peer not found") + } + + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockRequestMessage{Height: height}) + queued := peer.TrySend(BlockchainChannel, msgBytes) + if !queued { + return fmt.Errorf("send queue full") + } + return nil +} + +func (sio *switchIO) sendStatusResponse(height int64, peerID p2p.ID) error { + peer := sio.sw.Peers().Get(peerID) + if peer == nil { + return fmt.Errorf("peer not found") + } + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{Height: height}) + + if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { + return fmt.Errorf("peer queue full") + } + + return nil +} + +func (sio *switchIO) sendBlockToPeer(block *types.Block, peerID p2p.ID) error { + peer := sio.sw.Peers().Get(peerID) + if peer == nil { + return fmt.Errorf("peer not found") + } + if block == nil { + panic("trying to send nil block") + } + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockResponseMessage{Block: block}) + if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { + return fmt.Errorf("peer queue full") + } + + return nil +} + +func (sio *switchIO) sendBlockNotFound(height int64, peerID p2p.ID) error { + peer := sio.sw.Peers().Get(peerID) + if peer == nil { + return fmt.Errorf("peer not found") + } + msgBytes := cdc.MustMarshalBinaryBare(&bcNoBlockResponseMessage{Height: height}) + if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { + return fmt.Errorf("peer queue full") + } + + return nil +} + +func (sio *switchIO) trySwitchToConsensus(state state.State, blocksSynced int) { + conR, ok := sio.sw.Reactor("CONSENSUS").(consensusReactor) + if ok { + conR.SwitchToConsensus(state, blocksSynced) + } +} + +func (sio *switchIO) broadcastStatusRequest(height int64) { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusRequestMessage{height}) + // XXX: maybe we should use an io specific peer list here + sio.sw.Broadcast(BlockchainChannel, msgBytes) +} diff --git a/blockchain/v2/metrics.go b/blockchain/v2/metrics.go index d865e7360..c68ec6447 100644 --- a/blockchain/v2/metrics.go +++ b/blockchain/v2/metrics.go @@ -37,6 +37,7 @@ type Metrics struct { ErrorsShed metrics.Counter } +// PrometheusMetrics returns metrics for in and out events, errors, etc. handled by routines. // Can we burn in the routine name here? func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { labels := []string{} diff --git a/blockchain/v2/processor.go b/blockchain/v2/processor.go index e33b36058..d6a2fe1e8 100644 --- a/blockchain/v2/processor.go +++ b/blockchain/v2/processor.go @@ -4,23 +4,12 @@ import ( "fmt" "github.com/tendermint/tendermint/p2p" - tdState "github.com/tendermint/tendermint/state" + tmState "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) -type peerError struct { - priorityHigh - peerID p2p.ID -} - -type pcDuplicateBlock struct { - priorityNormal -} - -type pcShortBlock struct { - priorityNormal -} - +// Events generated by the processor: +// block execution failure, event will indicate the peer(s) that caused the error type pcBlockVerificationFailure struct { priorityNormal height int64 @@ -28,24 +17,18 @@ type pcBlockVerificationFailure struct { secondPeerID p2p.ID } +// successful block execution type pcBlockProcessed struct { priorityNormal height int64 peerID p2p.ID } -type pcProcessBlock struct { - priorityNormal -} - -type pcStop struct { - priorityNormal -} - +// processor has finished type pcFinished struct { priorityNormal - height int64 - blocksSynced int64 + blocksSynced int + tmState tmState.State } func (p pcFinished) Error() string { @@ -60,37 +43,38 @@ type queueItem struct { type blockQueue map[int64]queueItem type pcState struct { - height int64 // height of the last synced block - queue blockQueue // blocks waiting to be processed - chainID string - blocksSynced int64 - draining bool - tdState tdState.State - context processorContext + // blocks waiting to be processed + queue blockQueue + + // draining indicates that the next rProcessBlock event with a queue miss constitutes completion + draining bool + + // the number of blocks successfully synced by the processor + blocksSynced int + + // the processorContext which contains the processor dependencies + context processorContext } func (state *pcState) String() string { return fmt.Sprintf("height: %d queue length: %d draining: %v blocks synced: %d", - state.height, len(state.queue), state.draining, state.blocksSynced) + state.height(), len(state.queue), state.draining, state.blocksSynced) } // newPcState returns a pcState initialized with the last verified block enqueued -func newPcState(initHeight int64, tdState tdState.State, chainID string, context processorContext) *pcState { +func newPcState(context processorContext) *pcState { return &pcState{ - height: initHeight, queue: blockQueue{}, - chainID: chainID, draining: false, blocksSynced: 0, context: context, - tdState: tdState, } } // nextTwo returns the next two unverified blocks func (state *pcState) nextTwo() (queueItem, queueItem, error) { - if first, ok := state.queue[state.height+1]; ok { - if second, ok := state.queue[state.height+2]; ok { + if first, ok := state.queue[state.height()+1]; ok { + if second, ok := state.queue[state.height()+2]; ok { return first, second, nil } } @@ -102,18 +86,15 @@ func (state *pcState) synced() bool { return len(state.queue) <= 1 } -func (state *pcState) advance() { - state.height++ - delete(state.queue, state.height) - state.blocksSynced++ -} - -func (state *pcState) enqueue(peerID p2p.ID, block *types.Block, height int64) error { +func (state *pcState) enqueue(peerID p2p.ID, block *types.Block, height int64) { if _, ok := state.queue[height]; ok { - return fmt.Errorf("duplicate queue item") + panic("duplicate block enqueued by processor") } state.queue[height] = queueItem{block: block, peerID: peerID} - return nil +} + +func (state *pcState) height() int64 { + return state.context.tmState().LastBlockHeight } // purgePeer moves all unprocessed blocks from the queue @@ -129,23 +110,34 @@ func (state *pcState) purgePeer(peerID p2p.ID) { // handle processes FSM events func (state *pcState) handle(event Event) (Event, error) { switch event := event.(type) { - case *scBlockReceived: + case scFinishedEv: + if state.synced() { + return pcFinished{tmState: state.context.tmState(), blocksSynced: state.blocksSynced}, nil + } + state.draining = true + return noOp, nil + + case scPeerError: + state.purgePeer(event.peerID) + return noOp, nil + + case scBlockReceived: if event.block == nil { - panic("processor received an event with a nil block") - } - if event.block.Height <= state.height { - return pcShortBlock{}, nil - } - err := state.enqueue(event.peerID, event.block, event.block.Height) - if err != nil { - return pcDuplicateBlock{}, nil + return noOp, nil } - case pcProcessBlock: + // enqueue block if height is higher than state height, else ignore it + if event.block.Height > state.height() { + state.enqueue(event.peerID, event.block, event.block.Height) + } + return noOp, nil + + case rProcessBlock: + tmState := state.context.tmState() firstItem, secondItem, err := state.nextTwo() if err != nil { if state.draining { - return noOp, pcFinished{height: state.height} + return pcFinished{tmState: tmState, blocksSynced: state.blocksSynced}, nil } return noOp, nil } @@ -155,7 +147,7 @@ func (state *pcState) handle(event Event) (Event, error) { firstPartsHeader := firstParts.Header() firstID := types.BlockID{Hash: first.Hash(), PartsHeader: firstPartsHeader} - err = state.context.verifyCommit(state.chainID, firstID, first.Height, second.LastCommit) + err = state.context.verifyCommit(tmState.ChainID, firstID, first.Height, second.LastCommit) if err != nil { state.purgePeer(firstItem.peerID) state.purgePeer(secondItem.peerID) @@ -166,21 +158,15 @@ func (state *pcState) handle(event Event) (Event, error) { state.context.saveBlock(first, firstParts, second.LastCommit) - state.tdState, err = state.context.applyBlock(state.tdState, firstID, first) - if err != nil { + if err := state.context.applyBlock(firstID, first); err != nil { panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) } - state.advance() + + delete(state.queue, first.Height) + state.blocksSynced++ + return pcBlockProcessed{height: first.Height, peerID: firstItem.peerID}, nil - case *peerError: - state.purgePeer(event.peerID) - - case pcStop: - if state.synced() { - return noOp, pcFinished{height: state.height, blocksSynced: state.blocksSynced} - } - state.draining = true } return noOp, nil diff --git a/blockchain/v2/processor_context.go b/blockchain/v2/processor_context.go index c4c8770cd..7e96a3a69 100644 --- a/blockchain/v2/processor_context.go +++ b/blockchain/v2/processor_context.go @@ -4,37 +4,41 @@ import ( "fmt" "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) type processorContext interface { - applyBlock(state state.State, blockID types.BlockID, block *types.Block) (state.State, error) + applyBlock(blockID types.BlockID, block *types.Block) error verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error saveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) + tmState() state.State } -// nolint:unused type pContext struct { - store *store.BlockStore - executor *state.BlockExecutor - state *state.State + store blockStore + applier blockApplier + state state.State } -// nolint:unused,deadcode -func newProcessorContext(st *store.BlockStore, ex *state.BlockExecutor, s *state.State) *pContext { +func newProcessorContext(st blockStore, ex blockApplier, s state.State) *pContext { return &pContext{ - store: st, - executor: ex, - state: s, + store: st, + applier: ex, + state: s, } } -func (pc *pContext) applyBlock(state state.State, blockID types.BlockID, block *types.Block) (state.State, error) { - return pc.executor.ApplyBlock(state, blockID, block) +func (pc *pContext) applyBlock(blockID types.BlockID, block *types.Block) error { + newState, err := pc.applier.ApplyBlock(pc.state, blockID, block) + pc.state = newState + return err } -func (pc *pContext) verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error { +func (pc pContext) tmState() state.State { + return pc.state +} + +func (pc pContext) verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error { return pc.state.Validators.VerifyCommit(chainID, blockID, height, commit) } @@ -45,22 +49,28 @@ func (pc *pContext) saveBlock(block *types.Block, blockParts *types.PartSet, see type mockPContext struct { applicationBL []int64 verificationBL []int64 + state state.State } -func newMockProcessorContext(verificationBlackList []int64, applicationBlackList []int64) *mockPContext { +func newMockProcessorContext( + state state.State, + verificationBlackList []int64, + applicationBlackList []int64) *mockPContext { return &mockPContext{ applicationBL: applicationBlackList, verificationBL: verificationBlackList, + state: state, } } -func (mpc *mockPContext) applyBlock(state state.State, blockID types.BlockID, block *types.Block) (state.State, error) { +func (mpc *mockPContext) applyBlock(blockID types.BlockID, block *types.Block) error { for _, h := range mpc.applicationBL { if h == block.Height { - return state, fmt.Errorf("generic application error") + return fmt.Errorf("generic application error") } } - return state, nil + mpc.state.LastBlockHeight = block.Height + return nil } func (mpc *mockPContext) verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error { @@ -73,4 +83,9 @@ func (mpc *mockPContext) verifyCommit(chainID string, blockID types.BlockID, hei } func (mpc *mockPContext) saveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { + +} + +func (mpc *mockPContext) tmState() state.State { + return mpc.state } diff --git a/blockchain/v2/processor_test.go b/blockchain/v2/processor_test.go index 61be23663..fc35c4c72 100644 --- a/blockchain/v2/processor_test.go +++ b/blockchain/v2/processor_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/tendermint/tendermint/p2p" - tdState "github.com/tendermint/tendermint/state" + tmState "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -19,7 +19,7 @@ type pcBlock struct { type params struct { height int64 items []pcBlock - blocksSynced int64 + blocksSynced int verBL []int64 appBL []int64 draining bool @@ -33,13 +33,13 @@ func makePcBlock(height int64) *types.Block { // makeState takes test parameters and creates a specific processor state. func makeState(p *params) *pcState { var ( - tdState = tdState.State{} - context = newMockProcessorContext(p.verBL, p.appBL) + tmState = tmState.State{LastBlockHeight: p.height} + context = newMockProcessorContext(tmState, p.verBL, p.appBL) ) - state := newPcState(p.height, tdState, "test", context) + state := newPcState(context) for _, item := range p.items { - _ = state.enqueue(p2p.ID(item.pid), makePcBlock(item.height), item.height) + state.enqueue(p2p.ID(item.pid), makePcBlock(item.height), item.height) } state.blocksSynced = p.blocksSynced @@ -47,8 +47,8 @@ func makeState(p *params) *pcState { return state } -func mBlockResponse(peerID p2p.ID, height int64) *scBlockReceived { - return &scBlockReceived{ +func mBlockResponse(peerID p2p.ID, height int64) scBlockReceived { + return scBlockReceived{ peerID: peerID, block: makePcBlock(height), } @@ -101,9 +101,37 @@ func executeProcessorTests(t *testing.T, tests []testFields) { } } +func TestRProcessPeerError(t *testing.T) { + tests := []testFields{ + { + name: "error for existing peer", + steps: []pcFsmMakeStateValues{ + { + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, + event: scPeerError{peerID: "P2"}, + wantState: ¶ms{items: []pcBlock{{"P1", 1}}}, + wantNextEvent: noOp, + }, + }, + }, + { + name: "error for unknown peer", + steps: []pcFsmMakeStateValues{ + { + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, + event: scPeerError{peerID: "P3"}, + wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, + wantNextEvent: noOp, + }, + }, + }, + } + + executeProcessorTests(t, tests) +} + func TestPcBlockResponse(t *testing.T) { tests := []testFields{ - { name: "add one block", steps: []pcFsmMakeStateValues{ @@ -113,6 +141,7 @@ func TestPcBlockResponse(t *testing.T) { }, }, }, + { name: "add two blocks", steps: []pcFsmMakeStateValues{ @@ -126,62 +155,18 @@ func TestPcBlockResponse(t *testing.T) { }, }, }, - { - name: "add duplicate block from same peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{}, event: mBlockResponse("P1", 3), - wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, - }, - { // use previous wantState as currentState, - event: mBlockResponse("P1", 3), - wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcDuplicateBlock{}, - }, - }, - }, - { - name: "add duplicate block from different peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{}, event: mBlockResponse("P1", 3), - wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, - }, - { // use previous wantState as currentState, - event: mBlockResponse("P2", 3), - wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcDuplicateBlock{}, - }, - }, - }, - { - name: "attempt to add block with height equal to state.height", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, event: mBlockResponse("P1", 2), - wantState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcShortBlock{}, - }, - }, - }, - { - name: "attempt to add block with height smaller than state.height", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, event: mBlockResponse("P1", 1), - wantState: ¶ms{height: 2, items: []pcBlock{{"P1", 3}}}, wantNextEvent: pcShortBlock{}, - }, - }, - }, } executeProcessorTests(t, tests) } -func TestPcProcessBlockSuccess(t *testing.T) { +func TestRProcessBlockSuccess(t *testing.T) { tests := []testFields{ { name: "noop - no blocks over current height", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{}, event: pcProcessBlock{}, + currentState: ¶ms{}, event: rProcessBlock{}, wantState: ¶ms{}, wantNextEvent: noOp, }, }, @@ -190,7 +175,7 @@ func TestPcProcessBlockSuccess(t *testing.T) { name: "noop - high new blocks", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, event: pcProcessBlock{}, + currentState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, event: rProcessBlock{}, wantState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, wantNextEvent: noOp, }, }, @@ -199,7 +184,7 @@ func TestPcProcessBlockSuccess(t *testing.T) { name: "blocks H+1 and H+2 present", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: pcProcessBlock{}, + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: rProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}}, blocksSynced: 1}, wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, }, @@ -209,20 +194,20 @@ func TestPcProcessBlockSuccess(t *testing.T) { name: "blocks H+1 and H+2 present after draining", steps: []pcFsmMakeStateValues{ { // some contiguous blocks - on stop check draining is set - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}}, event: pcStop{}, + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}}, + event: scFinishedEv{}, wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}, draining: true}, wantNextEvent: noOp, }, { - event: pcProcessBlock{}, + event: rProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, }, { // finish when H+1 or/and H+2 are missing - event: pcProcessBlock{}, + event: rProcessBlock{}, wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, - wantNextEvent: noOp, - wantErr: pcFinished{height: 1}, + wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 1}, blocksSynced: 1}, }, }, }, @@ -231,13 +216,13 @@ func TestPcProcessBlockSuccess(t *testing.T) { executeProcessorTests(t, tests) } -func TestPcProcessBlockFailures(t *testing.T) { +func TestRProcessBlockFailures(t *testing.T) { tests := []testFields{ { name: "blocks H+1 and H+2 present from different peers - H+1 verification fails ", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, verBL: []int64{1}}, event: pcProcessBlock{}, + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, verBL: []int64{1}}, event: rProcessBlock{}, wantState: ¶ms{items: []pcBlock{}, verBL: []int64{1}}, wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P2"}, }, @@ -247,7 +232,7 @@ func TestPcProcessBlockFailures(t *testing.T) { name: "blocks H+1 and H+2 present from same peer - H+1 applyBlock fails ", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, appBL: []int64{1}}, event: pcProcessBlock{}, + currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, appBL: []int64{1}}, event: rProcessBlock{}, wantState: ¶ms{items: []pcBlock{}, appBL: []int64{1}}, wantPanic: true, }, }, @@ -256,9 +241,9 @@ func TestPcProcessBlockFailures(t *testing.T) { name: "blocks H+1 and H+2 present from same peers - H+1 verification fails ", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P1", 2}, {"P2", 3}}, verBL: []int64{1}}, - event: pcProcessBlock{}, - wantState: ¶ms{items: []pcBlock{{"P2", 3}}, verBL: []int64{1}}, + currentState: ¶ms{height: 0, items: []pcBlock{{"P1", 1}, {"P1", 2}, {"P2", 3}}, + verBL: []int64{1}}, event: rProcessBlock{}, + wantState: ¶ms{height: 0, items: []pcBlock{{"P2", 3}}, verBL: []int64{1}}, wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}, }, }, @@ -268,7 +253,7 @@ func TestPcProcessBlockFailures(t *testing.T) { steps: []pcFsmMakeStateValues{ { currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P2", 3}}, appBL: []int64{1}}, - event: pcProcessBlock{}, + event: rProcessBlock{}, wantState: ¶ms{items: []pcBlock{{"P2", 3}}, appBL: []int64{1}}, wantPanic: true, }, }, @@ -278,53 +263,15 @@ func TestPcProcessBlockFailures(t *testing.T) { executeProcessorTests(t, tests) } -func TestPcPeerError(t *testing.T) { - tests := []testFields{ - { - name: "peer not present", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: &peerError{peerID: "P3"}, - wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, - wantNextEvent: noOp, - }, - }, - }, - { - name: "some blocks are from errored peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 100}, {"P1", 99}, {"P2", 101}}}, event: &peerError{peerID: "P1"}, - wantState: ¶ms{items: []pcBlock{{"P2", 101}}}, - wantNextEvent: noOp, - }, - }, - }, - { - name: "all blocks are from errored peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 100}, {"P1", 99}}}, event: &peerError{peerID: "P1"}, - wantState: ¶ms{}, - wantNextEvent: noOp, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} - -func TestStop(t *testing.T) { +func TestScFinishedEv(t *testing.T) { tests := []testFields{ { name: "no blocks", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, event: pcStop{}, + currentState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, event: scFinishedEv{}, wantState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, - wantNextEvent: noOp, - wantErr: pcFinished{height: 100, blocksSynced: 100}, + wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 100}, blocksSynced: 100}, }, }, }, @@ -332,10 +279,10 @@ func TestStop(t *testing.T) { name: "maxHeight+1 block present", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}}, blocksSynced: 100}, event: pcStop{}, + currentState: ¶ms{height: 100, items: []pcBlock{ + {"P1", 101}}, blocksSynced: 100}, event: scFinishedEv{}, wantState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}}, blocksSynced: 100}, - wantNextEvent: noOp, - wantErr: pcFinished{height: 100, blocksSynced: 100}, + wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 100}, blocksSynced: 100}, }, }, }, @@ -343,8 +290,10 @@ func TestStop(t *testing.T) { name: "more blocks present", steps: []pcFsmMakeStateValues{ { - currentState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}, {"P1", 102}}, blocksSynced: 100}, event: pcStop{}, - wantState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}, {"P1", 102}}, blocksSynced: 100, draining: true}, + currentState: ¶ms{height: 100, items: []pcBlock{ + {"P1", 101}, {"P1", 102}}, blocksSynced: 100}, event: scFinishedEv{}, + wantState: ¶ms{height: 100, items: []pcBlock{ + {"P1", 101}, {"P1", 102}}, blocksSynced: 100, draining: true}, wantNextEvent: noOp, wantErr: nil, }, diff --git a/blockchain/v2/reactor.go b/blockchain/v2/reactor.go index 8f7143083..767e59819 100644 --- a/blockchain/v2/reactor.go +++ b/blockchain/v2/reactor.go @@ -1,118 +1,529 @@ package v2 import ( + "errors" "fmt" + "sync" "time" + "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/behaviour" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" ) -type timeCheck struct { - priorityHigh - time time.Time +//------------------------------------- + +type bcBlockRequestMessage struct { + Height int64 } -func schedulerHandle(event Event) (Event, error) { - if _, ok := event.(timeCheck); ok { - fmt.Println("scheduler handle timeCheck") +// ValidateBasic performs basic validation. +func (m *bcBlockRequestMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") } - return noOp, nil + return nil } -func processorHandle(event Event) (Event, error) { - if _, ok := event.(timeCheck); ok { - fmt.Println("processor handle timeCheck") +func (m *bcBlockRequestMessage) String() string { + return fmt.Sprintf("[bcBlockRequestMessage %v]", m.Height) +} + +type bcNoBlockResponseMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcNoBlockResponseMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") } - return noOp, nil - + return nil } -type Reactor struct { - events chan Event +func (m *bcNoBlockResponseMessage) String() string { + return fmt.Sprintf("[bcNoBlockResponseMessage %d]", m.Height) +} + +//------------------------------------- + +type bcBlockResponseMessage struct { + Block *types.Block +} + +// ValidateBasic performs basic validation. +func (m *bcBlockResponseMessage) ValidateBasic() error { + if m.Block == nil { + return errors.New("block response message has nil block") + } + + return m.Block.ValidateBasic() +} + +func (m *bcBlockResponseMessage) String() string { + return fmt.Sprintf("[bcBlockResponseMessage %v]", m.Block.Height) +} + +//------------------------------------- + +type bcStatusRequestMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcStatusRequestMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcStatusRequestMessage) String() string { + return fmt.Sprintf("[bcStatusRequestMessage %v]", m.Height) +} + +//------------------------------------- + +type bcStatusResponseMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcStatusResponseMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcStatusResponseMessage) String() string { + return fmt.Sprintf("[bcStatusResponseMessage %v]", m.Height) +} + +type blockStore interface { + LoadBlock(height int64) *types.Block + SaveBlock(*types.Block, *types.PartSet, *types.Commit) + Height() int64 +} + +// BlockchainReactor handles fast sync protocol. +type BlockchainReactor struct { + p2p.BaseReactor + + events chan Event // XXX: Rename eventsFromPeers stopDemux chan struct{} scheduler *Routine processor *Routine - ticker *time.Ticker logger log.Logger + + mtx sync.RWMutex + maxPeerHeight int64 + syncHeight int64 + + reporter behaviour.Reporter + io iIO + store blockStore } -func NewReactor(bufferSize int) *Reactor { - return &Reactor{ +//nolint:unused,deadcode +type blockVerifier interface { + VerifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error +} + +//nolint:deadcode +type blockApplier interface { + ApplyBlock(state state.State, blockID types.BlockID, block *types.Block) (state.State, error) +} + +// XXX: unify naming in this package around tmState +// XXX: V1 stores a copy of state as initialState, which is never mutated. Is that nessesary? +func newReactor(state state.State, store blockStore, reporter behaviour.Reporter, + blockApplier blockApplier, bufferSize int) *BlockchainReactor { + scheduler := newScheduler(state.LastBlockHeight, time.Now()) + pContext := newProcessorContext(store, blockApplier, state) + // TODO: Fix naming to just newProcesssor + // newPcState requires a processorContext + processor := newPcState(pContext) + + return &BlockchainReactor{ events: make(chan Event, bufferSize), stopDemux: make(chan struct{}), - scheduler: newRoutine("scheduler", schedulerHandle, bufferSize), - processor: newRoutine("processor", processorHandle, bufferSize), - ticker: time.NewTicker(1 * time.Second), + scheduler: newRoutine("scheduler", scheduler.handle, bufferSize), + processor: newRoutine("processor", processor.handle, bufferSize), + store: store, + reporter: reporter, logger: log.NewNopLogger(), } } -// nolint:unused -func (r *Reactor) setLogger(logger log.Logger) { +// NewBlockchainReactor creates a new reactor instance. +func NewBlockchainReactor( + state state.State, + blockApplier blockApplier, + store blockStore, + fastSync bool) *BlockchainReactor { + reporter := behaviour.NewMockReporter() + return newReactor(state, store, reporter, blockApplier, 1000) +} + +// SetSwitch implements Reactor interface. +func (r *BlockchainReactor) SetSwitch(sw *p2p.Switch) { + if sw == nil { + panic("set nil switch") + } + + r.Switch = sw + r.io = newSwitchIo(sw) +} + +func (r *BlockchainReactor) setMaxPeerHeight(height int64) { + r.mtx.Lock() + defer r.mtx.Unlock() + if height > r.maxPeerHeight { + r.maxPeerHeight = height + } +} + +func (r *BlockchainReactor) setSyncHeight(height int64) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.syncHeight = height +} + +// SyncHeight returns the height to which the BlockchainReactor has synced. +func (r *BlockchainReactor) SyncHeight() int64 { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.syncHeight +} + +// SetLogger sets the logger of the reactor. +func (r *BlockchainReactor) SetLogger(logger log.Logger) { r.logger = logger r.scheduler.setLogger(logger) r.processor.setLogger(logger) } -func (r *Reactor) Start() { +// Start implements cmn.Service interface +func (r *BlockchainReactor) Start() error { + r.reporter = behaviour.NewSwitchReporter(r.BaseReactor.Switch) go r.scheduler.start() go r.processor.start() go r.demux() - - <-r.scheduler.ready() - <-r.processor.ready() - - go func() { - for t := range r.ticker.C { - r.events <- timeCheck{time: t} - } - }() + return nil } -// XXX: How to make this deterministic? -// XXX: Would it be possible here to provide some kind of type safety for the types -// of events that each routine can produce and consume? -func (r *Reactor) demux() { +// reactor generated ticker events: +// ticker for cleaning peers +type rTryPrunePeer struct { + priorityHigh + time time.Time +} + +func (e rTryPrunePeer) String() string { + return fmt.Sprintf(": %v", e.time) +} + +// ticker event for scheduling block requests +type rTrySchedule struct { + priorityHigh + time time.Time +} + +func (e rTrySchedule) String() string { + return fmt.Sprintf(": %v", e.time) +} + +// ticker for block processing +type rProcessBlock struct { + priorityNormal +} + +// reactor generated events based on blockchain related messages from peers: +// blockResponse message received from a peer +type bcBlockResponse struct { + priorityNormal + time time.Time + peerID p2p.ID + size int64 + block *types.Block +} + +// blockNoResponse message received from a peer +type bcNoBlockResponse struct { + priorityNormal + time time.Time + peerID p2p.ID + height int64 +} + +// statusResponse message received from a peer +type bcStatusResponse struct { + priorityNormal + time time.Time + peerID p2p.ID + height int64 +} + +// new peer is connected +type bcAddNewPeer struct { + priorityNormal + peerID p2p.ID +} + +// existing peer is removed +type bcRemovePeer struct { + priorityHigh + peerID p2p.ID + reason interface{} +} + +func (r *BlockchainReactor) demux() { + var lastRate = 0.0 + var lastHundred = time.Now() + + var ( + processBlockFreq = 20 * time.Millisecond + doProcessBlockCh = make(chan struct{}, 1) + doProcessBlockTk = time.NewTicker(processBlockFreq) + + prunePeerFreq = 1 * time.Second + doPrunePeerCh = make(chan struct{}, 1) + doPrunePeerTk = time.NewTicker(prunePeerFreq) + + scheduleFreq = 20 * time.Millisecond + doScheduleCh = make(chan struct{}, 1) + doScheduleTk = time.NewTicker(scheduleFreq) + + statusFreq = 10 * time.Second + doStatusCh = make(chan struct{}, 1) + doStatusTk = time.NewTicker(statusFreq) + ) + + // XXX: Extract timers to make testing atemporal for { select { + // Pacers: send at most per frequency but don't saturate + case <-doProcessBlockTk.C: + select { + case doProcessBlockCh <- struct{}{}: + default: + } + case <-doPrunePeerTk.C: + select { + case doPrunePeerCh <- struct{}{}: + default: + } + case <-doScheduleTk.C: + select { + case doScheduleCh <- struct{}{}: + default: + } + case <-doStatusTk.C: + select { + case doStatusCh <- struct{}{}: + default: + } + + // Tickers: perform tasks periodically + case <-doScheduleCh: + r.scheduler.send(rTrySchedule{time: time.Now()}) + case <-doPrunePeerCh: + r.scheduler.send(rTryPrunePeer{time: time.Now()}) + case <-doProcessBlockCh: + r.processor.send(rProcessBlock{}) + case <-doStatusCh: + r.io.broadcastStatusRequest(r.SyncHeight()) + + // Events from peers case event := <-r.events: - // XXX: check for backpressure - r.scheduler.send(event) - r.processor.send(event) + switch event := event.(type) { + case bcStatusResponse: + r.setMaxPeerHeight(event.height) + r.scheduler.send(event) + case bcAddNewPeer, bcRemovePeer, bcBlockResponse, bcNoBlockResponse: + r.scheduler.send(event) + } + + // Incremental events form scheduler + case event := <-r.scheduler.next(): + switch event := event.(type) { + case scBlockReceived: + r.processor.send(event) + case scPeerError: + r.processor.send(event) + r.reporter.Report(behaviour.BadMessage(event.peerID, "scPeerError")) + case scBlockRequest: + r.io.sendBlockRequest(event.peerID, event.height) + case scFinishedEv: + r.processor.send(event) + r.scheduler.stop() + } + + // Incremental events from processor + case event := <-r.processor.next(): + switch event := event.(type) { + case pcBlockProcessed: + r.setSyncHeight(event.height) + if r.syncHeight%100 == 0 { + lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) + r.logger.Info("Fast Syncc Rate", "height", r.syncHeight, + "max_peer_height", r.maxPeerHeight, "blocks/s", lastRate) + lastHundred = time.Now() + } + r.scheduler.send(event) + case pcBlockVerificationFailure: + r.scheduler.send(event) + case pcFinished: + r.io.trySwitchToConsensus(event.tmState, event.blocksSynced) + r.processor.stop() + } + + // Terminal events from scheduler + case err := <-r.scheduler.final(): + r.logger.Info(fmt.Sprintf("scheduler final %s", err)) + // send the processor stop? + + // Terminal event from processor + case event := <-r.processor.final(): + r.logger.Info(fmt.Sprintf("processor final %s", event)) + case <-r.stopDemux: r.logger.Info("demuxing stopped") return - case event := <-r.scheduler.next(): - r.processor.send(event) - case event := <-r.processor.next(): - r.scheduler.send(event) - case err := <-r.scheduler.final(): - r.logger.Info(fmt.Sprintf("scheduler final %s", err)) - case err := <-r.processor.final(): - r.logger.Info(fmt.Sprintf("processor final %s", err)) - // XXX: switch to consensus } } } -func (r *Reactor) Stop() { +// Stop implements cmn.Service interface. +func (r *BlockchainReactor) Stop() error { r.logger.Info("reactor stopping") - r.ticker.Stop() r.scheduler.stop() r.processor.stop() close(r.stopDemux) close(r.events) r.logger.Info("reactor stopped") + return nil } -func (r *Reactor) Receive(event Event) { - // XXX: decode and serialize write events - // TODO: backpressure +const ( + // NOTE: keep up to date with bcBlockResponseMessage + bcBlockResponseMessagePrefixSize = 4 + bcBlockResponseMessageFieldKeySize = 1 + maxMsgSize = types.MaxBlockSizeBytes + + bcBlockResponseMessagePrefixSize + + bcBlockResponseMessageFieldKeySize +) + +// BlockchainMessage is a generic message for this reactor. +type BlockchainMessage interface { + ValidateBasic() error +} + +// RegisterBlockchainMessages registers the fast sync messages for amino encoding. +func RegisterBlockchainMessages(cdc *amino.Codec) { + cdc.RegisterInterface((*BlockchainMessage)(nil), nil) + cdc.RegisterConcrete(&bcBlockRequestMessage{}, "tendermint/blockchain/BlockRequest", nil) + cdc.RegisterConcrete(&bcBlockResponseMessage{}, "tendermint/blockchain/BlockResponse", nil) + cdc.RegisterConcrete(&bcNoBlockResponseMessage{}, "tendermint/blockchain/NoBlockResponse", nil) + cdc.RegisterConcrete(&bcStatusResponseMessage{}, "tendermint/blockchain/StatusResponse", nil) + cdc.RegisterConcrete(&bcStatusRequestMessage{}, "tendermint/blockchain/StatusRequest", nil) +} + +func decodeMsg(bz []byte) (msg BlockchainMessage, err error) { + if len(bz) > maxMsgSize { + return msg, fmt.Errorf("msg exceeds max size (%d > %d)", len(bz), maxMsgSize) + } + err = cdc.UnmarshalBinaryBare(bz, &msg) + return +} + +// Receive implements Reactor by handling different message types. +func (r *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := decodeMsg(msgBytes) + if err != nil { + r.logger.Error("error decoding message", + "src", src.ID(), "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + _ = r.reporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + if err = msg.ValidateBasic(); err != nil { + r.logger.Error("peer sent us invalid msg", "peer", src, "msg", msg, "err", err) + _ = r.reporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + r.logger.Debug("Receive", "src", src.ID(), "chID", chID, "msg", msg) + + switch msg := msg.(type) { + case *bcStatusRequestMessage: + if err := r.io.sendStatusResponse(r.store.Height(), src.ID()); err != nil { + r.logger.Error("Could not send status message to peer", "src", src) + } + + case *bcBlockRequestMessage: + block := r.store.LoadBlock(msg.Height) + if block != nil { + if err = r.io.sendBlockToPeer(block, src.ID()); err != nil { + r.logger.Error("Could not send block message to peer: ", err) + } + } else { + r.logger.Info("peer asking for a block we don't have", "src", src, "height", msg.Height) + peerID := src.ID() + if err = r.io.sendBlockNotFound(msg.Height, peerID); err != nil { + r.logger.Error("Couldn't send block not found: ", err) + } + } + + case *bcStatusResponseMessage: + r.events <- bcStatusResponse{peerID: src.ID(), height: msg.Height} + + case *bcBlockResponseMessage: + r.events <- bcBlockResponse{ + peerID: src.ID(), + block: msg.Block, + size: int64(len(msgBytes)), + time: time.Now(), + } + + case *bcNoBlockResponseMessage: + r.events <- bcNoBlockResponse{peerID: src.ID(), height: msg.Height, time: time.Now()} + } +} + +// AddPeer implements Reactor interface +func (r *BlockchainReactor) AddPeer(peer p2p.Peer) { + err := r.io.sendStatusResponse(r.store.Height(), peer.ID()) + if err != nil { + r.logger.Error("Could not send status message to peer new", "src", peer.ID, "height", r.SyncHeight()) + } + r.events <- bcAddNewPeer{peerID: peer.ID()} +} + +// RemovePeer implements Reactor interface. +func (r *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { + event := bcRemovePeer{ + peerID: peer.ID(), + reason: reason, + } r.events <- event } -func (r *Reactor) AddPeer() { - // TODO: add peer event and send to demuxer +// GetChannels implements Reactor +func (r *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { + return []*p2p.ChannelDescriptor{ + { + ID: BlockchainChannel, + Priority: 10, + SendQueueCapacity: 2000, + RecvBufferCapacity: 50 * 4096, + RecvMessageCapacity: maxMsgSize, + }, + } } diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go index 46a2e60c6..10457cdb0 100644 --- a/blockchain/v2/reactor_test.go +++ b/blockchain/v2/reactor_test.go @@ -1,22 +1,517 @@ package v2 import ( + "net" + "os" + "sort" + "sync" "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/behaviour" + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/service" + "github.com/tendermint/tendermint/mock" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/conn" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-db" ) -func TestReactor(t *testing.T) { +type mockPeer struct { + service.Service + id p2p.ID +} + +func (mp mockPeer) FlushStop() {} +func (mp mockPeer) ID() p2p.ID { return mp.id } +func (mp mockPeer) RemoteIP() net.IP { return net.IP{} } +func (mp mockPeer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.RemoteIP(), Port: 8800} } + +func (mp mockPeer) IsOutbound() bool { return true } +func (mp mockPeer) IsPersistent() bool { return true } +func (mp mockPeer) CloseConn() error { return nil } + +func (mp mockPeer) NodeInfo() p2p.NodeInfo { + return p2p.DefaultNodeInfo{ + DefaultNodeID: "", + ListenAddr: "", + } +} +func (mp mockPeer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } +func (mp mockPeer) SocketAddr() *p2p.NetAddress { return &p2p.NetAddress{} } + +func (mp mockPeer) Send(byte, []byte) bool { return true } +func (mp mockPeer) TrySend(byte, []byte) bool { return true } + +func (mp mockPeer) Set(string, interface{}) {} +func (mp mockPeer) Get(string) interface{} { return struct{}{} } + +//nolint:unused +type mockBlockStore struct { + blocks map[int64]*types.Block +} + +func (ml *mockBlockStore) Height() int64 { + return int64(len(ml.blocks)) +} + +func (ml *mockBlockStore) LoadBlock(height int64) *types.Block { + return ml.blocks[height] +} + +func (ml *mockBlockStore) SaveBlock(block *types.Block, part *types.PartSet, commit *types.Commit) { + ml.blocks[block.Height] = block +} + +type mockBlockApplier struct { +} + +// XXX: Add whitelist/blacklist? +func (mba *mockBlockApplier) ApplyBlock(state sm.State, blockID types.BlockID, block *types.Block) (sm.State, error) { + state.LastBlockHeight++ + return state, nil +} + +type mockSwitchIo struct { + mtx sync.Mutex + switchedToConsensus bool + numStatusResponse int + numBlockResponse int + numNoBlockResponse int +} + +func (sio *mockSwitchIo) sendBlockRequest(peerID p2p.ID, height int64) error { + return nil +} + +func (sio *mockSwitchIo) sendStatusResponse(height int64, peerID p2p.ID) error { + sio.mtx.Lock() + defer sio.mtx.Unlock() + sio.numStatusResponse++ + return nil +} + +func (sio *mockSwitchIo) sendBlockToPeer(block *types.Block, peerID p2p.ID) error { + sio.mtx.Lock() + defer sio.mtx.Unlock() + sio.numBlockResponse++ + return nil +} + +func (sio *mockSwitchIo) sendBlockNotFound(height int64, peerID p2p.ID) error { + sio.mtx.Lock() + defer sio.mtx.Unlock() + sio.numNoBlockResponse++ + return nil +} + +func (sio *mockSwitchIo) trySwitchToConsensus(state sm.State, blocksSynced int) { + sio.mtx.Lock() + defer sio.mtx.Unlock() + sio.switchedToConsensus = true +} + +func (sio *mockSwitchIo) hasSwitchedToConsensus() bool { + sio.mtx.Lock() + defer sio.mtx.Unlock() + return sio.switchedToConsensus +} + +func (sio *mockSwitchIo) broadcastStatusRequest(height int64) { +} + +type testReactorParams struct { + logger log.Logger + genDoc *types.GenesisDoc + privVals []types.PrivValidator + startHeight int64 + bufferSize int + mockA bool +} + +func newTestReactor(p testReactorParams) *BlockchainReactor { + store, state, _ := newReactorStore(p.genDoc, p.privVals, p.startHeight) + reporter := behaviour.NewMockReporter() + + var appl blockApplier + + if p.mockA { + appl = &mockBlockApplier{} + } else { + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(cc) + err := proxyApp.Start() + if err != nil { + panic(errors.Wrap(err, "error start app")) + } + db := dbm.NewMemDB() + appl = sm.NewBlockExecutor(db, p.logger, proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + sm.SaveState(db, state) + } + + r := newReactor(state, store, reporter, appl, p.bufferSize) + logger := log.TestingLogger() + r.SetLogger(logger.With("module", "blockchain")) + + return r +} + +func TestReactorTerminationScenarios(t *testing.T) { + + config := cfg.ResetTestRoot("blockchain_reactor_v2_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) + refStore, _, _ := newReactorStore(genDoc, privVals, 20) + + params := testReactorParams{ + logger: log.TestingLogger(), + genDoc: genDoc, + privVals: privVals, + startHeight: 10, + bufferSize: 100, + mockA: true, + } + + type testEvent struct { + evType string + peer string + height int64 + } + + tests := []struct { + name string + params testReactorParams + msgs []testEvent + }{ + { + name: "simple termination on max peer height - one peer", + params: params, + msgs: []testEvent{ + {evType: "AddPeer", peer: "P1"}, + {evType: "ReceiveS", peer: "P1", height: 13}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P1", height: 11}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P1", height: 12}, + {evType: "Process"}, + {evType: "ReceiveB", peer: "P1", height: 13}, + {evType: "Process"}, + }, + }, + { + name: "simple termination on max peer height - two peers", + params: params, + msgs: []testEvent{ + {evType: "AddPeer", peer: "P1"}, + {evType: "AddPeer", peer: "P2"}, + {evType: "ReceiveS", peer: "P1", height: 13}, + {evType: "ReceiveS", peer: "P2", height: 15}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P1", height: 11}, + {evType: "ReceiveB", peer: "P2", height: 12}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P1", height: 13}, + {evType: "Process"}, + {evType: "ReceiveB", peer: "P2", height: 14}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 15}, + {evType: "Process"}, + }, + }, + { + name: "termination on max peer height - two peers, noBlock error", + params: params, + msgs: []testEvent{ + {evType: "AddPeer", peer: "P1"}, + {evType: "AddPeer", peer: "P2"}, + {evType: "ReceiveS", peer: "P1", height: 13}, + {evType: "ReceiveS", peer: "P2", height: 15}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveNB", peer: "P1", height: 11}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 12}, + {evType: "ReceiveB", peer: "P2", height: 11}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 13}, + {evType: "Process"}, + {evType: "ReceiveB", peer: "P2", height: 14}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 15}, + {evType: "Process"}, + }, + }, + { + name: "termination on max peer height - two peers, remove one peer", + params: params, + msgs: []testEvent{ + {evType: "AddPeer", peer: "P1"}, + {evType: "AddPeer", peer: "P2"}, + {evType: "ReceiveS", peer: "P1", height: 13}, + {evType: "ReceiveS", peer: "P2", height: 15}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "RemovePeer", peer: "P1"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 12}, + {evType: "ReceiveB", peer: "P2", height: 11}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 13}, + {evType: "Process"}, + {evType: "ReceiveB", peer: "P2", height: 14}, + {evType: "Process"}, + {evType: "BlockReq"}, + {evType: "ReceiveB", peer: "P2", height: 15}, + {evType: "Process"}, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + reactor := newTestReactor(params) + reactor.Start() + reactor.reporter = behaviour.NewMockReporter() + mockSwitch := &mockSwitchIo{switchedToConsensus: false} + reactor.io = mockSwitch + // time for go routines to start + time.Sleep(time.Millisecond) + + for _, step := range tt.msgs { + switch step.evType { + case "AddPeer": + reactor.scheduler.send(bcAddNewPeer{peerID: p2p.ID(step.peer)}) + case "RemovePeer": + reactor.scheduler.send(bcRemovePeer{peerID: p2p.ID(step.peer)}) + case "ReceiveS": + reactor.scheduler.send(bcStatusResponse{ + peerID: p2p.ID(step.peer), + height: step.height, + time: time.Now(), + }) + case "ReceiveB": + reactor.scheduler.send(bcBlockResponse{ + peerID: p2p.ID(step.peer), + block: refStore.LoadBlock(step.height), + size: 10, + time: time.Now(), + }) + case "ReceiveNB": + reactor.scheduler.send(bcNoBlockResponse{ + peerID: p2p.ID(step.peer), + height: step.height, + time: time.Now(), + }) + case "BlockReq": + reactor.scheduler.send(rTrySchedule{time: time.Now()}) + case "Process": + reactor.processor.send(rProcessBlock{}) + } + // give time for messages to propagate between routines + time.Sleep(time.Millisecond) + } + + // time for processor to finish and reactor to switch to consensus + time.Sleep(20 * time.Millisecond) + assert.True(t, mockSwitch.hasSwitchedToConsensus()) + reactor.Stop() + }) + } +} + +func TestReactorHelperMode(t *testing.T) { var ( - bufferSize = 10 - reactor = NewReactor(bufferSize) + channelID = byte(0x40) ) - reactor.Start() - script := []Event{ - // TODO + config := cfg.ResetTestRoot("blockchain_reactor_v2_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) + + params := testReactorParams{ + logger: log.TestingLogger(), + genDoc: genDoc, + privVals: privVals, + startHeight: 20, + bufferSize: 100, + mockA: true, } - for _, event := range script { - reactor.Receive(event) + type testEvent struct { + peer string + event interface{} + } + + tests := []struct { + name string + params testReactorParams + msgs []testEvent + }{ + { + name: "status request", + params: params, + msgs: []testEvent{ + {"P1", bcStatusRequestMessage{}}, + {"P1", bcBlockRequestMessage{Height: 13}}, + {"P1", bcBlockRequestMessage{Height: 20}}, + {"P1", bcBlockRequestMessage{Height: 22}}, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + reactor := newTestReactor(params) + reactor.Start() + mockSwitch := &mockSwitchIo{switchedToConsensus: false} + reactor.io = mockSwitch + + for i := 0; i < len(tt.msgs); i++ { + step := tt.msgs[i] + switch ev := step.event.(type) { + case bcStatusRequestMessage: + old := mockSwitch.numStatusResponse + reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, cdc.MustMarshalBinaryBare(ev)) + assert.Equal(t, old+1, mockSwitch.numStatusResponse) + case bcBlockRequestMessage: + if ev.Height > params.startHeight { + old := mockSwitch.numNoBlockResponse + reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, cdc.MustMarshalBinaryBare(ev)) + assert.Equal(t, old+1, mockSwitch.numNoBlockResponse) + } else { + old := mockSwitch.numBlockResponse + reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, cdc.MustMarshalBinaryBare(ev)) + assert.Equal(t, old+1, mockSwitch.numBlockResponse) + } + } + } + reactor.Stop() + }) } - reactor.Stop() +} + +//---------------------------------------------- +// utility funcs + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + +type testApp struct { + abci.BaseApplication +} + +func randGenesisDoc(chainID string, numValidators int, randPower bool, minPower int64) ( + *types.GenesisDoc, []types.PrivValidator) { + validators := make([]types.GenesisValidator, numValidators) + privValidators := make([]types.PrivValidator, numValidators) + for i := 0; i < numValidators; i++ { + val, privVal := types.RandValidator(randPower, minPower) + validators[i] = types.GenesisValidator{ + PubKey: val.PubKey, + Power: val.VotingPower, + } + privValidators[i] = privVal + } + sort.Sort(types.PrivValidatorsByAddress(privValidators)) + + return &types.GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: chainID, + Validators: validators, + }, privValidators +} + +// Why are we importing the entire blockExecutor dependency graph here +// when we have the facilities to +func newReactorStore( + genDoc *types.GenesisDoc, + privVals []types.PrivValidator, + maxBlockHeight int64) (*store.BlockStore, sm.State, *sm.BlockExecutor) { + if len(privVals) != 1 { + panic("only support one validator") + } + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(cc) + err := proxyApp.Start() + if err != nil { + panic(errors.Wrap(err, "error start app")) + } + + stateDB := dbm.NewMemDB() + blockStore := store.NewBlockStore(dbm.NewMemDB()) + + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + panic(errors.Wrap(err, "error constructing state from genesis file")) + } + + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}) + sm.SaveState(db, state) + + // add blocks in + for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { + lastCommit := types.NewCommit(blockHeight-1, 0, types.BlockID{}, nil) + if blockHeight > 1 { + lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) + lastBlock := blockStore.LoadBlock(blockHeight - 1) + vote, err := types.MakeVote( + lastBlock.Header.Height, + lastBlockMeta.BlockID, + state.Validators, + privVals[0], + lastBlock.Header.ChainID) + if err != nil { + panic(err) + } + lastCommit = types.NewCommit(vote.Height, vote.Round, + lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()}) + } + + thisBlock := makeBlock(blockHeight, state, lastCommit) + + thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} + + state, err = blockExec.ApplyBlock(state, blockID, thisBlock) + if err != nil { + panic(errors.Wrap(err, "error apply block")) + } + + blockStore.SaveBlock(thisBlock, thisParts, lastCommit) + } + return blockStore, state, blockExec } diff --git a/blockchain/v2/routine.go b/blockchain/v2/routine.go index 897dd738c..1a883c3c4 100644 --- a/blockchain/v2/routine.go +++ b/blockchain/v2/routine.go @@ -10,7 +10,7 @@ import ( type handleFunc = func(event Event) (Event, error) -// Routines are a structure which model a finite state machine as serialized +// Routine is a structure that models a finite state machine as serialized // stream of events processed by a handle function. This Routine structure // handles the concurrency and messaging guarantees. Events are sent via // `send` are handled by the `handle` function to produce an iterator @@ -80,7 +80,7 @@ func (rt *Routine) start() { return } rt.metrics.EventsOut.With("routine", rt.name).Add(1) - rt.logger.Debug(fmt.Sprintf("%s produced %T %+v\n", rt.name, oEvent, oEvent)) + rt.logger.Debug(fmt.Sprintf("%s: produced %T %+v\n", rt.name, oEvent, oEvent)) rt.out <- oEvent } @@ -98,6 +98,7 @@ func (rt *Routine) send(event Event) bool { rt.logger.Info(fmt.Sprintf("%s: send failed, queue was full/stopped \n", rt.name)) return false } + rt.metrics.EventsSent.With("routine", rt.name).Add(1) return true } diff --git a/blockchain/v2/scheduler.go b/blockchain/v2/scheduler.go index ab3892dc5..3cf0b2468 100644 --- a/blockchain/v2/scheduler.go +++ b/blockchain/v2/scheduler.go @@ -11,52 +11,11 @@ import ( "github.com/tendermint/tendermint/types" ) -// Events - -// XXX: The handle API would be much simpler if it return a single event, an -// Event, which embeds a terminationEvent if it wants to terminate the routine. - -// Input events into the scheduler: -// ticker event for cleaning peers -type tryPrunePeer struct { - priorityHigh - time time.Time -} - -// ticker event for scheduling block requests -type trySchedule struct { - priorityHigh - time time.Time -} - -// blockResponse message received from a peer -type bcBlockResponse struct { - priorityNormal - time time.Time - peerID p2p.ID - height int64 - size int64 - block *types.Block -} - -// statusResponse message received from a peer -type bcStatusResponse struct { - priorityNormal - time time.Time - peerID p2p.ID - height int64 -} - -// new peer is connected -type addNewPeer struct { - priorityNormal - peerID p2p.ID -} - -// Output events issued by the scheduler: +// Events generated by the scheduler: // all blocks have been processed type scFinishedEv struct { priorityNormal + reason string } // send a blockRequest message @@ -80,6 +39,10 @@ type scPeerError struct { reason error } +func (e scPeerError) String() string { + return fmt.Sprintf("scPeerError - peerID %s, err %s", e.peerID, e.reason) +} + // scheduler removed a set of peers (timed out or slow peer) type scPeersPruned struct { priorityHigh @@ -160,9 +123,10 @@ func (p scPeer) String() string { func newScPeer(peerID p2p.ID) *scPeer { return &scPeer{ - peerID: peerID, - state: peerStateNew, - height: -1, + peerID: peerID, + state: peerStateNew, + height: -1, + lastTouched: time.Time{}, } } @@ -176,11 +140,17 @@ type scheduler struct { // in Processed state. height int64 + // lastAdvance tracks the last time a block execution happened. + // syncTimeout is the maximum time the scheduler waits to advance in the fast sync process before finishing. + // This covers the cases where there are no peers or all peers have a lower height. + lastAdvance time.Time + syncTimeout time.Duration + // a map of peerID to scheduler specific peer struct `scPeer` used to keep // track of peer specific state peers map[p2p.ID]*scPeer - peerTimeout time.Duration - minRecvRate int64 // minimum receive rate from peer otherwise prune + peerTimeout time.Duration // maximum response time from a peer otherwise prune + minRecvRate int64 // minimum receive rate from peer otherwise prune // the maximum number of blocks that should be New, Received or Pending at any point // in time. This is used to enforce a limit on the blockStates map. @@ -204,15 +174,20 @@ func (sc scheduler) String() string { sc.initHeight, sc.blockStates, sc.peers, sc.pendingBlocks, sc.pendingTime, sc.receivedBlocks) } -func newScheduler(initHeight int64) *scheduler { +func newScheduler(initHeight int64, startTime time.Time) *scheduler { sc := scheduler{ initHeight: initHeight, + lastAdvance: startTime, + syncTimeout: 60 * time.Second, height: initHeight + 1, blockStates: make(map[int64]blockState), peers: make(map[p2p.ID]*scPeer), pendingBlocks: make(map[int64]p2p.ID), pendingTime: make(map[int64]time.Time), receivedBlocks: make(map[int64]p2p.ID), + targetPending: 10, // TODO - pass as param + peerTimeout: 15 * time.Second, // TODO - pass as param + minRecvRate: 0, //int64(7680), TODO - pass as param } return &sc @@ -316,6 +291,7 @@ func (sc *scheduler) setPeerHeight(peerID p2p.ID, height int64) error { } if height < peer.height { + sc.removePeer(peerID) return fmt.Errorf("cannot move peer height lower. from %d to %d", peer.height, height) } @@ -327,7 +303,7 @@ func (sc *scheduler) setPeerHeight(peerID p2p.ID, height int64) error { } func (sc *scheduler) getStateAtHeight(height int64) blockState { - if height <= sc.initHeight { + if height < sc.height { return blockStateProcessed } else if state, ok := sc.blockStates[height]; ok { return state @@ -349,41 +325,8 @@ func (sc *scheduler) getPeersAtHeightOrAbove(height int64) []p2p.ID { return peers } -func (sc *scheduler) peersInactiveSince(duration time.Duration, now time.Time) []p2p.ID { - peers := []p2p.ID{} - for _, peer := range sc.peers { - if peer.state != peerStateReady { - continue - } - if now.Sub(peer.lastTouched) > duration { - peers = append(peers, peer.peerID) - } - } - - // Ensure the order is deterministic for testing - sort.Sort(PeerByID(peers)) - return peers -} - -// will return peers who's lastRate i slower than minSpeed denominated in bytes -func (sc *scheduler) peersSlowerThan(minSpeed int64) []p2p.ID { - peers := []p2p.ID{} - for peerID, peer := range sc.peers { - if peer.state != peerStateReady { - continue - } - if peer.lastRate < minSpeed { - peers = append(peers, peerID) - } - } - - // Ensure the order is deterministic for testing - sort.Sort(PeerByID(peers)) - return peers -} - func (sc *scheduler) prunablePeers(peerTimout time.Duration, minRecvRate int64, now time.Time) []p2p.ID { - prunable := []p2p.ID{} + prunable := make([]p2p.ID, 0) for peerID, peer := range sc.peers { if peer.state != peerStateReady { continue @@ -407,8 +350,8 @@ func (sc *scheduler) markReceived(peerID p2p.ID, height int64, size int64, now t return fmt.Errorf("couldn't find peer %s", peerID) } - if peer.state == peerStateRemoved { - return fmt.Errorf("cannot receive blocks from removed peer %s", peerID) + if peer.state != peerStateReady { + return fmt.Errorf("cannot receive blocks from not ready peer %s", peerID) } if state := sc.getStateAtHeight(height); state != blockStatePending || sc.pendingBlocks[height] != peerID { @@ -454,14 +397,13 @@ func (sc *scheduler) markPending(peerID p2p.ID, height int64, time time.Time) er sc.setStateAtHeight(height, blockStatePending) sc.pendingBlocks[height] = peerID - // XXX: to make this more accurate we can introduce a message from - // the IO routine which indicates the time the request was put on the wire sc.pendingTime[height] = time return nil } func (sc *scheduler) markProcessed(height int64) error { + sc.lastAdvance = time.Now() state := sc.getStateAtHeight(height) if state != blockStateReceived { return fmt.Errorf("cannot mark height %d received from block state %s", height, state) @@ -476,6 +418,9 @@ func (sc *scheduler) markProcessed(height int64) error { } func (sc *scheduler) allBlocksProcessed() bool { + if len(sc.peers) == 0 { + return false + } return sc.height >= sc.maxHeight() } @@ -486,7 +431,7 @@ func (sc *scheduler) maxHeight() int64 { if peer.state != peerStateReady { continue } - if peer.height > max { + if max < peer.height { max = peer.height } } @@ -532,15 +477,15 @@ func (sc *scheduler) selectPeer(height int64) (p2p.ID, error) { } // find the set of peers with minimum number of pending requests. - minPending := math.MaxInt64 + var minPending int64 = math.MaxInt64 for mp := range pendingFrom { - if mp < minPending { - minPending = mp + if int64(mp) < minPending { + minPending = int64(mp) } } - sort.Sort(PeerByID(pendingFrom[minPending])) - return pendingFrom[minPending][0], nil + sort.Sort(PeerByID(pendingFrom[int(minPending)])) + return pendingFrom[int(minPending)][0], nil } // PeerByID is a list of peers sorted by peerID. @@ -570,12 +515,30 @@ func (sc *scheduler) handleBlockResponse(event bcBlockResponse) (Event, error) { err = sc.markReceived(event.peerID, event.block.Height, event.size, event.time) if err != nil { + _ = sc.removePeer(event.peerID) return scPeerError{peerID: event.peerID, reason: err}, nil } return scBlockReceived{peerID: event.peerID, block: event.block}, nil } +func (sc *scheduler) handleNoBlockResponse(event bcNoBlockResponse) (Event, error) { + if len(sc.peers) == 0 { + return noOp, nil + } + + peer, ok := sc.peers[event.peerID] + if !ok || peer.state == peerStateRemoved { + return noOp, nil + } + // The peer may have been just removed due to errors, low speed or timeouts. + _ = sc.removePeer(event.peerID) + + return scPeerError{peerID: event.peerID, + reason: fmt.Errorf("peer %v with height %d claims no block for %d", + event.peerID, peer.height, event.height)}, nil +} + func (sc *scheduler) handleBlockProcessed(event pcBlockProcessed) (Event, error) { if event.height != sc.height { panic(fmt.Sprintf("processed height %d but expected height %d", event.height, sc.height)) @@ -584,12 +547,12 @@ func (sc *scheduler) handleBlockProcessed(event pcBlockProcessed) (Event, error) if err != nil { // It is possible that a peer error or timeout is handled after the processor // has processed the block but before the scheduler received this event, - // so when pcBlockProcessed event is received the block had been requested again + // so when pcBlockProcessed event is received the block had been requested again. return scSchedulerFail{reason: err}, nil } if sc.allBlocksProcessed() { - return scFinishedEv{}, nil + return scFinishedEv{reason: "processed all blocks"}, nil } return noOp, nil @@ -608,13 +571,13 @@ func (sc *scheduler) handleBlockProcessError(event pcBlockVerificationFailure) ( } if sc.allBlocksProcessed() { - return scFinishedEv{}, nil + return scFinishedEv{reason: "error on last block"}, nil } return noOp, nil } -func (sc *scheduler) handleAddNewPeer(event addNewPeer) (Event, error) { +func (sc *scheduler) handleAddNewPeer(event bcAddNewPeer) (Event, error) { err := sc.addPeer(event.peerID) if err != nil { return scSchedulerFail{reason: err}, nil @@ -622,8 +585,7 @@ func (sc *scheduler) handleAddNewPeer(event addNewPeer) (Event, error) { return noOp, nil } -// XXX: unify types peerError -func (sc *scheduler) handlePeerError(event peerError) (Event, error) { +func (sc *scheduler) handleRemovePeer(event bcRemovePeer) (Event, error) { err := sc.removePeer(event.peerID) if err != nil { // XXX - It is possible that the removePeer fails here for legitimate reasons @@ -631,12 +593,23 @@ func (sc *scheduler) handlePeerError(event peerError) (Event, error) { return scSchedulerFail{reason: err}, nil } if sc.allBlocksProcessed() { - return scFinishedEv{}, nil + return scFinishedEv{reason: "removed peer"}, nil } return noOp, nil } -func (sc *scheduler) handleTryPrunePeer(event tryPrunePeer) (Event, error) { +func (sc *scheduler) handleTryPrunePeer(event rTryPrunePeer) (Event, error) { + + // Check behavior of peer responsible to deliver block at sc.height. + timeHeightAsked, ok := sc.pendingTime[sc.height] + if ok && time.Since(timeHeightAsked) > sc.peerTimeout { + // A request was sent to a peer for block at sc.height but a response was not received + // from that peer within sc.peerTimeout. Remove the peer. This is to ensure that a peer + // will be timed out even if it sends blocks at higher heights but prevents progress by + // not sending the block at current height. + sc.removePeer(sc.pendingBlocks[sc.height]) + } + prunablePeers := sc.prunablePeers(sc.peerTimeout, sc.minRecvRate, event.time) if len(prunablePeers) == 0 { return noOp, nil @@ -649,17 +622,19 @@ func (sc *scheduler) handleTryPrunePeer(event tryPrunePeer) (Event, error) { } } - // If all blocks are processed we should finish even some peers were pruned. + // If all blocks are processed we should finish. if sc.allBlocksProcessed() { - return scFinishedEv{}, nil + return scFinishedEv{reason: "after try prune"}, nil } return scPeersPruned{peers: prunablePeers}, nil } -// TODO - Schedule multiple block requests -func (sc *scheduler) handleTrySchedule(event trySchedule) (Event, error) { +func (sc *scheduler) handleTrySchedule(event rTrySchedule) (Event, error) { + if time.Since(sc.lastAdvance) > sc.syncTimeout { + return scFinishedEv{reason: "timeout, no advance"}, nil + } nextHeight := sc.nextHeightToSchedule() if nextHeight == -1 { @@ -693,17 +668,20 @@ func (sc *scheduler) handle(event Event) (Event, error) { case bcBlockResponse: nextEvent, err := sc.handleBlockResponse(event) return nextEvent, err - case trySchedule: + case bcNoBlockResponse: + nextEvent, err := sc.handleNoBlockResponse(event) + return nextEvent, err + case rTrySchedule: nextEvent, err := sc.handleTrySchedule(event) return nextEvent, err - case addNewPeer: + case bcAddNewPeer: nextEvent, err := sc.handleAddNewPeer(event) return nextEvent, err - case tryPrunePeer: - nextEvent, err := sc.handleTryPrunePeer(event) + case bcRemovePeer: + nextEvent, err := sc.handleRemovePeer(event) return nextEvent, err - case peerError: - nextEvent, err := sc.handlePeerError(event) + case rTryPrunePeer: + nextEvent, err := sc.handleTryPrunePeer(event) return nextEvent, err case pcBlockProcessed: nextEvent, err := sc.handleBlockProcessed(event) @@ -714,5 +692,4 @@ func (sc *scheduler) handle(event Event) (Event, error) { default: return scSchedulerFail{reason: fmt.Errorf("unknown event %v", event)}, nil } - //return noOp, nil } diff --git a/blockchain/v2/scheduler_test.go b/blockchain/v2/scheduler_test.go index a3a6db672..445ba51a7 100644 --- a/blockchain/v2/scheduler_test.go +++ b/blockchain/v2/scheduler_test.go @@ -23,6 +23,8 @@ type scTestParams struct { peerTimeout time.Duration minRecvRate int64 targetPending int + startTime time.Time + syncTimeout time.Duration } func verifyScheduler(sc *scheduler) { @@ -37,8 +39,9 @@ func verifyScheduler(sc *scheduler) { func newTestScheduler(params scTestParams) *scheduler { peers := make(map[p2p.ID]*scPeer) + var maxHeight int64 - sc := newScheduler(params.initHeight) + sc := newScheduler(params.initHeight, params.startTime) if params.height != 0 { sc.height = params.height } @@ -46,6 +49,9 @@ func newTestScheduler(params scTestParams) *scheduler { for id, peer := range params.peers { peer.peerID = p2p.ID(id) peers[p2p.ID(id)] = peer + if maxHeight < peer.height { + maxHeight = peer.height + } } for _, h := range params.allB { sc.blockStates[h] = blockStateNew @@ -64,6 +70,12 @@ func newTestScheduler(params scTestParams) *scheduler { sc.peers = peers sc.peerTimeout = params.peerTimeout + if params.syncTimeout == 0 { + sc.syncTimeout = 10 * time.Second + } else { + sc.syncTimeout = params.syncTimeout + } + if params.targetPending == 0 { sc.targetPending = 10 } else { @@ -80,7 +92,7 @@ func newTestScheduler(params scTestParams) *scheduler { func TestScInit(t *testing.T) { var ( initHeight int64 = 5 - sc = newScheduler(initHeight) + sc = newScheduler(initHeight, time.Now()) ) assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight)) assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1)) @@ -181,21 +193,21 @@ func TestScAddPeer(t *testing.T) { name: "add first peer", fields: scTestParams{}, args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}}, + wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, }, { name: "add second peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}}, + fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, args: args{peerID: "P2"}, wantFields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateNew, height: -1}, - "P2": {state: peerStateNew, height: -1}}}, + "P1": {height: -1, state: peerStateNew}, + "P2": {height: -1, state: peerStateNew}}}, }, { name: "attempt to add duplicate peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}}, + fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}}, + wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, wantErr: true, }, { @@ -271,8 +283,8 @@ func TestScTouchPeer(t *testing.T) { name: "touch peer in state Ready", fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}}, args: args{peerID: "P1", time: now.Add(3 * time.Second)}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, - lastTouched: now.Add(3 * time.Second)}}}, + wantFields: scTestParams{peers: map[string]*scPeer{ + "P1": {state: peerStateReady, lastTouched: now.Add(3 * time.Second)}}}, }, } @@ -289,195 +301,6 @@ func TestScTouchPeer(t *testing.T) { } } -func TestScPeersInactiveSince(t *testing.T) { - now := time.Now() - - type args struct { - threshold time.Duration - time time.Time - } - - tests := []struct { - name string - fields scTestParams - args args - wantResult []p2p.ID - }{ - { - name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, - args: args{threshold: time.Second, time: now}, - wantResult: []p2p.ID{}, - }, - { - name: "one active peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}}, - args: args{threshold: time.Second, time: now}, - wantResult: []p2p.ID{}, - }, - { - name: "one inactive peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}}, - args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond)}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one active and one inactive peer", - fields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateReady, lastTouched: now}, - "P2": {state: peerStateReady, lastTouched: now.Add(time.Second)}}}, - args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond)}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one New peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}}, - args: args{threshold: time.Second, time: now.Add(time.Millisecond)}, - wantResult: []p2p.ID{}, - }, - { - name: "one Removed peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastTouched: now}}}, - args: args{threshold: time.Second, time: now.Add(time.Millisecond)}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready active peer and one New", - fields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateRemoved, lastTouched: now}, - "P2": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)}}}, - args: args{threshold: time.Second, time: now.Add(2 * time.Millisecond)}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready inactive peer and one New", - fields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateRemoved, lastTouched: now}, - "P2": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)}}}, - args: args{threshold: time.Second, time: now.Add(time.Second + 2*time.Millisecond)}, - wantResult: []p2p.ID{"P2"}, - }, - { - name: "combination of New, Removed and, active and non active Ready peers", - fields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateNew}, - "P2": {state: peerStateRemoved, lastTouched: now}, - "P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second)}, - "P4": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)}, - "P5": {state: peerStateReady, lastTouched: now.Add(3 * time.Millisecond)}}}, - args: args{threshold: time.Second, time: now.Add(time.Second + 2*time.Millisecond)}, - wantResult: []p2p.ID{"P4"}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // peersInactiveSince should not mutate the scheduler - wantSc := sc - res := sc.peersInactiveSince(tt.args.threshold, tt.args.time) - sort.Sort(PeerByID(res)) - assert.Equal(t, tt.wantResult, res) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScPeersSlowerThan(t *testing.T) { - type args struct { - minSpeed int64 - } - - tests := []struct { - name string - fields scTestParams - args args - wantResult []p2p.ID - }{ - { - name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready faster peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 101}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready equal peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 100}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready slow peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 99}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one Removed faster peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 101}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, { - name: "one Removed equal peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 100}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one Removed slow peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 99}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one New peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "one New peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "mixed peers", - fields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateRemoved, lastRate: 101}, - "P2": {state: peerStateReady, lastRate: 101}, - "P3": {state: peerStateRemoved, lastRate: 100}, - "P4": {state: peerStateReady, lastRate: 100}, - "P5": {state: peerStateReady, lastRate: 99}, - "P6": {state: peerStateNew}, - "P7": {state: peerStateRemoved, lastRate: 99}, - "P8": {state: peerStateReady, lastRate: 99}, - }}, - args: args{minSpeed: 100}, - wantResult: []p2p.ID{"P5", "P8"}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // peersSlowerThan should not mutate the scheduler - wantSc := sc - res := sc.peersSlowerThan(tt.args.minSpeed) - assert.Equal(t, tt.wantResult, res) - assert.Equal(t, wantSc, sc) - }) - } -} - func TestScPrunablePeers(t *testing.T) { now := time.Now() @@ -716,8 +539,8 @@ func TestScSetPeerHeight(t *testing.T) { allB: []int64{1, 2, 3, 4}}, args: args{peerID: "P1", height: 2}, wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, + peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}, + allB: []int64{}}, wantErr: true, }, { @@ -845,7 +668,7 @@ func TestScGetPeersAtHeight(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { sc := newTestScheduler(tt.fields) - // getPeersAtHeightOrAbove should not mutate the scheduler + // getPeersAtHeight should not mutate the scheduler wantSc := sc res := sc.getPeersAtHeightOrAbove(tt.args.height) sort.Sort(PeerByID(res)) @@ -1082,8 +905,11 @@ func TestScMarkReceived(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { sc := newTestScheduler(tt.fields) - if err := sc.markReceived(tt.args.peerID, - tt.args.height, tt.args.size, now.Add(time.Second)); (err != nil) != tt.wantErr { + if err := sc.markReceived( + tt.args.peerID, + tt.args.height, + tt.args.size, + now.Add(time.Second)); (err != nil) != tt.wantErr { t.Errorf("markReceived() wantErr %v, error = %v", tt.wantErr, err) } wantSc := newTestScheduler(tt.wantFields) @@ -1145,11 +971,17 @@ func TestScMarkProcessed(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { sc := newTestScheduler(tt.fields) + oldBlockState := sc.getStateAtHeight(tt.args.height) if err := sc.markProcessed(tt.args.height); (err != nil) != tt.wantErr { t.Errorf("markProcessed() wantErr %v, error = %v", tt.wantErr, err) } + if tt.wantErr { + assert.Equal(t, oldBlockState, sc.getStateAtHeight(tt.args.height)) + } else { + assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(tt.args.height)) + } wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc) + checkSameScheduler(t, wantSc, sc) }) } } @@ -1163,9 +995,9 @@ func TestScAllBlocksProcessed(t *testing.T) { wantResult bool }{ { - name: "no blocks", + name: "no blocks, no peers", fields: scTestParams{}, - wantResult: true, + wantResult: false, }, { name: "only New blocks", @@ -1225,7 +1057,7 @@ func TestScAllBlocksProcessed(t *testing.T) { wantSc := sc res := sc.allBlocksProcessed() assert.Equal(t, tt.wantResult, res) - assert.Equal(t, wantSc, sc) + checkSameScheduler(t, wantSc, sc) }) } } @@ -1305,8 +1137,7 @@ func TestScNextHeightToSchedule(t *testing.T) { resMin := sc.nextHeightToSchedule() assert.Equal(t, tt.wantHeight, resMin) - assert.Equal(t, wantSc, sc) - + checkSameScheduler(t, wantSc, sc) }) } } @@ -1414,7 +1245,7 @@ func TestScSelectPeer(t *testing.T) { res, err := sc.selectPeer(tt.args.height) assert.Equal(t, tt.wantResult, res) assert.Equal(t, tt.wantError, err != nil) - assert.Equal(t, wantSc, sc) + checkSameScheduler(t, wantSc, sc) }) } } @@ -1424,6 +1255,20 @@ func makeScBlock(height int64) *types.Block { return &types.Block{Header: types.Header{Height: height}} } +// used in place of assert.Equal(t, want, actual) to avoid failures due to +// scheduler.lastAdvanced timestamp inequalities. +func checkSameScheduler(t *testing.T, want *scheduler, actual *scheduler) { + assert.Equal(t, want.initHeight, actual.initHeight) + assert.Equal(t, want.height, actual.height) + assert.Equal(t, want.peers, actual.peers) + assert.Equal(t, want.blockStates, actual.blockStates) + assert.Equal(t, want.pendingBlocks, actual.pendingBlocks) + assert.Equal(t, want.pendingTime, actual.pendingTime) + assert.Equal(t, want.blockStates, actual.blockStates) + assert.Equal(t, want.receivedBlocks, actual.receivedBlocks) + assert.Equal(t, want.blockStates, actual.blockStates) +} + // checkScResults checks scheduler handler test results func checkScResults(t *testing.T, wantErr bool, err error, wantEvent Event, event Event) { if (err != nil) != wantErr { @@ -1439,8 +1284,6 @@ func checkScResults(t *testing.T, wantErr bool, err error, wantEvent Event, even assert.Equal(t, wantEvent.block, event.(scBlockReceived).block) case scSchedulerFail: assert.Equal(t, wantEvent.reason != nil, event.(scSchedulerFail).reason != nil) - default: - assert.Equal(t, wantEvent, event) } } @@ -1449,7 +1292,6 @@ func TestScHandleBlockResponse(t *testing.T) { block6FromP1 := bcBlockResponse{ time: now.Add(time.Millisecond), peerID: p2p.ID("P1"), - height: 6, size: 100, block: makeScBlock(6), } @@ -1530,6 +1372,82 @@ func TestScHandleBlockResponse(t *testing.T) { } } +func TestScHandleNoBlockResponse(t *testing.T) { + now := time.Now() + noBlock6FromP1 := bcNoBlockResponse{ + time: now.Add(time.Millisecond), + peerID: p2p.ID("P1"), + height: 6, + } + + tests := []struct { + name string + fields scTestParams + wantEvent Event + wantFields scTestParams + wantErr bool + }{ + { + name: "empty scheduler", + fields: scTestParams{}, + wantEvent: noOpEvent{}, + wantFields: scTestParams{}, + }, + { + name: "noBlock from removed peer", + fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, + wantEvent: noOpEvent{}, + wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, + }, + { + name: "for block we haven't asked for", + fields: scTestParams{ + peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, + allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}}, + wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, + wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, + }, + { + name: "noBlock from peer we don't have", + fields: scTestParams{ + peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, + allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, + pending: map[int64]p2p.ID{6: "P2"}, + pendingTime: map[int64]time.Time{6: now}, + }, + wantEvent: noOpEvent{}, + wantFields: scTestParams{ + peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, + allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, + pending: map[int64]p2p.ID{6: "P2"}, + pendingTime: map[int64]time.Time{6: now}, + }, + }, + { + name: "noBlock from existing peer", + fields: scTestParams{ + peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, + allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, + pending: map[int64]p2p.ID{6: "P1"}, + pendingTime: map[int64]time.Time{6: now}, + }, + wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, + wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + sc := newTestScheduler(tt.fields) + event, err := sc.handleNoBlockResponse(noBlock6FromP1) + checkScResults(t, tt.wantErr, err, tt.wantEvent, event) + wantSc := newTestScheduler(tt.wantFields) + assert.Equal(t, wantSc, sc) + }) + } +} + func TestScHandleBlockProcessed(t *testing.T) { now := time.Now() processed6FromP1 := pcBlockProcessed{ @@ -1702,11 +1620,11 @@ func TestScHandleBlockVerificationFailure(t *testing.T) { } func TestScHandleAddNewPeer(t *testing.T) { - addP1 := addNewPeer{ + addP1 := bcAddNewPeer{ peerID: p2p.ID("P1"), } type args struct { - event addNewPeer + event bcAddNewPeer } tests := []struct { @@ -1754,76 +1672,14 @@ func TestScHandleAddNewPeer(t *testing.T) { } } -func TestScHandlePeerError(t *testing.T) { - errP1 := peerError{ - peerID: p2p.ID("P1"), - } - type args struct { - event peerError - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "no peers", - fields: scTestParams{}, - args: args{event: errP1}, - wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")}, - }, - { - name: "error finds no peer", - fields: scTestParams{ - height: 6, - peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - }, - args: args{event: errP1}, - wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")}, - }, - { - name: "error finds peer, only peer is removed", - fields: scTestParams{ - height: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - }, - args: args{event: errP1}, - wantEvent: scFinishedEv{}, - }, - { - name: "error finds peer, one of two peers are removed", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - }, - args: args{event: errP1}, - wantEvent: noOpEvent{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handlePeerError(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - func TestScHandleTryPrunePeer(t *testing.T) { now := time.Now() - pruneEv := tryPrunePeer{ + pruneEv := rTryPrunePeer{ time: now.Add(time.Second + time.Millisecond), } type args struct { - event tryPrunePeer + event rTryPrunePeer } tests := []struct { @@ -1914,14 +1770,14 @@ func TestScHandleTryPrunePeer(t *testing.T) { } } -func TestHandleTrySchedule(t *testing.T) { +func TestScHandleTrySchedule(t *testing.T) { now := time.Now() - tryEv := trySchedule{ + tryEv := rTrySchedule{ time: now.Add(time.Second + time.Millisecond), } type args struct { - event trySchedule + event rTrySchedule } tests := []struct { name string @@ -1932,41 +1788,44 @@ func TestHandleTrySchedule(t *testing.T) { }{ { name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, + fields: scTestParams{startTime: now, peers: map[string]*scPeer{}}, args: args{event: tryEv}, wantEvent: noOpEvent{}, }, { name: "only new peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, + fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, args: args{event: tryEv}, wantEvent: noOpEvent{}, }, { name: "only Removed peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, + fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, args: args{event: tryEv}, wantEvent: noOpEvent{}, }, { name: "one Ready shorter peer", fields: scTestParams{ - height: 6, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}}, + startTime: now, + height: 6, + peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}}, args: args{event: tryEv}, wantEvent: noOpEvent{}, }, { name: "one Ready equal peer", fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, + startTime: now, + peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, + allB: []int64{1, 2, 3, 4}}, args: args{event: tryEv}, wantEvent: scBlockRequest{peerID: "P1", height: 1}, }, { name: "many Ready higher peers with different number of pending requests", fields: scTestParams{ + startTime: now, peers: map[string]*scPeer{ "P1": {height: 4, state: peerStateReady}, "P2": {height: 5, state: peerStateReady}}, @@ -1983,6 +1842,7 @@ func TestHandleTrySchedule(t *testing.T) { { name: "many Ready higher peers with same number of pending requests", fields: scTestParams{ + startTime: now, peers: map[string]*scPeer{ "P2": {height: 8, state: peerStateReady}, "P1": {height: 8, state: peerStateReady}, @@ -2084,6 +1944,8 @@ func TestScHandleStatusResponse(t *testing.T) { } func TestScHandle(t *testing.T) { + now := time.Now() + type unknownEv struct { priorityNormal } @@ -2123,24 +1985,27 @@ func TestScHandle(t *testing.T) { name: "single peer, sync 3 blocks", steps: []scStep{ { // add P1 - currentSc: &scTestParams{peers: map[string]*scPeer{}, height: 1}, - args: args{event: addNewPeer{peerID: "P1"}}, + currentSc: &scTestParams{startTime: now, peers: map[string]*scPeer{}, height: 1}, + args: args{event: bcAddNewPeer{peerID: "P1"}}, wantEvent: noOpEvent{}, - wantSc: &scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}, height: 1}, + wantSc: &scTestParams{startTime: now, peers: map[string]*scPeer{ + "P1": {height: -1, state: peerStateNew}}, height: 1}, }, { // set height of P1 args: args{event: bcStatusResponse{peerID: "P1", time: tick[0], height: 3}}, wantEvent: noOpEvent{}, wantSc: &scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - height: 1, + startTime: now, + peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, + allB: []int64{1, 2, 3}, + height: 1, }, }, { // schedule block 1 - args: args{event: trySchedule{time: tick[1]}}, + args: args{event: rTrySchedule{time: tick[1]}}, wantEvent: scBlockRequest{peerID: "P1", height: 1}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, allB: []int64{1, 2, 3}, pending: map[int64]p2p.ID{1: "P1"}, @@ -2149,9 +2014,10 @@ func TestScHandle(t *testing.T) { }, }, { // schedule block 2 - args: args{event: trySchedule{time: tick[2]}}, + args: args{event: rTrySchedule{time: tick[2]}}, wantEvent: scBlockRequest{peerID: "P1", height: 2}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, allB: []int64{1, 2, 3}, pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, @@ -2160,9 +2026,10 @@ func TestScHandle(t *testing.T) { }, }, { // schedule block 3 - args: args{event: trySchedule{time: tick[3]}}, + args: args{event: rTrySchedule{time: tick[3]}}, wantEvent: scBlockRequest{peerID: "P1", height: 3}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, allB: []int64{1, 2, 3}, pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, @@ -2171,9 +2038,10 @@ func TestScHandle(t *testing.T) { }, }, { // block response 1 - args: args{event: bcBlockResponse{peerID: "P1", height: 1, time: tick[4], size: 100, block: makeScBlock(1)}}, + args: args{event: bcBlockResponse{peerID: "P1", time: tick[4], size: 100, block: makeScBlock(1)}}, wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(1)}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[4]}}, allB: []int64{1, 2, 3}, pending: map[int64]p2p.ID{2: "P1", 3: "P1"}, @@ -2183,9 +2051,10 @@ func TestScHandle(t *testing.T) { }, }, { // block response 2 - args: args{event: bcBlockResponse{peerID: "P1", height: 2, time: tick[5], size: 100, block: makeScBlock(2)}}, + args: args{event: bcBlockResponse{peerID: "P1", time: tick[5], size: 100, block: makeScBlock(2)}}, wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(2)}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[5]}}, allB: []int64{1, 2, 3}, pending: map[int64]p2p.ID{3: "P1"}, @@ -2195,33 +2064,36 @@ func TestScHandle(t *testing.T) { }, }, { // block response 3 - args: args{event: bcBlockResponse{peerID: "P1", height: 3, time: tick[6], size: 100, block: makeScBlock(3)}}, + args: args{event: bcBlockResponse{peerID: "P1", time: tick[6], size: 100, block: makeScBlock(3)}}, wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(3)}, wantSc: &scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{1, 2, 3}, - received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, - height: 1, + startTime: now, + peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, + allB: []int64{1, 2, 3}, + received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, + height: 1, }, }, { // processed block 1 args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 1}}, wantEvent: noOpEvent{}, wantSc: &scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{2, 3}, - received: map[int64]p2p.ID{2: "P1", 3: "P1"}, - height: 2, + startTime: now, + peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, + allB: []int64{2, 3}, + received: map[int64]p2p.ID{2: "P1", 3: "P1"}, + height: 2, }, }, { // processed block 2 args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 2}}, wantEvent: scFinishedEv{}, wantSc: &scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{3}, - received: map[int64]p2p.ID{3: "P1"}, - height: 3, + startTime: now, + peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, + allB: []int64{3}, + received: map[int64]p2p.ID{3: "P1"}, + height: 3, }, }, }, @@ -2231,6 +2103,7 @@ func TestScHandle(t *testing.T) { steps: []scStep{ { // failure processing block 1 currentSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{ "P1": {height: 4, state: peerStateReady, lastTouched: tick[6]}, "P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, @@ -2241,6 +2114,7 @@ func TestScHandle(t *testing.T) { args: args{event: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}}, wantEvent: noOpEvent{}, wantSc: &scTestParams{ + startTime: now, peers: map[string]*scPeer{ "P1": {height: 4, state: peerStateRemoved, lastTouched: tick[6]}, "P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, @@ -2249,19 +2123,6 @@ func TestScHandle(t *testing.T) { height: 1, }, }, - /* - { // processed block 2 - args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 2}}, - wantEvent: scFinishedEv{}, - wantSc: &scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{3}, - received: map[int64]p2p.ID{3: "P1"}, - height: 3, - }, - }, - - */ }, }, } @@ -2280,8 +2141,10 @@ func TestScHandle(t *testing.T) { } nextEvent, err := sc.handle(step.args.event) - assert.Equal(t, newTestScheduler(*step.wantSc), sc) + wantSc := newTestScheduler(*step.wantSc) + t.Logf("step %d(%v): %s", i, step.args.event, sc) + checkSameScheduler(t, wantSc, sc) checkScResults(t, step.wantErr, err, step.wantEvent, nextEvent) diff --git a/blockchain/v2/types.go b/blockchain/v2/types.go index 836e87fd8..7a73728e4 100644 --- a/blockchain/v2/types.go +++ b/blockchain/v2/types.go @@ -4,6 +4,7 @@ import ( "github.com/Workiva/go-datastructures/queue" ) +// Event is the type that can be added to the priority queue. type Event queue.Item type priority interface { diff --git a/docs/architecture/adr-043-blockchain-riri-org.md b/docs/architecture/adr-043-blockchain-riri-org.md index 8daef1817..6bb018f51 100644 --- a/docs/architecture/adr-043-blockchain-riri-org.md +++ b/docs/architecture/adr-043-blockchain-riri-org.md @@ -4,6 +4,7 @@ * 18-06-2019: Initial draft * 08-07-2019: Reviewed * 29-11-2019: Implemented +* 14-02-2020: Updated with the implementation details ## Context @@ -27,7 +28,15 @@ This ADR is meant to specify the missing components and control necessary to ach Partition the responsibilities of the blockchain reactor into a set of components which communicate exclusively with events. Events will contain timestamps allowing each component to track time as internal state. The internal state will be mutated by a set of `handle*` which will produce event(s). The integration between components will happen in the reactor and reactor tests will then become integration tests between components. This design will be known as `v2`. ![v2 Blockchain Reactor Architecture -Diagram](https://github.com/tendermint/tendermint/blob/f9e556481654a24aeb689bdadaf5eab3ccd66829/docs/architecture/img/blockchain-reactor-v2.png) +Diagram](https://github.com/tendermint/tendermint/blob/584e67ac3fac220c5c3e0652e3582eca8231e814/docs/architecture/img/blockchain-reactor-v2.png) + +### Fast Sync Related Communication Channels + +The diagram below shows the fast sync routines and the types of channels and queues used to communicate with each other. +In addition the per reactor channels used by the sendRoutine to send messages over the Peer MConnection are shown. + +![v2 Blockchain Channels and Queues +Diagram](https://github.com/tendermint/tendermint/blob/5cf570690f989646fb3b615b734da503f038891f/docs/architecture/img/blockchain-v2-channels.png) ### Reactor changes in detail diff --git a/docs/architecture/img/blockchain-reactor-v2.png b/docs/architecture/img/blockchain-reactor-v2.png index 086bf71bd88d47e02fc4f47b563ebe365695836a..5f15333a0080cd93d01496d7b711e00d84d2083e 100644 GIT binary patch literal 171024 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%0AyOZVkp1ilOu3?*@M!GZ| zqfaCw*=66}PNYB2>JIQ3zS}1*i}{lxP!5{9p}!x53){#AnIhUH;$*muf>MAq2<> z-aebCuzsib-Av__WUd3J;Amdx8^_0=bv}B(h-zl+4XKRsBG#9i`Ol7=Z(~1c+-+QM zs!NB^RRDmF12;OK%lc<4^EDv6x6GBTQ_<$cm z!AsUWx;hE7|cgQmpHqWjd_btV;8>@DM|AW+W{THbX{-%2s z99f80+Wudx7lvpyoN`Z^KlWWL<>RNwQYoSLg{qTHwLsXgGYFr!)^_ne&NO>DInuK) zd{=mE`N;BH(beeLtg>E@@W4CZYi0Ud{1pgrV(!L$QHXHt12~9i)jgGEe%uBRc87LT zOnyczgfD~3F;4ZI8jnXXT3g7F3L-k>40qKSYC_zPt%|goo_5e; zbe))vC{mhA7tK}@#j$8X<6^Z3A*H%4_o|PGTTU;m=2s6c4UxjZjtl^?8!e~xZ2-PR z?|Z=nKVRBeEj1Co`cPyy`t)@%3IZ#Lo^1n~m4DOyS~xuQAJF+ts~`$kt^6KneER5hWr$pQ2dNJN3%iY7aOeJ1Y zX*BZ1-FZJl^p8K!)s8d=mmqagwXMIf?R^lt>%f;TTRdAG7+Mf}ld|lM$E!j z`oeyWF-2-$Qxb4n9akypa0_`r6)*(lmR?|``RqeFg9ODTeBbh1PgI1*q9%CIw=JtU z{~Pr8#o@8Y`F`yPTkBz!?>Aj=_(;3Q`ZM3@)*aQ-)&BsZ3`!>%P6zwI>%apU6^l*; z4xaM*@iP1uJhfeJ>I%VU{fziCeDuQh*1P1ua20vY0{|!)Mk$71M{30*<#*}?6-ivFa3pmfVuHU`sWMg`9J8R)4An% z&hyLfu7@ja@Wh&OB^Cam+{G%3zsrvgOznr-h$*_iF&e&OTkbOd53IVo>3nl;-5Ewo z4E9tpunF%Hh-w$TTd_UWn*7-?Fy7zvMPxd2wb>wTE*t(DUV=iO(uObmkAdDX@COd% z1iuGIS3SoJjqh6a_KG|yS5O|OzD&QRg2u{mOkhgA_D4ZTY>m>@Y~v7m=d;wn2fppX z3+_zE^Rd|t{7Lm7k8!A+leLY@Ra|>MexzmfC+Q1u>oHmU90!G2;n)61vK1e2R(<@W zME3B-BMF1-##G*oZ*`UG4dn_PKTHW$ z_4NW(xZZHgu^Y-4Z163-TUrvHSCN5xNi|j8llm8@>;3Y1$$q)pue*~lo zr8S*BuuhumDJE;ku^@e%v5?_}>S3glUHcZYu0o zo}!Rk>qmpNNzK|992W2WP)V;>3o+U;OP8BO>FEL?7vw_i#qQ3S2PY$#FSY6@ihlAp zyoDjyEY|sxSJL+Us&vMrm6_3le2%sK(F&6wKAN1)C*@i`vsAUaoq35vuQUg%cb@(L z@BISw7|4Ts9E5nA>mHTYi^&go3>utOT+zQTVrutXf7qZ-@42cC0vpTkdFsd|SN_j_ zfxX|(kR1;RyNnk*Xlj$W>xc3#%GE5?E3iUP3ij3p-Sc(a?*|2 z5d6S%ZKq_l9}|JzCsQTj^^dPGS#|@ylFlFHs+Ndk*4Zx+KJ5Iau{+wX#u%ApL*F`4}=YA%hi9Lc1H&WX!m6sO*n9a>(*0U-Ydd zqktTH85m{ZkwUztsIi9^Dmei(lm_w_slPk za+gJ+Rl<7xAte~W3~4x-5ZgiZ{=|Tbnayn3({Z}6Rqg+MNDe zQfoAL2{c&4K9IWo9w=4Hl>3ue=k|y47?&{!{8hT$@auSXTV^22uRnDTylrHNI0*g} zOo-m#RomMgpi@8a;nI%i0BTc-!S6gDp3i#RN*+m%J`sUF+rj0m4_#tEdO}mHUKA6l zb_E8VgnD1?ZedT9yeBZsWf7g&gus5*JGP+-c-^D@DVz|!%h_vfd438@er?KG;h8pY zVc31gn#hj}91=KNG;8eeOk~J{Oy$!u)$)VQW}19a4TYW0aT=Vh{O7%v-UL;s*sZOQ zId1XdTpTPy+zB~dGq$`=f9edzzkVv-Gju<4wE2kFa$7A~z!S&y7++!|RUGnZK>(FF9o!rzu5NJZ3KI_qbzYydH?%w1*M7Y-j^A>3&;QhT1U;=g6ecAt-(T{48ze}w zE=Sume3Qi?7F{9DhNop)90qMGqV%aZD?i924W#LCvrh+#CpMoZ)(9gky0?ouNg&ycg;eK_Mml1uUL>hO;@ zJSV$Bq%s-lUhsKx@8Buv!AA}BieT9&l8>wh{9~Xbnk_ffHaacAM1PK{6r)%b3q{{$ z4@Kb89Gi$#^T~$V@z}f(7Vtd%>8F*(?|}7*5nr^{GYghJ+n}g&c}nU-{tQR5ja=!a z{_kwyDTHJqOK8~15geK3N%Z*pv8hF^$B^Yt~Ql!=2S+wJn<^-_Oc6Z;bafS7M9*&d_o`&FC)_Vwl|h>L44VnKj&M*^5`Cg zmFiq+SXDscH9S3e3{MI4B=-JOfPV^70*$t(OXWxb!I7q{y3in#A;n@d#n<$@=-BwI zqP3%j5v-bARr%^qv?W8K_Xiaj?|iRaj&}(Sj7IdEUJ`1g6d0L*PGEb)h|6fyL{*tB zy_dwzZl2M;IbT83|7RpXjrZg4qj6K0DlgWsWC7zQ4AvsufcRO5m0II>uuJtSY2`t3 zffw~0rrq2$Z!-=tW)+)VAZQ6f*pMuVt_UaVP=dZIH!jQIJ!B01-CIPLI@=Ncdd9op zNF3bp80K&j6~?U3KDSWs&xeu5u)H6hIilI6d3jW{2^y; zwNQzlwLaX{_h-QF_QHXFArHtw4qL~ckqDEec^)%x$)^TpaF`ej+&|e|s)BIs&RVK{ z44p?OV^6LR$AR0U*4pGL7vN}oC|z$X&DfjMAMFl}ud(7K+nx6Xu_4oOU`kEEk&Q*W z#fP@pO@D8uyzb`kCDFz9*AvU>`0CQd`e((+=$-kv19vIQkyipV@E0YS$35mKIE$NH zg6pXlkRIVMSWDb%p%l%`CJ<^xMn2zd-8I$>ZQiU_W0L2yT5u-m<^lO>LrKH(Dzx+tE1vKY#zV-y*f9wHSJHX$$O()2+;TE z^_w_=N}Hho!>sN4Z`sYsYFtmjkN-3W$h@go(TQo5nuMfw>y7Sj{AQfI?2$UCkRAZwSiq5GY!6*?zFamWW;VMykRBt(b zC&@;@g)@&q-W}wy!B*fvN7b$fgOTwDgECL4sTX(VVzXeQ%)Qjpm{YxM{FmXn98PG* zMt!gV`P7vYlKR7eD2ao$ywg|EdjX3o%tr~_QK6|x%dspW%hN?-9b|%`^gMOeNf>kX z9jSu!OI5r!2Xj7w54sby>lnPSIQoz?1z1z1WdMUqhhbsWf%iPWbGk&!J^F5B{-@mQML)JiQNMgytH%Q+)f$He=> z_T#rdWTUrf-!}kDxAg}!kKscVG$ys`0Thc4WT%dP8izJYW-HEZ&K~^oRfGPz47KXB z_^XuSyk`=e{?JNtMD~`@uZG>g`%_)HXJsq8ht`irZUF%bGipoWlC>njj6&2an6?h? z=Ok*k9qx@|)%C`0?D!h4=ilsbgNHYg!n}&?n#+M8HvnvA@`C-JN}S z0f8LfH#c^VN2ErM8g{mi>=Ef-WzL!$oOWAH{@@q;if-5`wGJEn_6PYkw8egPj-0ls z(X8(&u*2gCUYGknlznwnR^7X;A}FPF3DT)_gLESxB7z{&2uLH{C0!Cq3KD_>QW7HF zAPv$;H%NEh=jHp(@1Ak)xMSS=4>IJ9z4v<7lXK1$szBuFWpYX8rxUX$RBD?cmrQTl z1b{Ql*A%_q{kFsQtcAvG#+q=5f9GZ-gh~`C*?b^=-J^@OkATwnctP>_JK^8lgbr!E zC%-A;dv0C{ZGomj_k8m^8W)v<(SnE8zgGDzKvM6fGmk$%q>vUKbHI;+wPG^!IcYDk!`P%cays!PK+o1$LFxGLX0B89)37_9cc8aeDJ;OF<+c)0sy%dm~#?m#8>Gpe~cn7{Z+ zmFR3zX8V&2JEHH(yb&?E>XudfZ`!tU#)>k~K0YR6V@-Vi)mMh#7mdT${-g7q_fCuH z&TcMqH>V^-CWuH6>pVPSm`U#1Z~ACHcuta5?z%nO^1yy+h6Vt5m@L+OMnQ-N>?xbF zg-dhEAj9dE#&T#Fu@}tl+=+=N^B1OsR`ml6p8H)&N2e=zjB1oz5oexQlBeeuLWH*> zQmo!ex}wu7U6i(0gHD0|*_(^BcE7fq;p5uVu^==2zQ^Z|o%8xer+?!bTXLhnNuNKG znLoS#>MnCK7fEg$3p-35m0Lnnv9ZV}xXw>cO6o(pWNjAuU37cefDUY^;YG2||7PXm zfPYE=&l->HHm90yiR4P0r=c5ITrLgQzUIv+63cnX;<535L0F zysdH?9x|OAy7l?j^Gm&MfGPZ(ZL6;~7FBz+DASTRs-(e7YG?3NsM>xe8BMGFuMc^; zXr}V=0~tpv?Wx1+QPBEWobQu?r&GnhjeECekZrc9v8eHs!&Q!r2LF(PLoFpL;w&S* z2+=o+0C!lxRl$4Jv-vHMO`?Y{4*B=wFz zSZY)o-eS$u@?(sm_IMH|i4orFTW{(s;bW4x^kU>wMRq^ID1JE%&(})(OR@Nd`!KZa`7Vkye|%3z-1T@rF#u02sPI@(tE(3cI6*y#00yv#F>VL#0*=YrgFr`N!2^ zE<0i8y|($5OsA|Vk7ELw45=_viC#_bz_clrQ!KH=mT>ID-5!XPer7u`}-0qZs*2HeR-0mP;z*xu;Bm7#9lCA* zdH`DsE4lG==#pYh^Bs{JPtslp1AGLx(#e9ef`sHzSD{rvIv3kKoC&D%RqFBF3^AP< z_ula_gX+u0-j#9Z^OMp|Tlrc={f3e?p4XxGc}UZ(0R|qvUar578$`nanNl8?QnZ_?`k!=62ne zdg0sU%3HxW*x4)^k(s)l;fV{oa|<2N=K_DZcsBo&u;9EkBi3L(cuuzKXd%=+viQAu zQp3Xo-US4F@dWx+KM=6S4J+|)x`=+2P$<~&@QCWab3kYzSiH|oOy_dwh~=G(^c1G4 zkPCSI{hdOmX`jvOMnRfxjshylk0cVwG?F)~Ja=#UWh==>+Rk}MCVhXN8zIT?cy`P8 z7!^-2ww>eHA)2;jymxV>jr}jNO2%wxa)qtY3=*vZHihl(S)0?ySY8=u(=8DB5J~2<*`LMZZ?Rn8-oU1uDk_`U|0?V`ogEM7Ti2PB+s4XnOP!n* z)N}LQhC4b>QU6p~i{>~k>q-1x5m-qj*392m&ceoO{FN%?Va(e*lk#6!fQFvKu7vOf zt+IF<$9#YNsc^Ub78Csjz2u&ri;$>flk*MX_`dRTCgP?BQG6$_ws(_nhpZF1>$f_! z%4?Y1B{}+Qk1bJx)`eHj4q}LpML9TC%gD0caBD#|{p73b4b|!jm9G98{jsVUv*D@_ z?fY^wVQK}k%9;p~BwuO$7d$xzC$iy$Hw4}g)g`aYg*iK#Ub4>Bw|!tWR(+wRE5W~H|1hliyfi8kT#7H63cr` zsQLS|MVdvH+-=DuCj5(u{Gx)2m3?{JLY)||A(d~6I(VzqE|;5G20Wp4@l(UELk2q* zlg<+M?v~t>?TbpA#NQqXd7p12(SPjbN)gAF_~-4r7>wqp=q32{OfpZey~4StrB!O5 z(yq`>EgnTJE#3hqxOw&P?($-w6-_Ix^KW;#!_j|SGjnIc&H6iv2Mfcd8sB4?3LB64 z4@L(V!id}5<1fNxt<}&|F~b?Mb^KZq(xp04emzqa;}=14jWe36+y0g|MKvBC5#!sk zQnN&MeO_1k{x$0}U;moc&k9jU+N3;UGZdcewT5c1171#z)!a^X-$BEjf8f2^9Fp!7 zx(EQ6##)jT#Z83(Ufq|qL-?swX$gFRWXdlp*Sz2yvUHUo%_6; zO5ENcxVLUZQO0?^lOPnuoUB@;^bpj3dYyFlM#9MHenA?)v9@d47xm3dZOT3QYt0$1 zLcksACe#@v9VPV4GFsBbmz;CWLB}qi9;!ey)fK_K&S0G{s@AY26Lz+`_S@J!z|O8~@9zHig}G4e?$KY#>+x>s_tsxS|2Hj5 z1nP_wF{m>bG*sNKpej9p_wMH@`xiyMYQqtYQE|D2LoFSqS4BQ$jVWN<##2okL+AWF z`N!ol4y$gh&s$pq8na9%|A0EHchFM!v^1Z=pu8m=M(lHMQMXl9@s)l&?e=U!@1_?T zw%26KZXDY?Mym<0YGs@#7vh9Vj!)8hsHv_VAHK_i9^FZ zQ+RpOE2BAhI$LTU(6Q8)QES)stT7@6+DWM%p_bO&4$rArZ80LR7}8g1H!qJYMkvD(agj6-v-)mhA_*=8k8)gO%Tgpv%xj{UQ=@88(1BBYBz01HZ$pMNACR zWlU7MRtkVhZ7Y5hFenU(Vs3bISbrn7J+)_E@EIP&1i@QUbsTUlf&m#akYwA4!j=5# zF@8~m_zY)Gctw6u{uK|wX-JQN=C5|d>Q9g#=yoE#6H@m<}%F5*jv9bZ&jeJSlvEyJlu&WaK_kF2O%ou^+eLEmns2S)N@@_Iwf1Enf4V z=(fh-wM&|uD2axD{~4-MBDXjIGVTfH-^9D_gqY2I{FYsMHJo>V;b2L=WeF$ZimnF9 z+J+AQE(jPU-66#}F2mz6nBvmNM+4GSTQbDk4KX86aU9425Q$XXD&h(Jk$91Mbr(ce zcTxV|p1^MQ)OV8}Y?c??Ff?nZr!nXMXW{>hd%mbfyhj5uWzT&fvj(_u?CYDf(~k}2 zL7tt%?HAcR(>u~_+?K|%g*EK8(0GJAR=#d^gpxUL#?x;hs08<87s6qpzeDe8;saJ^ zQ83pUnPJcz!f!kK)=zfGhI)DuuI^{~d5J6H{tEBI)v1?;4bfed^zwBdo7!NWNG+L; zNz7-$?Af+^BZsgcU@u`H0{|!^<#rSZg(DA>Ln?zz)DL`FO>!Mcat4`PN+kx(=!(hw zI1irZ+b?q8)k2;?q{ro434~Yav6VP%Vy$@j&?|mU0TtX)CIbl!e=Q=;ya4Yx(n z*e-UGgx%%&_5bid5Ju$)5RCsYDK7TRr)sL(N!nA*z-3$l+Ty9nzmrBT#H8`2TKE;z zgjx?Vus(>!cmaD%e2!?MnodSA-WkX=DusIDTiFOEU6b6$8~?O8x)IdG3U{8Lh(!q! zn?~#-GB(+JFa`a0#(ZZn8OC(>%e~jnz9J^AFyOqxy#{&Pff?De>V~r-9xOFBKUnsS z)(#a}g)n>k_GuDXgG@hxZ26d*Uh#1qoX6|*@F@W3~(DOdWLUrlM-T4 zh)BUuo9y4Kf;U$G%^tkkA8LsGk@KpA6=6R>Ye67e{xC1rtO596Y+TsuV&Yl44Gs@8SUg8nbE;GVDSH!~u zXcuBjIh7`#e$#?4v^*HMu1$OT`4NIVd_#8d3H3CBK;{~7uN32ITiiu#i|;{0H2++D zK4Q=0E=*YefjwpVT{7@a8W1?A|3B|?bM*Ty%dwK{Ud4ZY*xo2IY;~N*y!!}wBDAj5 zd>EV0>gD(4!)CeK&5Ggtk{ACx-3JH+F`c<7%Ri4`1Az>9sMS+F#LB@LbZ}q$C-)sP zes6$MQyO>1BTk82>`9HR@i;XAlhPb zEgIfU_!mJnwz<6UfHDbb%6<2C1f0IuMDkl3c_L0M@XkGU(_YHmV>nnXV6+WL+FLsd z3MH1~LqP|Kje4~;G?4smVaWi7pcfcg+~z}=sh%fHjF0Y6h>%qs?5zyk`u3s}eZBC5 z@Z_c^z#FX&pwr^zvhK-Zj>>X{CKF1AwZ-Bq^e+SQ;PsZaWC5-_GX?leM)|Kt-hjoeJ_w-MCb}7guKL%ml`wW9Cd`fiSI40}qQqz8Mq& zzHXWV^_g>K92iu8k(bjr?U$bb1?AT#=v8pPD7RuMkh2!$DDgt9G%B^vVF6o;XZUv6 z`OWRUCr($(?{f|JhwFZh{I}+V8se`)m)GBZX+%;2z|f~Cj~_RhWwi(_U8Dt3a{{(e zkv83Ae?91@>C92O+B_X-?ro=&F$ElxW&iC;uV2?nxdake(r*SJKD98Ux~B_W>gc&W zw^jjv#5_Ix@TpGJ9p4;%p#bl$BtO#V=b5eXYNUv!+x0lStu>bA+bM_t;}aK-50PlJ zY(fz`13%;)9NEwcG|kut{XQ>wVo&i37kAxw)P$1Y8ISS-jtK)4d~V!f^~60SmvQO;WtIcZXLT&TalsY= z=i<%O{>MpZ_+Aqh48SDu1*>U)u!G|^?#W^s{Tk=$6K4>g*m&Eb{%?g;*hHBnDIv3l zHz04O&yIJ2KRv=gBac!PYnIVB66v?vh|*Vy}N z=#XY?ADx@~!Nqhlh($)cC^gduEgo_;@o~{N@En&?#S{OT5{bE7INU+z^MTZ=Cq+24 z%yN8oqQdTr=6Cv?g^s|2niKR?j{{AcO|4xN?>Ynr7D)-cLZ#1yA%HhjwrxvJSjlZ;sjnTKQ;X@Dq-;w>6xbG1*AD zO|h4y)qmQ*#wd8Eup4=8fScQL3?G~$hqP-|1-PdUp%TQ&vS3MzZ0I7>AYL0Fu*sy6 zbA>beCx9V-{s{2iIjan)EbqR{Y%mY|JxbWkk^QU$Zg}$7nBjqTngq(+47YJ7F`S+E zfREF5Rtl3`Xv7&9)E_$EEA&l>(~1>XyMmslX0_|SFMpz&qX|v{_v?PW!;L>j8Ii8D0Is|{D+&Wc1XYm60Xp7E#D39OcggcsEY4w5>eg4j`4=m^ky3TeUg;|A?PB<*1^M56buGsg?M{3cVXwPx+-!JcT`u&t>(ED5f3vR zI3bL$09yEo!)wC@U7@eUvM7o!mBd?iCK(BT^jA80Z|?D0XzUu8 zrNEjd5$!&a4Z&}(qLz&a0~Mb5s$xitxv+!{n}YP8#xd$T)a5ag-U^F}xJpKy80t=K zbq0Xs_Sa2&X+QaR^mxEg$?NQ>Xw%`3qmoJWo!Vgsoq^+G?EwK^(lyzU{I#mA@&Eh? z#31Ufa{d-U%#psy^AYh%7t3<(0nJF?VTXus^hl3Es_5~@5OCRse||co!w7Y#K-ape z*7Ni)AJ&-KmbE{U2j~IK%+2AQ#hA-vBe1p)5!!8_48(7F=fNHKgi|FBftMYdfQ`q$-?6jA7K2E9phKzBbxeYu|2Aqa~-g8iA7d)v9 zDn?V+#N$pVH_B3YYYf=s`+#mNf50(V6kwW~3hY<3bKtGpk! z77_Y9{cBCIV8#8%qrO=Gh^vQ^&>f7WHYgP6t=cS?Gxx0YYhvYEAmD!f9(~+f9~}_7 zzA{x?J9q2Q7e`ar4J={ghiwMY`p{`vJV;fjZnY=nm>;eVcAQl&_0;ha@@3d`{8Nlz ze+sMzcDEAB320!=P1pOi-0&rD!yjX;+HPiB-&5Zkl3`PvtWrr9cG=0qG1s(@Bqv5_ zrhH)5+&Vv;N;)f1$2k5Fe#3F-=lAd%Ws>)xR-3+!%qX}qR*C`nNVCt4Tb+%i5W!|3 zt8M0+ayKN;Qt{q(>b}CXAiF52 z5o8J{;@+umu=#-58N$<^RxYi>(?hWoxjn1Q(?e629PI8&5L%8{3_5E>s5_C7j?avK_Wr?arsRV_RsG+NNX*}%SgoI`pDenp55csAe`N z5EcN|e&VJS3I-uJiWHog*XoUxNy8$JxPjUu4yU`y_;8h!r#HK@&MXRf zjXl>NQx_&JWp$z~g!*)gs|1pXSTO4 zo3xx~HkgyJCfGPOn1_I(?^_y;56Tdfx?dXcpvny99}|ETyEaW+GkZjxv_Y z9z+)8u3F{uRW0{}$_q2^4l|HGBNMjQ^-i(ahW{66_!OsChk3Do(Po*c?5rYdrQQp4KD}I5yReBu1 z3e%4S3O%nb(lQ;1u&ez+FN1}k=#OU+0fd03tT38{#TwJH)JS@_0kDagj{?f)#snz z#J?QsJ5}Q?Ygt(wZh;gES(tY4Ds@vq{^_t5pk3*x59tEp>>tmQ6$aVc-)z?@>yRY0 zf7**f>f7ZIKlS~09s=#MZ!ydJAexwjTU8#mv}o?(2M;=#7w2SyCodI%qzbJzj@SLP zz?``O=PpQpg_BaD!FP?o>&@TQKVApbZimo_BY?6T4N45})xq2h*!hc75zeLtcr>pJ zhd(~J7>&n5$bEFPCTb#fQL~(gy@>|<1AeZ1IJ*R#xIvb3JtmV)sOzpnZ(CXvz$RXlO;%3yPhim;GMQlw(m6vxINW3Sx!OE z^urOJvelskoMt*1m{LNB0IvG^hdLMpqL zNxD>U=k(@y^99M^012|tqcAv9ifXLPG8DugPl|KYzulFIB)4q%^qWips^VVfNB_sN zOnY$jZ<;}dXk25o+i?Kph@yiDq1U4X&d=>$dkb;8)SAUca_``LC3?jmC#r zy~nvMx0wc%jCa-}h4rPG!ZsjaAkLN3l=Er(yQ{TwDcfN)hMNsv_gC!D>8$+YWclgZ zrZ%iDxAPMRn!;_2ioYGlpB3Ud;~YY`tG>chy#vj|aHtC>s-P-0%c)W6)c1= z#ATXiU!HVN=)7wI#v9L(cY<`m(NCw%6dXg6faW%$8fUHsqKYjB6wS%HK2l#kd+3jg zLqH3+QDD9Px}FL27B9x*rEh`NFL0A8gkiR2i?@PLLJknqlfEim4EP{byMDJ|sqB^+ zMEP69EA4^C@Ly19Gw&8_4_gk~69&xl{8^o)YnC2Iq{lVRN=s92kotPl9MM!t?qgk8)U~kzP>;9=t%!?> zN3)5%ju&#_gjRwN?q^47>A-x#&Zm=R}#J$C$gEs}(*aZ5+%ePGNo~QdjsTk|#fM>Z`M=fg4B3>iMNA<3bmKZdKoGsPBEBJ^aN&~|_ifjqg&dou77ur2AU}m=(D`|Ico#y$7CFP=&9skQ* zkRl#3)U@}*4oOG94)67mSrW8EH7Zdl9suM2=|gY3iPAU2?>S%WjKyJlhqp$^{I5dU;9N!&r(G^ zdMoTqV!{R&Th7xw>pcv>aPQg%tBKE<>`xYN*0RuY^skzH8zM?ZCS#Hr`wp9TEA5*u z)z&#Fx+f8O+ih{NX1PhB?T15wZi0;w}`#%iA_g$<95GNu9^eI#i2ZZ4Yc0FGGjVr$sN|1&`M^nly#CxU=A3M^ut2COBQH;ErxW(bmB)5 z4jvQvPG+gU2UlhyRX<=8ILGc*v$uwmnk;769D62lnMAbzZQ5Q=RHU+p4xH&wUIGp) zlSKc+7@Y(Qci&zR+MR{FWN!%mns(Uib_Cp!CrQ$TKkn5b~O==yyCw{$e z_iW9M1zNiK3zuo55Z`;~BXpwvL)E6G?CVHe@>w@fs&q~!c3JMFn!5i8^M(N{k}K&` zZzDM#X?ZP?HpsY1lx4L{`GhGtVwn(7$e>Kd<>x^9&5c7FH)cEpR~yDa+iK_)pvGm9 zR6&)X5N)TjZp6=HC-O9`#Kqu*fabeVyTMzPMdh&?kIMFY^24v`KQA3&b{TvAg6?I~ zSuDN?lME+LrPsLy-MZgeYz*Wj{fJB{l1lw7k3%`0=8Zp!CC}vsBG}vS34GqZLi?nPmd`Fney5aF{{EpycNgw9wba0v_zbld;OutM`?u%ZSs5`3j;gqS@l$PE6}L zYk%@OBmhx68Bj?}AE4b6EU7gx%2ofSs86yOFwc)qxxxb0r%2<>YuZ~!Yn5G2TTRUL z^3BUdpFJQen93J)085;-M-iQfIa)H!c7JuSr6Z>ORfT#)qYOv1?-#H0_%oh=?CAi% zD5O2$_*j2|kI-!Pc)gs1$`%F|>=q;JF|&1(Ee058oyF-d+N0@|9uJZUyou+=@Hjmv zb<)V*w|jmH?SEqE={P-76IlqeA6v=t`Gb(GS|d^Vzu$Yy9O$gXr#~roKYpXTiZ8>s zsPZY(rtOeK!qU9VJ!0>1=BJVGCdzoYU8_%C4`lK1EkM2}3#udRHq&BLo}GW?IHLK6 z$^2TM$QdPp1eV!Q-s={Fe7OjvzPD<{EB@`rVA57i9T094Mer^7Z%LMDP6z1!VUB2Q zaZowRAa1ukTO*ptPK51*s{Fiyq^Kcn+?sPl_i6HRu|e++Ojb3uUFo0Bf0=ORPzzrD`6zGw2vYqS6{0;#K>n z)@$lngE6Yp51Sbz3o2JlICY&w*JFRMwm|~U_cRxx)^vEN7|(v4JVrkY-wb-1W^a{X z%yR$1V^7nn%T=Q3c0cD&aBtT!?-YeBwL{jSD=87UX0cCKv~k~J{@IiYg%c+eGNT@V4bKUq4Z5E#%6*R8M07w* zA{%JEdOY#jG`&Mor9(pPmb!V!-|bPS z*r1Z^hlQRVMQG0E*% zTikLHIq~XI{#P{o^_mizrz5U8P|RqV#MI>OLqfHv|w{t8C_%5bSB*H$9p# zUH>%7$=`I}byK}S=MSnU)CoLH*BDm^VX}d`gg?SGYdP_w&murNjxl0j{9QP~JX7&^ z4&m$7xV7g8T63ie);dXi?3W0PZW?_S-zO7Ve`)k|d~A{&rKoL?` z$*_A5c}-=`A}8zhbxl}AAm7p3(B?%i!^e58Jm!10ZizrZWP7F{RG#+pfaB`QYr6+U znqcCW78ndb)%M2@{^Vc@P?NE;qO(;*{U%jUrLHoc!AI#Y=E_$rhTE9K#NC(iE}g1+ z8X=p4&e!M04)wWASf*UXn}tWJEGdKY0yi5SS8O{E87=xNqc*GAWNMTHs3}pzT(yhu zxL+U&0Bq}@=nKUlg|}{^DM!D zHk54}>&Miuoz7Rk+`sPeW)4#cQ>;WC)ey@UMWK9#DT}!f`O0Ux9+^dtJeYH|fB%JT z#v77gKAg{EK$rXLlz>JPJ4E2+3JD27N2RkI|Bk(C`MgB{n)ozzC(kXDUK1@-t%hBh z=}!S;5YP|%J96eKbCV}9vq{cGQ*rJmV%k3RJrS;cw5bm&*cH?CyUH-zg_Y_-{HZh? zO8@beom@l^!9xkukfw?fc(y_yr`+PQyGSA)EkW%s>-vz_YGP#mS>_YyP+7zfy@%$Z zRKHe#0kOQY0=j4TT^`Pv;ZE~MR69^}!4ia|@?t3lsHwW=y{ss1H*7cFePQ(3c`+=L zkJe5E=G0T2jmfds;pWy~LzWHL^t_#}iClamQ2sO$qB-7#LoO%{Tf!c9kdlxc+#Vx2 znwbQQ09jP0pThP#s%?C#cJ?nypnZ#$Q_FDGFEEFqHbsf7ni|N@^f}v*P5}<*kPM7c zd)&E{xc!Nw`q;rZ;}-w`wau3r24!cE`cp2pWn7y_gU{*BBPKIY1OKA9@3~313AT+4#+_3^!#|i>+1Rghv;Lyu^p`ybt(wC47W?N&>V!0w zD=bK=^F7*sr=0+oaKl8}Nk|#?TX&1#3!t^@A^R%q8Bp~?7bww@({B{a^?rQ<;#S zvKwVwA@1^ zvS8LT#5+EBmDw0C#~Lku#buYuZ`&{*0y1zV*3106f=>%G<>ZP|(x%IJZ0`y69F5NV zAtik=)uNCrPuPvtNycP2fCOT>y_%hmp0u$%j#<+HS=pp?y0Cz2NCY%Qf9F2$p-OqT zS)dG9B<3uRKkvN$(oW*tOd#6&r!Cr)P}Wh9SPhSS%3 zn5{OvA{v^|=1&XRv$-hPcYd0NvKO{Ku6oSH$K7htpV|IUl7C0Ywpr2@z(dJ6s{jkx z;y~&qtX>YPKV>&Vvj}@;NQ)O5dhy&)_?is==+3s61;s_RwHWB} z^{zlp>i561UmRT5_;uK1P6(7NbZZ!&kn!8p1+hb?oJW)?^GWhdoQ-E2@nb<4R;r@D zP0KzjmQjhZ1ogK?55lEs3e*rG3_@mh-Q^XW7BWwTI98t7+f#E@dos|1V2j$5nx-Ii z4IBKXHGGy-yCkG;hUiL!PvhrmkS)SoZBb_7yv1xZF9njO`D@leRDUr09>+39p)K6D zFbv|hwn(s!=2PE{tS?%LGRb}S*U6pm<1utD)^k=nLuSt==oI67oz(@uqhx!0tiY0u z04!7#H_wMkyh2Fx6~5L73Z998s5yBj%y19I`@sW0`BZmV>O^1o6knTE!~#9UHzdfH znL25mdbWYc&l?+G`hb%0lb#%As8}y?6sinbkk5~%Li%4hlu5`qP1C-nQsSCTq23nH zaZs)k;GggQPMsyqcM{pC9^e|_I#=h^Pgq%9U9xvlt9I!A>exNtd9*c?5Ft8s5U3p{ zMN1FGI!Awj`xd)F>*){A!)OKdn}TpyC6LBBHeOx-c*r5}z6dYr4~IG^Dd&;~wuD4Y z+<;zeVba!9EW^o$2?pjmt_wPAo-}_cQS7f`vC@;$CoqQ<)MfCd+{A*CtN)ocvR_7vrRL>^B=BT z21)GtzNDEPL35wIZ@m~ntQIr%f34PgVv+;5XMYvgd`|yzsGj-X4=yydtMh(+3^7gEX%&{R+M!ibG^PE#Bxx znt&k6Dmo0iRM2`G2#ya6*8H+WF0n=XThuWs1)mCLNo~Vu-oe&Sq#?f2sv$7N%sxgT zIu>;+stRfMCLD&fyESvf{ch%?F*6@K2~$&sKEWsZxNNu`mf7SfTPyyX%;U!9qH$NE zz@|Lmw`HPbj9x0ny>Y32m1WTpU2l|TlN6U0v`;$2Vp~!Vf(d9mQe(?EW!I*wYS<$* z%B{4g`P4Z7;yN4BXz0&3%aNPjM4W!*h((0OlH?3WO#E?SSY*LWcKJjihl7Ntg-zqa zv{gvWx_Fx-{I=}2%OUY6IE-jat#OfB=m@j7J;RT9MHs)q+4@nKV&{F_T--ULgABwp zK`tdqzt()P<{2((5;2UM4No|w!#X1gQn;KyH-$tg>XCHJk~I1XVk{`iB>XP! z4R6gkESCChge!ln|I{}M#*dbwVzfBvELet)vjP{zAix`j2O}Qovot3iBZ)TyeFmg zNvVcn+6mRinQG@=sNx>Q-Z;O?wpUPSRpr-KS(B4P2mR(YXAjZD(5bg3FhQ`g#oIjo z6be;I@J%h<`R2Bf(UXeE)eITXZny9Q7kBtNf1Cdr4f;rg!M^Jhum} z)uD#v<4c<<7s`T?FibtF)zETHv4!q^fE<+25|&5k#nKsQ0VdDA~c{Cpj)9q?aNBd zzVkG2o+#^H!DcoaB@tc9A(Wi*V_mU|m9*dbaB}#I4Kk-p>4&)b3s%3Ub$(#cN^Ygj zz2?ai7npo2-V1N6%M961kev3W833%x1szs@i2vy6kovY=0obsiNP| zV8Aw)oF*Cg^Y&TI$?AipaN>YyCIedDhDQI}H``97?IUzMBW@4Kh#6zHZ;lcqbzu;% z87u+NsREU}rI@*JPP%=BfQ_W(%Sa>#n}IE~2eg-Ymu4=}?NSTddTbSWHSJqf2i#;@ zSo`^)@(X{zYlozlNA}-*5;OpojON|HUoNJI)7B`hME!VcMb8k z3NSZ4hyvWG)(vQ1yH68p#ba_tH5q<}u*aGh_=QYLpI0MH8+D2Qts$~_m0A^FZb76R zh36*UYa`sypwX|Y3`FR1DI6Et@uzPO4%UWyJ|c09Tj$jg-?f(8348kEa;vkm2kjFA zy!nS53OvcLAyCsApwf=th&-pqMoREfMYV2G{bS7-5Lc=Et>KWFeII~I`@{2zw1YR( zXQ3bT$AFTW9z%Fs;u2rENi>w-nGXKy-C7HeIZ2i2pTeJ5RIVr|4 zx@yfuVN%+)rUxF%m*JE{e#j%EoGt(wl$Nto<>)76Wo7h^Yz6JVapBvryM%`&*h&_rQWYhUOU)x{KFio*d+EBZ7%Fr^@R83J77g~t8 zU{Fzkprz^sM8!5U-F|2sD&8HVe$LEM+_ucQC>9(=VtAkD88T-GOz>?I6%35{|ITUw zMq}at&0&pJhTb^kOQ>3KAI4>$O1ZT~N}Kc0(~GvPsa7-nH>ic%s8qjDY3|QZVP`&( z9?^n+`K!6D_~BAoInu!O{@-K}qjp^JN>Lq~>8JdE(ymi>5A^(_{R zHshA%C@LRUQN6`_dGCCZ&wP7XIIz>9!4k-0e?jT$JT%lis;$=HR zMPAeg;G-ky(xDdTC;KbbVK3exSVBKJLV#{z6xCO8Bjdh0oeYazL`1qr&wB>Wl>Jyy zPP+~BQf>{T9+A^LfO)smE2mfhgc!Z-&7is-l5+{1SJR|KN#V9$ghcWD9mTH}l(g8h zDcXUScI)bHK)b&~Lk5udBH?$l;e1N~E|zq#)e7Myesr)DvR{6urP0~mUIZ%48UVih zzH#^af-kL%aVXt@%{^QiJU?=2s3;5)9PIJ`YP}PR+RpoO7M0-Om0MbmqfznZb2x?Q zWCXdencvOZ#ew9mZGfUCoIrb~T7#@1q>uOQxsFRSNX(l+$yE%qJO=*{7RmT|OL^P{ z;W&xorRGlEb9*-s*lx!AK2x5jT`4Y$Bn&bL2W2_ye7-+SR4RDyIVzHriPA^GL2z~p zghZ*%vuF>X+@&(>RaQkX6mo)1`i>FC*~u897L3!8myuyS<+~UhW+J^1*_n5$_3d^_ z9iXr=H(4&$L2l7uV`5a$aorShO7sgOoNwDu29`jxW=zYv`W3W03gM8JQIEqvCWjMt z%8!EDTH0HatwBL zj6=)oHWi$9B9gBQkX_f7my)rhiO)E29bD_Yua`kwWaR1|GdFn>zQ*O1gP(FO%>@p5 zu%mLlKPo%jCreQZ(0F1mV>c$m9usoEkh}PO|4rEx&(9iXCk_UNJ)#spok5x4g1z&8m7|Js0LWS-Lup#%tjW ziqqwcNDHtNt3ZnuDJh;p6(#5&JRKwa$ZU%wPe2n%A!-$;?U-;5-;FNtXhJcQfPJ_MHXgcSs zo#qK(S8Nm{&?+p|qBOK!2Q0fo1QXm~DIC}Hu%LY{zp{IQAC4k2z_)3tZlUlx>%%5r z8QAumD0v;@vE5A6-8a`lYHg2E%G?iANy`3Y&hy!r`;q>QYuB!!Tggj_Ka)Yq1|StL zkoaQ#>x_PibACO?JOTLATovXsbY<`N96QhM2|4$|fecTM(Cwv6(a!b;lpPn|KmSQ) z^VZnNPM%iTNQf1DQkPR$K8p$xW*g2p_T&+}##E7EpxU<=gZUMUcXOe!kD)v}g*gUw zpT%=+hxhN{c*H){?7z;0B3J-eJ3ZWV!Ykjby_|I1oV0vf54P^KF=atqhkVEOLpvEB zKD|?dhSA)5xh?%rzDYAIg{DZ%z^U544fvDX|aUF7e{|qqfrw(oc|8 z=$|NNu~zRr+O$#@+I|}j zXL{vSah?2>zu6N&HlGiCB3Bt*g^{~G5o3seOQh@GC+M_6R<`(67=^OXw2#_tK1w$E zf7Nw0Kuwl$+}0$$MJAR|cj`oJr7?}mEy)2_fLq2?%y2XciNI_Ot_}yzlyRtV&K#i= zl0}H|72_jx$j5=VR#L~z4;5a!?9}@@IgB>bJS4l9U%%ER%++0QUs@6g~9p6N;9?|Ze|(-dwa zSF7*X^0$5_EY;Qi8gSvC#(2g?G?e!D>KL|=dQ&vcayLg+uEd$LQ}Xuc8xuhIhS16T zASh00z|w%$M7858-$Xe#yqvz1n!4&Mc#)|}WP*<$73cS^%RhFC8(*FBS4=ltzdPQM z|M{&)03}oBBNX4dcs$wK{xcHWJ=YxiLSYC_-D==?EPnO+OB3sW01^^L&r+r3Z%wPd z01j|*x5zGMW|uW{Rpc;nfs1Yj3@$di9hA+k{*2ZHL#W@%%@ue$HQG*H=xEkQPDTI35?{e|( z9~=+rp*DG?B1<(RG@O-a5`l@U!6GUE1QZ!61Y=s-CL_2OYRQmk%#yRyKYIkjk#%_V zfb`yJb_J$I7WgIuowX&qJM?U`hyY74n7W`RRs-<#x)mDcT4&mlXL9jYDMh!Jk)R0J zw=PY2WyDzXAzw}aE`g{knZg6u#aCnX6+kI0g6;&Z<-osE;O@2gT9)DrXDoMx_k=yM z#U8h8+i;pXs?iXs5q%7h`28tnLu;tqYd_Na)IfW(v}vAo1t}qn&s%?@-I2j7s2t_{ zjWwmb3ClJ-Xr2;}m+h;$h)&SkW3k%vk3b)K;T(h=?@Mi4`h0oy7*?v2mP@>xd@sUk z8tAS|EZ@ZIo1=duW<_?5JhuufX(;_Y(oQgUuYk8$KRf`{fU3R+{vspFz0q)F+iC>C z7K4-b&{GLtMK{)Dl~+AE{|PJep0>kBuTc>NqmDbzNp_@=Qut9O{*cQ#TkF>(HriO8 z%CHGAm|teIJFEuvCpaCs0>m7N!}Q-I&;=!%zP$Tgge1ntl=B}yWZvkcH)5yR#*|tQ zIt=6^>J8-f5$Mww(?npYGoP?rlRd76l9?rWAgtqsAw+`hZo~)46YH@<=wsCpF_fh%;_R z9F02Q(;0$&#-YwV5e|`l zq4zMh3`V$z2jTa47du!6YjOliaRHzI1+B9ZuJ+O{^-uE$v=3p%TaxywT@RjrlZvit z_jN`xm@!#Q|4~6wAnr*QFdS0p8J*#nC!G_}zkzFdvy!aZ?Y@if;f3&nE8mDt{~t$( B=v@E+ literal 120617 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

Ah*9Vo97=Fg^|9GhFQ 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 diff --git a/docs/architecture/img/blockchain-v2-channels.png b/docs/architecture/img/blockchain-v2-channels.png new file mode 100644 index 0000000000000000000000000000000000000000..69886da95d6b00b974c241fb54a29d422f769a9d GIT binary patch literal 109340 zcmaHS3p~^R_kWa%ltLk;+)6^buuTfP+Ldi=vy}?l*nMVZTQ|~8xhIi|P|`(3sYK~c z=&C|Dg)Y)fDRle4rqB2L`ThQn|Ks07V|%~%x}4X!JkRr-cPc$NXtLQ%voT}FOePU= z%rRrek;jZNJ~eSX_{4fjd-a$x7Pr;-aJ5V;6iEeRT;bTif4RaSv5I)LD;(zvgC!;= zdhu0}iC!WFcn2=Y6nv3TB;fzO41>TRo)DNP)H@6YafPG3k>CY_@`6DjyubHH#t7v9 zj73xEMN(;`D~#ad1p!03(ITToajG#QYjU11W^=`+Asjm+G`+EC$LE(nkLnDQeD$)NK#;`k$Kou`i$p3B*3iCo57HYU4P)El6{gjEJs^ema z;XEOCr2r$)ymcoQ_RNH(3thoMJ#(l#jOtO-IKBqYa`!QaD1sH0KGPb{Y3$>6t6#q^2hCW1j=!E`}1O)TIG64a7lTp~iA5DTNLgj}*L z0R`m}VT5obQ5VZq31ob}KrBPT5>Wy2p zUP*!oG#nH%7*6+&^$q36BjDUH0t|-o4h7nVq3X3VETEtG0Q<54Vi9VCoP|s!79FnC66297NFoOxE+NstK@-3w z@Cwn1@KO|(h~r3Ou`0Y0mmmQb$S84?mczve`FL6)mK?;@GAU|?Lh36bu;L~0RNo*S z6h`un_2EVZYvn97OTr?0t6~Vqm|z+!DwrjT6vuNhk@({Uf5?VqaqYc6%L8;Ng zV2V)1q$!CBLEboTWfUs}+=WMlk6RYQd@kH9h5SA(gj}`NnP&`(W zNW?`kqQzncO~a9~iBVCANF|O4RwUx6fZ<_;dK8Z;OF&TBz&gP9(Y`3~8ZW~sed6RH zC>o4JiWDb^LZX*9>Vn&hwwx}2#h>I9D@_1G(0pGA0>rrV+g(~sT>j# z5{(wI!H5tAhl=s$#0u%)4cUi*mr0?CA}JBZ4g8!VxO>BU58e5@}k)R&77SOO0+gc6c&jL^A5r&!M*YE!K^4c4tU*(lP^34T8g8&6q{dsr3gzi}K`Heh4v<$I<8+p*~c!UYkH= z!DICyu`oUdhTs81V@3zd6j32!j*KZoM`Jm`8Xq#22oENbArv-2h2bUg@m#t{i$thl z=wN1OES0PdPYf5+*^p=)MHL$y6B8K|rl5<%!)3fMHHIz~Nb#W{1mWQ*JdK45gJX3% z6hcLnFbE-XPC^im3?PCeR6!2H$x$*XlP6b^$ðMKVH-S72~TqLj|jQJGP`(K0d7 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

_Ec~xce+}wz*Lw6oPvO3LR@35LX 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 Date: Wed, 19 Feb 2020 15:21:27 +0000 Subject: [PATCH 16/74] consensus: reduce log severity for ErrVoteNonDeterministicSignature (#4431) from error to debug Closes #3583 --- consensus/state.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/consensus/state.go b/consensus/state.go index 05c70b652..d543c9c84 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1661,6 +1661,8 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) { } cs.evpool.AddEvidence(voteErr.DuplicateVoteEvidence) return added, err + } else if err == types.ErrVoteNonDeterministicSignature { + cs.Logger.Debug("Vote has non-deterministic signature", "err", err) } else { // Either // 1) bad peer OR From 7ab81a7fdd8ed266cb4ccd49abd0d72df4d92f52 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2020 17:26:23 +0100 Subject: [PATCH 17/74] deps: bump github.com/stretchr/testify from 1.4.0 to 1.5.0 (#4435) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.4.0...v1.5.0) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7b669b1db..b828c3c4f 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.6.2 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.5.0 github.com/tendermint/go-amino v0.14.1 github.com/tendermint/tm-db v0.4.0 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 diff --git a/go.sum b/go.sum index e606cd3fa..ff0cf1d88 100644 --- a/go.sum +++ b/go.sum @@ -374,6 +374,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= +github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= From 24c8b351c671bd5d12ed4dce405d1858cc2532f2 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Thu, 20 Feb 2020 11:34:18 +0100 Subject: [PATCH 18/74] rpc: fix TxSearch test nits (#4446) --- rpc/client/rpc_test.go | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index c1418c816..90612d5e0 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -18,7 +18,6 @@ import ( "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/tmhash" - tmbytes "github.com/tendermint/tendermint/libs/bytes" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" mempl "github.com/tendermint/tendermint/mempool" @@ -419,50 +418,45 @@ func TestTxSearch(t *testing.T) { c := getHTTPClient() // first we broadcast a few txs - var tx []byte - var txHeight int64 - var txHash tmbytes.HexBytes for i := 0; i < 10; i++ { - _, _, tx = MakeTxKV() - res, err := c.BroadcastTxCommit(tx) + _, _, tx := MakeTxKV() + _, err := c.BroadcastTxCommit(tx) require.NoError(t, err) - txHeight = res.Height - txHash = res.Hash } - // Since we're not using an isolated test server, we'll have lingering transactions + // since we're not using an isolated test server, we'll have lingering transactions // from other tests as well result, err := c.TxSearch("tx.height >= 0", true, 1, 100, "asc") require.NoError(t, err) txCount := len(result.Txs) + // pick out the last tx to have something to search for in tests + find := result.Txs[len(result.Txs)-1] anotherTxHash := types.Tx("a different tx").Hash() for i, c := range GetClients() { t.Logf("client %d", i) // now we query for the tx. - // since there's only one tx, we know index=0. - result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", txHash), true, 1, 30, "asc") + result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", find.Hash), true, 1, 30, "asc") require.Nil(t, err) require.Len(t, result.Txs, 1) - require.Equal(t, txHash, result.Txs[0].Hash) + require.Equal(t, find.Hash, result.Txs[0].Hash) ptx := result.Txs[0] - assert.EqualValues(t, txHeight, ptx.Height) - assert.EqualValues(t, tx, ptx.Tx) + assert.EqualValues(t, find.Height, ptx.Height) + assert.EqualValues(t, find.Tx, ptx.Tx) assert.Zero(t, ptx.Index) assert.True(t, ptx.TxResult.IsOK()) - assert.EqualValues(t, txHash, ptx.Hash) + assert.EqualValues(t, find.Hash, ptx.Hash) // time to verify the proof - proof := ptx.Proof - if assert.EqualValues(t, tx, proof.Data) { - assert.NoError(t, proof.Proof.Verify(proof.RootHash, txHash)) + if assert.EqualValues(t, find.Tx, ptx.Proof.Data) { + assert.NoError(t, ptx.Proof.Proof.Verify(ptx.Proof.RootHash, find.Hash)) } // query by height - result, err = c.TxSearch(fmt.Sprintf("tx.height=%d", txHeight), true, 1, 30, "asc") + result, err = c.TxSearch(fmt.Sprintf("tx.height=%d", find.Height), true, 1, 30, "asc") require.Nil(t, err) require.Len(t, result.Txs, 1) @@ -506,10 +500,12 @@ func TestTxSearch(t *testing.T) { } // check pagination - seen := map[int64]bool{} - maxHeight := int64(0) - perPage := 3 - pages := int(math.Ceil(float64(txCount) / float64(perPage))) + var ( + seen = map[int64]bool{} + maxHeight int64 + perPage = 3 + pages = int(math.Ceil(float64(txCount) / float64(perPage))) + ) for page := 1; page <= pages; page++ { result, err = c.TxSearch("tx.height >= 1", false, page, perPage, "asc") require.NoError(t, err) From d00384f0f511e58c344213c16ad3bb4fec42098a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2020 13:02:24 +0100 Subject: [PATCH 19/74] build(deps): bump github.com/stretchr/testify from 1.5.0 to 1.5.1 (#4441) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.5.0...v1.5.1) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b828c3c4f..c0cfe34e7 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.6.2 - github.com/stretchr/testify v1.5.0 + github.com/stretchr/testify v1.5.1 github.com/tendermint/go-amino v0.14.1 github.com/tendermint/tm-db v0.4.0 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 diff --git a/go.sum b/go.sum index ff0cf1d88..9d0655c22 100644 --- a/go.sum +++ b/go.sum @@ -376,6 +376,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= From 8010f3b4df70e0a185871cdc34ec9283b17ea09b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2020 13:31:59 +0100 Subject: [PATCH 20/74] build(deps): bump github.com/spf13/cobra from 0.0.3 to 0.0.6 (#4440) Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 0.0.3 to 0.0.6. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v0.0.3...v0.0.6) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c0cfe34e7..52d2d285d 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a github.com/rs/cors v1.7.0 github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa - github.com/spf13/cobra v0.0.3 + github.com/spf13/cobra v0.0.6 github.com/spf13/viper v1.6.2 github.com/stretchr/testify v1.5.1 github.com/tendermint/go-amino v0.14.1 diff --git a/go.sum b/go.sum index 9d0655c22..3146cb41f 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -223,6 +224,7 @@ github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoR github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -236,6 +238,7 @@ github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -356,11 +359,14 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= From 8f48c49543428ef628097654c2da7b53fd94d64d Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Thu, 20 Feb 2020 13:43:40 +0100 Subject: [PATCH 21/74] Fix some golangci-lint warnings (#4448) --- .golangci.yml | 2 +- abci/tests/test_app/main.go | 3 ++- consensus/state.go | 1 + crypto/secp256k1/secp256k1.go | 4 +--- libs/autofile/group.go | 6 +++--- lite/helpers.go | 4 ++-- node/node.go | 2 +- p2p/switch.go | 4 ---- p2p/test_util.go | 2 +- rpc/core/routes.go | 1 + types/priv_validator.go | 10 +++++----- types/protobuf.go | 1 + 12 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ce0925357..dc934f43d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,7 +23,7 @@ linters: - interfacer - lll - misspell - - maligned + # - maligned - nakedret - prealloc - scopelint diff --git a/abci/tests/test_app/main.go b/abci/tests/test_app/main.go index ae47ac6f9..ca298d7e2 100644 --- a/abci/tests/test_app/main.go +++ b/abci/tests/test_app/main.go @@ -53,7 +53,8 @@ func testCounter() { } fmt.Printf("Running %s test with abci=%s\n", abciApp, abciType) - cmd := exec.Command("bash", "-c", fmt.Sprintf("abci-cli %s", abciApp)) + subCommand := fmt.Sprintf("abci-cli %s", abciApp) + cmd := exec.Command("bash", "-c", subCommand) cmd.Stdout = os.Stdout if err := cmd.Start(); err != nil { log.Fatalf("starting %q err: %v", abciApp, err) diff --git a/consensus/state.go b/consensus/state.go index d543c9c84..cd25aa32f 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1644,6 +1644,7 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) { // If the vote height is off, we'll just ignore it, // But if it's a conflicting sig, add it to the cs.evpool. // If it's otherwise invalid, punish peer. + // nolint: gocritic if err == ErrVoteHeightMismatch { return added, err } else if voteErr, ok := err.(*types.ErrVoteConflictingVotes); ok { diff --git a/crypto/secp256k1/secp256k1.go b/crypto/secp256k1/secp256k1.go index fc64a0b0f..26dcead59 100644 --- a/crypto/secp256k1/secp256k1.go +++ b/crypto/secp256k1/secp256k1.go @@ -8,12 +8,10 @@ import ( "io" "math/big" - "golang.org/x/crypto/ripemd160" - secp256k1 "github.com/btcsuite/btcd/btcec" + "golang.org/x/crypto/ripemd160" // nolint: staticcheck // necessary for Bitcoin address format amino "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/crypto" ) diff --git a/libs/autofile/group.go b/libs/autofile/group.go index ca9f57003..5d5822ebc 100644 --- a/libs/autofile/group.go +++ b/libs/autofile/group.go @@ -77,7 +77,7 @@ type Group struct { // OpenGroup creates a new Group with head at headPath. It returns an error if // it fails to open head file. -func OpenGroup(headPath string, groupOptions ...func(*Group)) (g *Group, err error) { +func OpenGroup(headPath string, groupOptions ...func(*Group)) (*Group, error) { dir, err := filepath.Abs(filepath.Dir(headPath)) if err != nil { return nil, err @@ -87,7 +87,7 @@ func OpenGroup(headPath string, groupOptions ...func(*Group)) (g *Group, err err return nil, err } - g = &Group{ + g := &Group{ ID: "group:" + head.ID, Head: head, headBuf: bufio.NewWriterSize(head, 4096*10), @@ -109,7 +109,7 @@ func OpenGroup(headPath string, groupOptions ...func(*Group)) (g *Group, err err gInfo := g.readGroupInfo() g.minIndex = gInfo.MinIndex g.maxIndex = gInfo.MaxIndex - return + return g, nil } // GroupCheckDuration allows you to overwrite default groupCheckDuration. diff --git a/lite/helpers.go b/lite/helpers.go index 18a64aa25..29dd50b5b 100644 --- a/lite/helpers.go +++ b/lite/helpers.go @@ -42,7 +42,7 @@ func (pkz privKeys) Extend(n int) privKeys { } // GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. -func GenSecpPrivKeys(n int) privKeys { +func genSecpPrivKeys(n int) privKeys { res := make(privKeys, n) for i := range res { res[i] = secp256k1.GenPrivKey() @@ -52,7 +52,7 @@ func GenSecpPrivKeys(n int) privKeys { // ExtendSecp adds n more secp256k1 keys (to remove, just take a slice). func (pkz privKeys) ExtendSecp(n int) privKeys { - extra := GenSecpPrivKeys(n) + extra := genSecpPrivKeys(n) return append(pkz, extra...) } diff --git a/node/node.go b/node/node.go index 104e572fb..485709120 100644 --- a/node/node.go +++ b/node/node.go @@ -6,7 +6,7 @@ import ( "fmt" "net" "net/http" - _ "net/http/pprof" + _ "net/http/pprof" // nolint: gosec // securely exposed on separate, optional port "os" "strings" "time" diff --git a/p2p/switch.go b/p2p/switch.go index bc675cdb8..3f9325808 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -584,10 +584,6 @@ func (sw *Switch) AddUnconditionalPeerIDs(ids []string) error { return nil } -func (sw *Switch) isPeerPersistentFn() func(*NetAddress) bool { - return sw.IsPeerPersistent -} - func (sw *Switch) IsPeerPersistent(na *NetAddress) bool { for _, pa := range sw.persistentPeersAddrs { if pa.Equals(na) { diff --git a/p2p/test_util.go b/p2p/test_util.go index f592f5f2f..045dc4c7e 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -34,7 +34,7 @@ func AddPeerToSwitchPeerSet(sw *Switch, peer Peer) { sw.peers.Add(peer) } -func CreateRandomPeer(outbound bool) *peer { +func CreateRandomPeer(outbound bool) Peer { addr, netAddr := CreateRoutableAddr() p := &peer{ peerConn: peerConn{ diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 42a57456a..aa0403f87 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -6,6 +6,7 @@ import ( // TODO: better system than "unsafe" prefix // NOTE: Amino is registered in rpc/core/types/codec.go. + var Routes = map[string]*rpc.RPCFunc{ // subscribe/unsubscribe are reserved for websocket events. "subscribe": rpc.NewWSRPCFunc(Subscribe, "query"), diff --git a/types/priv_validator.go b/types/priv_validator.go index 45d0a67b5..bd35973ca 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -107,23 +107,23 @@ func (pv *MockPV) DisableChecks() { // as MockPV has no safety checks at all. } -type erroringMockPV struct { +type ErroringMockPV struct { *MockPV } var ErroringMockPVErr = errors.New("erroringMockPV always returns an error") // Implements PrivValidator. -func (pv *erroringMockPV) SignVote(chainID string, vote *Vote) error { +func (pv *ErroringMockPV) SignVote(chainID string, vote *Vote) error { return ErroringMockPVErr } // Implements PrivValidator. -func (pv *erroringMockPV) SignProposal(chainID string, proposal *Proposal) error { +func (pv *ErroringMockPV) SignProposal(chainID string, proposal *Proposal) error { return ErroringMockPVErr } // NewErroringMockPV returns a MockPV that fails on each signing request. Again, for testing only. -func NewErroringMockPV() *erroringMockPV { - return &erroringMockPV{&MockPV{ed25519.GenPrivKey(), false, false}} +func NewErroringMockPV() *ErroringMockPV { + return &ErroringMockPV{&MockPV{ed25519.GenPrivKey(), false, false}} } diff --git a/types/protobuf.go b/types/protobuf.go index c1063ac94..52815593f 100644 --- a/types/protobuf.go +++ b/types/protobuf.go @@ -27,6 +27,7 @@ const ( ) // TODO: Make non-global by allowing for registration of more pubkey types + var ABCIPubKeyTypesToAminoNames = map[string]string{ ABCIPubKeyTypeEd25519: ed25519.PubKeyAminoName, ABCIPubKeyTypeSr25519: sr25519.PubKeyAminoName, From 2c7af01cb67b07a481779fbafb2544027725f9a2 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Thu, 20 Feb 2020 15:45:11 +0100 Subject: [PATCH 22/74] lite2: return already verified headers and verify earlier headers (#4428) closes #4413 and #4419 When VerifyHeaderAtHeight() is called, TrustedHeader is initially run to check if the header has already been verified and returns the Header. If the new header height is less than the lite clients latestTrustedHeader height, than backwards verification is performed else either sequence or bisection Refactored a test to reflect the changes * use trustedHeader func for already verified Headers * remove fetch missing header from TrustedHeader * check for already trusted Header in VerifyHeaderAtHeight * replace updateTrustedHeaderAndVals to updateTrustedHeaderAndNextVals * rename trustedHeader and trustedNextVals * refactored backwards and included it in VerifyHeader * cleaned up test to match changes * lite2: fixes after my own review Refs https://github.com/tendermint/tendermint/pull/4428#pullrequestreview-361730169 * fix ineffectual assignment * lite2: check that header exists in VerifyHeader * extract function Co-authored-by: Anton Kaliaev --- lite2/client.go | 213 ++++++++++++++++++++++--------------------- lite2/client_test.go | 8 +- 2 files changed, 111 insertions(+), 110 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 2c487f9c9..61559a694 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -121,9 +121,9 @@ type Client struct { // Where trusted headers are stored. trustedStore store.Store // Highest trusted header from the store (height=H). - trustedHeader *types.SignedHeader + latestTrustedHeader *types.SignedHeader // Highest next validator set from the store (height=H+1). - trustedNextVals *types.ValidatorSet + latestTrustedNextVals *types.ValidatorSet // See UpdatePeriod option updatePeriod time.Duration @@ -164,13 +164,13 @@ func NewClient( return nil, err } - if c.trustedHeader != nil { + if c.latestTrustedHeader != nil { if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil { return nil, err } } - if c.trustedHeader == nil || c.trustedHeader.Height < trustOptions.Height { + if c.latestTrustedHeader == nil || c.latestTrustedHeader.Height < trustOptions.Height { if err := c.initializeWithTrustOptions(trustOptions); err != nil { return nil, err } @@ -253,8 +253,8 @@ func (c *Client) restoreTrustedHeaderAndNextVals() error { return errors.Wrap(err, "can't get last trusted next validators") } - c.trustedHeader = trustedHeader - c.trustedNextVals = trustedNextVals + c.latestTrustedHeader = trustedHeader + c.latestTrustedNextVals = trustedNextVals c.logger.Debug("Restored trusted header and next vals", lastHeight) } @@ -283,24 +283,24 @@ func (c *Client) restoreTrustedHeaderAndNextVals() error { func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { var primaryHash []byte switch { - case options.Height > c.trustedHeader.Height: - h, err := c.signedHeaderFromPrimary(c.trustedHeader.Height) + case options.Height > c.latestTrustedHeader.Height: + h, err := c.signedHeaderFromPrimary(c.latestTrustedHeader.Height) if err != nil { return err } primaryHash = h.Hash() - case options.Height == c.trustedHeader.Height: + case options.Height == c.latestTrustedHeader.Height: primaryHash = options.Hash - case options.Height < c.trustedHeader.Height: + case options.Height < c.latestTrustedHeader.Height: c.logger.Info("Client initialized with old header (trusted is more recent)", "old", options.Height, - "trustedHeight", c.trustedHeader.Height, - "trustedHash", hash2str(c.trustedHeader.Hash())) + "trustedHeight", c.latestTrustedHeader.Height, + "trustedHash", hash2str(c.latestTrustedHeader.Hash())) action := fmt.Sprintf( "Rollback to %d (%X)? Note this will remove newer headers up to %d (%X)", options.Height, options.Hash, - c.trustedHeader.Height, c.trustedHeader.Hash()) + c.latestTrustedHeader.Height, c.latestTrustedHeader.Hash()) if c.confirmationFn(action) { // remove all the headers (options.Height, trustedHeader.Height] c.cleanup(options.Height + 1) @@ -314,13 +314,13 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { primaryHash = options.Hash } - if !bytes.Equal(primaryHash, c.trustedHeader.Hash()) { + if !bytes.Equal(primaryHash, c.latestTrustedHeader.Hash()) { c.logger.Info("Prev. trusted header's hash (h1) doesn't match hash from primary provider (h2)", - "h1", c.trustedHeader.Hash(), "h1", primaryHash) + "h1", hash2str(c.latestTrustedHeader.Hash()), "h2", hash2str(primaryHash)) action := fmt.Sprintf( "Prev. trusted header's hash %X doesn't match hash %X from primary provider. Remove all the stored headers?", - c.trustedHeader.Hash(), primaryHash) + c.latestTrustedHeader.Hash(), primaryHash) if c.confirmationFn(action) { err := c.Cleanup() if err != nil { @@ -449,14 +449,7 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader // 2) Get header from store. h, err := c.trustedStore.SignedHeader(height) - switch { - case errors.Is(err, store.ErrSignedHeaderNotFound): - // 2.1) If not found, try to fetch header from primary. - h, err = c.fetchMissingTrustedHeader(height, now) - if err != nil { - return nil, err - } - case err != nil: + if err != nil { return nil, err } @@ -523,15 +516,25 @@ func (c *Client) ChainID() string { return c.chainID } -// VerifyHeaderAtHeight fetches the header and validators at the given height -// and calls VerifyHeader. +// VerifyHeaderAtHeight fetches header and validators at the given height +// and calls VerifyHeader. It returns header immediately if such exists in +// trustedStore (no verification is needed). // -// If the trusted header is more recent than one here, an error is returned. -// If the header is not found by the primary provider, -// provider.ErrSignedHeaderNotFound error is returned. +// It returns provider.ErrSignedHeaderNotFound if header is not found by +// primary. +// It returns ErrOldHeaderExpired if header expired. func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { - if c.trustedHeader.Height >= height { - return nil, errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) + if height <= 0 { + return nil, errors.New("negative or zero height") + } + + h, err := c.TrustedHeader(height, now) + switch err.(type) { + case nil: // Return already trusted header + c.logger.Info("Header has already been verified", "height", height, "hash", hash2str(h.Hash())) + return h, nil + case ErrOldHeaderExpired: + return nil, err } // Request the header and the vals. @@ -540,10 +543,12 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe return nil, err } - return newHeader, c.VerifyHeader(newHeader, newVals, now) + return newHeader, c.verifyHeader(newHeader, newVals, now) } -// VerifyHeader verifies new header against the trusted state. +// VerifyHeader verifies new header against the trusted state. It returns +// immediately if newHeader exists in trustedStore (no verification is +// needed). // // SequentialVerification: verifies that 2/3 of the trusted validator set has // signed the new header. If the headers are not adjacent, **all** intermediate @@ -555,7 +560,7 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // intermediate headers will be requested. See the specification for details. // https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md // -// If the trusted header is more recent than one here, an error is returned. +// It returns ErrOldHeaderExpired if newHeader expired. // // If, at any moment, SignedHeader or ValidatorSet are not found by the primary // provider, provider.ErrSignedHeaderNotFound / @@ -565,21 +570,49 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // validator set at height newHeader.Height+1 (i.e. // newHeader.NextValidatorsHash). func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { + h, err := c.TrustedHeader(newHeader.Height, now) + switch err.(type) { + case nil: // Return already trusted header + // Make sure it's the same header. + if !bytes.Equal(h.Hash(), newHeader.Hash()) { + return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) + } + c.logger.Info("Header has already been verified", + "height", newHeader.Height, "hash", hash2str(newHeader.Hash())) + return nil + case ErrOldHeaderExpired: + return err + } + + return c.verifyHeader(newHeader, newVals, now) +} + +func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { c.logger.Info("VerifyHeader", "height", newHeader.Height, "hash", hash2str(newHeader.Hash()), "vals", hash2str(newVals.Hash())) - if c.trustedHeader.Height >= newHeader.Height { - return errors.Errorf("header at more recent height #%d exists", c.trustedHeader.Height) - } - var err error - switch c.verificationMode { - case sequential: - err = c.sequence(c.trustedHeader, c.trustedNextVals, newHeader, newVals, now) - case skipping: - err = c.bisection(c.trustedHeader, c.trustedNextVals, newHeader, newVals, now) - default: - panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) + + // 1) If going forward, perform either bisection or sequential verification + if newHeader.Height >= c.latestTrustedHeader.Height { + switch c.verificationMode { + case sequential: + err = c.sequence(c.latestTrustedHeader, c.latestTrustedNextVals, newHeader, newVals, now) + case skipping: + err = c.bisection(c.latestTrustedHeader, c.latestTrustedNextVals, newHeader, newVals, now) + default: + panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) + } + } else { + // 2) Otherwise, perform backwards verification + // Find the closest trusted header after newHeader.Height + var closestHeader *types.SignedHeader + closestHeader, err = c.trustedStore.SignedHeaderAfter(newHeader.Height) + if err != nil { + return errors.Wrapf(err, "can't get signed header after height %d", newHeader.Height) + } + + err = c.backwards(closestHeader, newHeader, now) } if err != nil { c.logger.Error("Can't verify", "err", err) @@ -596,6 +629,7 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali if err != nil { return err } + return c.updateTrustedHeaderAndNextVals(newHeader, nextVals) } @@ -652,8 +686,8 @@ func (c *Client) cleanup(stopHeight int64) error { } } - c.trustedHeader = nil - c.trustedNextVals = nil + c.latestTrustedHeader = nil + c.latestTrustedNextVals = nil err = c.restoreTrustedHeaderAndNextVals() if err != nil { return err @@ -682,8 +716,8 @@ func (c *Client) sequence( } c.logger.Debug("Verify newHeader against trustedHeader", - "trustedHeight", c.trustedHeader.Height, - "trustedHash", hash2str(c.trustedHeader.Hash()), + "trustedHeight", c.latestTrustedHeader.Height, + "trustedHash", hash2str(c.latestTrustedHeader.Hash()), "newHeight", interimHeader.Height, "newHash", hash2str(interimHeader.Hash())) err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, trustedNextVals, @@ -709,7 +743,7 @@ func (c *Client) sequence( } // 2) Verify the new header. - return VerifyAdjacent(c.chainID, c.trustedHeader, newHeader, newVals, c.trustingPeriod, now) + return VerifyAdjacent(c.chainID, c.latestTrustedHeader, newHeader, newVals, c.trustingPeriod, now) } // see VerifyHeader @@ -779,8 +813,8 @@ func (c *Client) updateTrustedHeaderAndNextVals(h *types.SignedHeader, nextVals return errors.Wrap(err, "failed to save trusted header") } - c.trustedHeader = h - c.trustedNextVals = nextVals + c.latestTrustedHeader = h + c.latestTrustedNextVals = nextVals return nil } @@ -799,77 +833,44 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, return h, vals, nil } -// fetchMissingTrustedHeader finds the closest height after the -// requested height and does backwards verification. -func (c *Client) fetchMissingTrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) { - c.logger.Info("Fetching missing header", "height", height) - - closestHeader, err := c.trustedStore.SignedHeaderAfter(height) - if err != nil { - return nil, errors.Wrapf(err, "can't get signed header after %d", height) - } - - // Perform backwards verification from closestHeader to header at the given - // height. - h, err := c.backwards(height, closestHeader, now) - if err != nil { - return nil, err - } - - // Fetch next validator set from primary and persist it. - nextVals, err := c.validatorSetFromPrimary(height + 1) - if err != nil { - return nil, errors.Wrapf(err, "failed to obtain the vals #%d", height) - } - if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) { - return nil, errors.Errorf("expected next validator's hash %X, but got %X", - h.NextValidatorsHash, nextVals.Hash()) - } - if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil { - return nil, errors.Wrap(err, "failed to save trusted header") - } - - return h, nil -} - // Backwards verification (see VerifyHeaderBackwards func in the spec) -func (c *Client) backwards(toHeight int64, fromHeader *types.SignedHeader, now time.Time) (*types.SignedHeader, error) { +func (c *Client) backwards(trustedHeader *types.SignedHeader, newHeader *types.SignedHeader, + now time.Time) error { var ( - trustedHeader = fromHeader - untrustedHeader *types.SignedHeader - err error + interimHeader *types.SignedHeader + err error ) - for i := trustedHeader.Height - 1; i >= toHeight; i-- { - untrustedHeader, err = c.signedHeaderFromPrimary(i) + for trustedHeader.Height > newHeader.Height { + interimHeader, err = c.signedHeaderFromPrimary(trustedHeader.Height - 1) if err != nil { - return nil, errors.Wrapf(err, "failed to obtain the header #%d", i) + return errors.Wrapf(err, "failed to obtain the header at height #%d", trustedHeader.Height-1) } - if err := untrustedHeader.ValidateBasic(c.chainID); err != nil { - return nil, errors.Wrap(err, "untrustedHeader.ValidateBasic failed") + if err := interimHeader.ValidateBasic(c.chainID); err != nil { + return errors.Wrap(err, "untrustedHeader.ValidateBasic failed") } - if !untrustedHeader.Time.Before(trustedHeader.Time) { - return nil, errors.Errorf("expected older header time %v to be before newer header time %v", - untrustedHeader.Time, + if !interimHeader.Time.Before(trustedHeader.Time) { + return errors.Errorf("expected older header time %v to be before newer header time %v", + interimHeader.Time, trustedHeader.Time) } - if HeaderExpired(untrustedHeader, c.trustingPeriod, now) { - return nil, ErrOldHeaderExpired{untrustedHeader.Time.Add(c.trustingPeriod), now} + if HeaderExpired(interimHeader, c.trustingPeriod, now) { + return ErrOldHeaderExpired{interimHeader.Time.Add(c.trustingPeriod), now} } - if !bytes.Equal(untrustedHeader.Hash(), trustedHeader.LastBlockID.Hash) { - return nil, errors.Errorf("older header hash %X does not match trusted header's last block %X", - untrustedHeader.Hash(), + if !bytes.Equal(interimHeader.Hash(), trustedHeader.LastBlockID.Hash) { + return errors.Errorf("older header hash %X does not match trusted header's last block %X", + interimHeader.Hash(), trustedHeader.LastBlockID.Hash) } - trustedHeader = untrustedHeader + trustedHeader = interimHeader } - return trustedHeader, nil + return nil } // compare header with all witnesses provided. @@ -899,7 +900,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { } if !bytes.Equal(h.Hash(), altH.Hash()) { - if err = c.trustedNextVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, + if err = c.latestTrustedNextVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, altH.Height, altH.Commit, c.trustLevel); err != nil { c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) witnessesToRemove = append(witnessesToRemove, i) @@ -989,7 +990,7 @@ func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { // 3) Remove all headers that are outside of the trusting period. // // NOTE: even the latest header can be removed. it's okay because - // c.trustedHeader will retain it in memory so other funcs like VerifyHeader + // c.latestTrustedHeader will retain it in memory so other funcs like VerifyHeader // don't crash. for height := oldestHeight; height <= latestHeight; height++ { h, err := c.trustedStore.SignedHeader(height) diff --git a/lite2/client_test.go b/lite2/client_test.go index d8f44adc6..a6ffc31ab 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -732,7 +732,7 @@ func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { assert.Equal(t, 1, len(c.Witnesses())) } -func TestClient_TrustedHeaderFetchesMissingHeader(t *testing.T) { +func TestClient_BackwardsVerification(t *testing.T) { c, err := NewClient( chainID, TrustOptions{ @@ -752,16 +752,16 @@ func TestClient_TrustedHeaderFetchesMissingHeader(t *testing.T) { defer c.Stop() // 1) header is missing => expect no error - h, err := c.TrustedHeader(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + h, err := c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) require.NoError(t, err) if assert.NotNil(t, h) { assert.EqualValues(t, 2, h.Height) } // 2) header is missing, but it's expired => expect error - h, err = c.TrustedHeader(1, bTime.Add(1*time.Hour).Add(1*time.Second)) + h, err = c.VerifyHeaderAtHeight(1, bTime.Add(1*time.Hour).Add(1*time.Second)) assert.Error(t, err) - assert.Nil(t, h) + assert.NotNil(t, h) } func TestClient_NewClientFromTrustedStore(t *testing.T) { From b0683b7fc6979a013c09ba782b0fad6365aa6147 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 21 Feb 2020 12:43:59 +0100 Subject: [PATCH 23/74] p2p: use curve25519.X25519() instead of ScalarMult() (#4449) * p2p: use curve25519.X25519() instead of ScalarMult() * Renamed array to shrKeyArray * Updated CHANGELOG_PENDING * Revert "Updated CHANGELOG_PENDING" This reverts commit dbb72e0bf721287847ac136c99f385ce7456d1f7. --- p2p/conn/secret_connection.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index d6b20186f..c14f1bb5c 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -5,7 +5,6 @@ import ( "crypto/cipher" crand "crypto/rand" "crypto/sha256" - "crypto/subtle" "encoding/binary" "io" "math" @@ -38,7 +37,6 @@ const ( var ( ErrSmallOrderRemotePubKey = errors.New("detected low order point from remote peer") - ErrSharedSecretIsZero = errors.New("shared secret is all zeroes") labelEphemeralLowerPublicKey = []byte("EPHEMERAL_LOWER_PUBLIC_KEY") labelEphemeralUpperPublicKey = []byte("EPHEMERAL_UPPER_PUBLIC_KEY") @@ -358,19 +356,14 @@ func deriveSecrets( // computeDHSecret computes a Diffie-Hellman shared secret key // from our own local private key and the other's public key. -// -// It returns an error if the computed shared secret is all zeroes. -func computeDHSecret(remPubKey, locPrivKey *[32]byte) (shrKey *[32]byte, err error) { - shrKey = new([32]byte) - curve25519.ScalarMult(shrKey, locPrivKey, remPubKey) - - // reject if the returned shared secret is all zeroes - // related to: https://github.com/tendermint/tendermint/issues/3010 - zero := new([32]byte) - if subtle.ConstantTimeCompare(shrKey[:], zero[:]) == 1 { - return nil, ErrSharedSecretIsZero +func computeDHSecret(remPubKey, locPrivKey *[32]byte) (*[32]byte, error) { + shrKey, err := curve25519.X25519(locPrivKey[:], remPubKey[:]) + if err != nil { + return nil, err } - return + var shrKeyArray [32]byte + copy(shrKeyArray[:], shrKey) + return &shrKeyArray, nil } func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) { From 8d1198b39a3268068b67989b1c53ec906b5a62a0 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 21 Feb 2020 13:28:05 +0100 Subject: [PATCH 24/74] ADR-053: update with implementation plan after prototype (#4427) --- .../adr-053-state-sync-prototype.md | 157 +++++++++++++++--- 1 file changed, 133 insertions(+), 24 deletions(-) diff --git a/docs/architecture/adr-053-state-sync-prototype.md b/docs/architecture/adr-053-state-sync-prototype.md index 95615325d..79e39b24d 100644 --- a/docs/architecture/adr-053-state-sync-prototype.md +++ b/docs/architecture/adr-053-state-sync-prototype.md @@ -6,6 +6,14 @@ This ADR outlines the plan for an initial state sync prototype, and is subject t * 2020-01-28: Initial draft (Erik Grinaker) +* 2020-02-18: Updates after initial prototype (Erik Grinaker) + * ABCI: added missing `reason` fields. + * ABCI: used 32-bit 1-based chunk indexes (was 64-bit 0-based). + * ABCI: moved `RequestApplySnapshotChunk.chain_hash` to `RequestOfferSnapshot.app_hash`. + * Gaia: snapshots must include node versions as well, both for inner and leaf nodes. + * Added experimental prototype info. + * Added open questions and implementation plan. + ## Context State sync will allow a new node to receive a snapshot of the application state without downloading blocks or going through consensus. This bootstraps the node significantly faster than the current fast sync system, which replays all historical blocks. @@ -34,14 +42,14 @@ A node can have multiple snapshots taken at various heights. Snapshots can be ta message Snapshot { uint64 height = 1; // The height at which the snapshot was taken uint32 format = 2; // The application-specific snapshot format - uint64 chunks = 3; // The number of chunks in the snapshot + uint32 chunks = 3; // The number of chunks in the snapshot bytes metadata = 4; // Arbitrary application metadata } message SnapshotChunk { uint64 height = 1; // The height of the corresponding snapshot uint32 format = 2; // The application-specific snapshot format - uint64 chunk = 3; // The chunk index (zero-based) + uint32 chunk = 3; // The chunk index (one-based) bytes data = 4; // Serialized application state in an arbitrary format bytes checksum = 5; // SHA-1 checksum of data } @@ -64,11 +72,13 @@ message ResponseListSnapshots { // Offers a snapshot to the application message RequestOfferSnapshot { Snapshot snapshot = 1; + bytes app_hash = 2; } message ResponseOfferSnapshot { bool accepted = 1; - enum reason { // Reason why snapshot was rejected + Reason reason = 2; // Reason why snapshot was rejected + enum Reason { unknown = 0; // Unknown or generic reason invalid_height = 1; // Height is rejected: avoid this height invalid_format = 2; // Format is rejected: avoid this format @@ -79,7 +89,7 @@ message ResponseOfferSnapshot { message RequestGetSnapshotChunk { uint64 height = 1; uint32 format = 2; - uint64 chunk = 3; + uint32 chunk = 3; } message ResponseGetSnapshotChunk { @@ -89,12 +99,12 @@ message ResponseGetSnapshotChunk { // Applies a snapshot chunk message RequestApplySnapshotChunk { SnapshotChunk chunk = 1; - bytes chain_hash = 2; } message ResponseApplySnapshotChunk { bool applied = 1; - enum reason { // Reason why chunk failed + Reason reason = 2; // Reason why chunk failed + enum Reason { unknown = 0; // Unknown or generic reason verify_failed = 1; // Chunk verification failed } @@ -165,24 +175,13 @@ The Gaia data structure consists of a set of named IAVL trees. A root hash is co IAVL trees are versioned, but a snapshot only contains the version relevant for the snapshot height. All historical versions are ignored. -IAVL trees are insertion-order dependent, so key/value pairs must be stored in an appropriate insertion order to produce the same tree branching structure and thus the same Merkle hashes. +IAVL trees are insertion-order dependent, so key/value pairs must be set in an appropriate insertion order to produce the same tree branching structure. This insertion order can be found by doing a breadth-first scan of all nodes (including inner nodes) and collecting unique keys in order. However, the node hash also depends on the node's version, so snapshots must contain the inner nodes' version numbers as well. -A chunk corresponds to a subtree key range within an IAVL tree, in insertion order, along with the Merkle proof from the root of the subtree all the way up to the root of the multistore (including the Merkle proof for the surrounding multistore. +For the initial prototype, each chunk consists of a complete dump of all node data for all nodes in an entire IAVL tree. Thus the number of chunks equals the number of persistent stores in Gaia. No incremental verification of chunks is done, only a final app hash comparison at the end of the snapshot restoration. -```go -struct SnapshotChunk { - Store string // Name (key) of IAVL store in outer MultiStore - Keys [][]byte // Snapshotted keys in insertion order - Values [][]byte // Snapshotted values corresponding to Keys - Proof []merkle.ProofOp // Merkle proof from subtree root to MultiStore root -} -``` +For a production version, it should be sufficient to store key/value/version for all nodes (leaf and inner) in insertion order, chunked in some appropriate way. If per-chunk verification is required, the chunk must also contain enough information to reconstruct the Merkle proofs all the way up to the root of the multistore, e.g. by storing a complete subtree's key/value/version data plus Merkle hashes of all other branches up to the multistore root. The exact approach will depend on tradeoffs between size, time, and verification. IAVL RangeProofs are not recommended, since these include redundant data such as proofs for intermediate and leaf nodes that can be derived from the above data. -This chunk structure is believed to be sufficient to reconstruct an identical IAVL tree by applying separate chunks, but this may require the chunks to be ordered in a certain way; further research is needed. - -We do not use IAVL RangeProofs, since these include redundant data such as proofs for intermediate and leaf nodes that can be derived from the above data. - -Chunks should be built greedily by collecting key/value pairs constituting a complete subtree up to some size limit (e.g. 32 MB), then serialized. Chunk data is stored in the file system as `snapshots////data`, along with a SHA-1 checksum in `snapshots////checksum`, and served via `RequestGetSnapshotChunk`. +Chunks should be built greedily by collecting node data up to some size limit (e.g. 32 MB) and serializing it. Chunk data is stored in the file system as `snapshots////data`, along with a SHA-1 checksum in `snapshots////checksum`, and served via `RequestGetSnapshotChunk`. ### Snapshot Scheduling @@ -192,17 +191,127 @@ Taking consistent snapshots of IAVL trees is greatly simplified by them being ve Snapshots must also be garbage collected after some configurable time, e.g. by keeping the latest `n` snapshots. +## Experimental Prototype + +An experimental but functional state sync prototype is available in the `erik/statesync-prototype` branches of the Tendermint, IAVL, Cosmos SDK, and Gaia repositories. To fetch the necessary branches: + +```sh +$ mkdir statesync +$ cd statesync +$ git clone git@github.com:tendermint/tendermint -b erik/statesync-prototype +$ git clone git@github.com:tendermint/iavl -b erik/statesync-prototype +$ git clone git@github.com:cosmos/cosmos-sdk -b erik/statesync-prototype +$ git clone git@github.com:cosmos/gaia -b erik/statesync-prototype +``` + +To spin up three nodes of a four-node testnet: + +```sh +$ cd gaia +$ ./tools/start.sh +``` + +Wait for the first snapshot to be taken at height 3, then (in a separate terminal) start the fourth node with state sync enabled: + +```sh +$ ./tools/sync.sh +``` + +To stop the testnet, run: + +```sh +$ ./tools/stop.sh +``` + ## Open Questions -* Is it possible to reconstruct an identical IAVL tree given separate subtrees in an appropriate order, or is more data needed about the branch structure? +* Should we have a simpler scheme for discovering snapshots? E.g. announce supported formats, and have peer supply latest available snapshot. -* Should we punish nodes that provide invalid snapshots? + Downsides: app has to announce supported formats, having a single snapshot per peer may make fewer peers available for chosen snapshot. + +## Resolved Questions * Is it OK for state-synced nodes to not have historical blocks nor historical IAVL versions? + > Yes, this is as intended. Maybe backfill blocks later. + +* Do we need incremental chunk verification for first version? + + > No, we'll start simple. Can add chunk verification via a new snapshot format without any breaking changes in Tendermint. For adversarial conditions, maybe consider support for whitelisting peers to download chunks from. + +* Should the snapshot ABCI interface be a separate optional ABCI service, or mandatory? + + > Mandatory, to keep things simple for now. It will therefore be a breaking change and push the release. For apps using the Cosmos SDK, we can provide a default implementation that does not serve snapshots and errors when trying to apply them. + +* How can we make sure `ListSnapshots` data is valid? An adversary can provide fake/invalid snapshots to DoS peers. + + > For now, just pick snapshots that are available on a large number of peers. Maybe support whitelisting. We may consider e.g. placing snapshot manifests on the blockchain later. + +* Should we punish nodes that provide invalid snapshots? How? + + > No, these are full nodes not validators, so we can't punish them. Just disconnect from them and ignore them. + +* Should we call these snapshots? The SDK already uses the term "snapshot" for `PruningOptions.SnapshotEvery`, and state sync will introduce additional SDK options for snapshot scheduling and pruning that are not related to IAVL snapshotting or pruning. + + > Yes. Hopefully these concepts are distinct enough that we can refer to state sync snapshots and IAVL snapshots without too much confusion. + +* Should we store snapshot and chunk metadata in a database? Can we use the database for chunks? + + > As a first approach, store metadata in a database and chunks in the filesystem. + +* Should a snapshot at height H be taken before or after the block at H is processed? E.g. RPC `/commit` returns app_hash after _previous_ height, i.e. _before_ current height. + + > After commit. + +* Do we need to support all versions of blockchain reactor (i.e. fast sync)? + + > We should remove the v1 reactor completely once v2 has stabilized. + +* Should `ListSnapshots` be a streaming API instead of a request/response API? + + > No, just use a max message size. + +## Implementation Plan + +### Core Tasks + +* **Tendermint:** light client P2P transport [#4456](https://github.com/tendermint/tendermint/issues/4456) + +* **IAVL:** export/import API [#210](https://github.com/tendermint/iavl/issues/210) + +* **Cosmos SDK:** snapshotting, scheduling, and pruning [#5689](https://github.com/cosmos/cosmos-sdk/issues/5689) + +* **Tendermint:** support starting with a truncated block history + +* **Tendermint:** state sync reactor and ABCI interface [#828](https://github.com/tendermint/tendermint/issues/828) + +* **Cosmos SDK:** snapshot ABCI implementation [#5690](https://github.com/cosmos/cosmos-sdk/issues/5690) + +### Nice-to-Haves + +* **Tendermint:** staged reactor startup (state sync → fast sync → block replay → wal replay → consensus) + + > Let's do a time-boxed prototype (a few days) and see how much work it will be. + + * Notify P2P peers about channel changes [#4394](https://github.com/tendermint/tendermint/issues/4394) + + * Check peers have certain channels [#1148](https://github.com/tendermint/tendermint/issues/1148) + +* **Tendermint:** prune blockchain history [#3652](https://github.com/tendermint/tendermint/issues/3652) + +* **Tendermint:** allow genesis to start from non-zero height [#2543](https://github.com/tendermint/tendermint/issues/2543) + +### Follow-up Tasks + +* **Tendermint:** light client verification for fast sync [#4457](https://github.com/tendermint/tendermint/issues/4457) + +* **Tendermint:** allow start with only blockstore [#3713](https://github.com/tendermint/tendermint/issues/3713) + +* **Tendermint:** node should go back to fast-syncing when lagging significantly [#129](https://github.com/tendermint/tendermint/issues/129) + ## Status -Proposed +Accepted ## References From c63fd32fc2341f753afb147e41a607e482608a11 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 21 Feb 2020 15:23:48 +0100 Subject: [PATCH 25/74] circleci: run P2P IPv4 and IPv6 tests in parallel (#4459) --- .circleci/config.yml | 9 ++++++++- test/p2p/circleci.sh | 25 +++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0d8129a9..8fe23501f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -171,11 +171,15 @@ jobs: GOPATH: /home/circleci/.go_workspace machine: image: circleci/classic:latest + parameters: + ipv: + type: integer + default: 4 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/p2p/circleci.sh + - run: bash test/p2p/circleci.sh << parameters.ipv >> - store_artifacts: path: /home/circleci/project/test/p2p/logs @@ -425,6 +429,9 @@ workflows: requires: - setup_dependencies - test_p2p + - test_p2p: + name: test_p2p_ipv6 + ipv: 6 - upload_coverage: requires: - test_cover diff --git a/test/p2p/circleci.sh b/test/p2p/circleci.sh index 92869ff45..fe8a972bd 100644 --- a/test/p2p/circleci.sh +++ b/test/p2p/circleci.sh @@ -1,21 +1,26 @@ #! /bin/bash set -eux +# Take IP version as parameter +IPV="${1:-4}" + # 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 )" # Enable IPv6 support in Docker daemon -echo -echo "* [$(date +"%T")] enabling IPv6 stack in Docker daemon" -cat <<'EOF' | sudo tee /etc/docker/daemon.json +if [[ "$IPV" == "6" ]]; then + echo + echo "* [$(date +"%T")] enabling IPv6 stack in Docker daemon" + cat <<'EOF' | sudo tee /etc/docker/daemon.json { - "ipv6": true, - "fixed-cidr-v6": "2001:db8:1::/64" + "ipv6": true, + "fixed-cidr-v6": "2001:db8:1::/64" } EOF -sudo service docker restart + sudo service docker restart +fi LOGS_DIR="$DIR/logs" echo @@ -42,12 +47,8 @@ if [[ "$SKIP_BUILD" == "" ]]; then fi echo -echo "* [$(date +"%T")] running p2p tests on a local docker network" -bash "$DIR/../p2p/test.sh" tester - -echo -echo "* [$(date +"%T")] running IPv6 p2p tests on a local docker network" -bash "$DIR/../p2p/test.sh" tester 6 +echo "* [$(date +"%T")] running IPv$IPV p2p tests on a local docker network" +bash "$DIR/../p2p/test.sh" tester $IPV echo echo "* [$(date +"%T")] copying log files out of docker container into $LOGS_DIR" From fe1121979507edf714113d54188f96e528ee4871 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 21 Feb 2020 19:21:39 +0100 Subject: [PATCH 26/74] Fix unexported returns (#4450) --- abci/client/grpc_client.go | 2 +- abci/client/local_client.go | 2 +- abci/client/socket_client.go | 2 +- abci/client/socket_client_test.go | 6 +++++- consensus/common_test.go | 4 ++-- consensus/replay_test.go | 2 +- consensus/wal.go | 30 +++++++++++++------------- lite/multiprovider.go | 2 +- p2p/conn_set.go | 2 +- p2p/pex/addrbook.go | 2 +- p2p/pex/addrbook_test.go | 12 ++++++----- p2p/pex/pex_reactor_test.go | 11 +++++----- privval/socket_listeners.go | 36 +++++++++++++++---------------- proxy/app_conn.go | 6 +++--- proxy/multi_app_conn.go | 8 +++---- rpc/lib/server/ws_handler.go | 4 ++-- 16 files changed, 69 insertions(+), 62 deletions(-) diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index f853a4254..01583bc1f 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -31,7 +31,7 @@ type grpcClient struct { resCb func(*types.Request, *types.Response) // listens to all callbacks } -func NewGRPCClient(addr string, mustConnect bool) *grpcClient { +func NewGRPCClient(addr string, mustConnect bool) Client { cli := &grpcClient{ addr: addr, mustConnect: mustConnect, diff --git a/abci/client/local_client.go b/abci/client/local_client.go index f50d4eae1..3946bfbc5 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -21,7 +21,7 @@ type localClient struct { Callback } -func NewLocalClient(mtx *sync.Mutex, app types.Application) *localClient { +func NewLocalClient(mtx *sync.Mutex, app types.Application) Client { if mtx == nil { mtx = new(sync.Mutex) } diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 2e765119b..7898a8f26 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -43,7 +43,7 @@ type socketClient struct { } -func NewSocketClient(addr string, mustConnect bool) *socketClient { +func NewSocketClient(addr string, mustConnect bool) Client { cli := &socketClient{ reqQueue: make(chan *ReqRes, reqQueueSize), flushTimer: timer.NewThrottleTimer("socketClient", flushThrottleMS), diff --git a/abci/client/socket_client_test.go b/abci/client/socket_client_test.go index c4877ce70..37bc2b57a 100644 --- a/abci/client/socket_client_test.go +++ b/abci/client/socket_client_test.go @@ -16,8 +16,12 @@ import ( "github.com/tendermint/tendermint/libs/service" ) +type errorStopper interface { + StopForError(error) +} + func TestSocketClientStopForErrorDeadlock(t *testing.T) { - c := abcicli.NewSocketClient(":80", false) + c := abcicli.NewSocketClient(":80", false).(errorStopper) err := errors.New("foo-tendermint") // See Issue https://github.com/tendermint/abci/issues/114 diff --git a/consensus/common_test.go b/consensus/common_test.go index c6cd18193..0403ccaf3 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -71,7 +71,7 @@ type validatorStub struct { var testMinPower int64 = 10 -func NewValidatorStub(privValidator types.PrivValidator, valIndex int) *validatorStub { +func newValidatorStub(privValidator types.PrivValidator, valIndex int) *validatorStub { return &validatorStub{ Index: valIndex, PrivValidator: privValidator, @@ -385,7 +385,7 @@ func randState(nValidators int) (*State, []*validatorStub) { cs := newState(state, privVals[0], counter.NewApplication(true)) for i := 0; i < nValidators; i++ { - vss[i] = NewValidatorStub(privVals[i], i) + vss[i] = newValidatorStub(privVals[i], i) } // since cs1 starts at 1 incrementHeight(vss[1:]...) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 530948a18..6a33cf382 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -329,7 +329,7 @@ func TestSimulateValidatorsChange(t *testing.T) { vss := make([]*validatorStub, nPeers) for i := 0; i < nPeers; i++ { - vss[i] = NewValidatorStub(css[i].privValidator, i) + vss[i] = newValidatorStub(css[i].privValidator, i) } height, round := css[0].Height, css[0].Round // start the machine diff --git a/consensus/wal.go b/consensus/wal.go index 64f04fee0..989a5dc29 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -78,7 +78,7 @@ type WAL interface { // TODO: currently the wal is overwritten during replay catchup, give it a mode // so it's either reading or appending - must read to end to start appending // again. -type baseWAL struct { +type BaseWAL struct { service.BaseService group *auto.Group @@ -89,11 +89,11 @@ type baseWAL struct { flushInterval time.Duration } -var _ WAL = &baseWAL{} +var _ WAL = &BaseWAL{} // NewWAL returns a new write-ahead logger based on `baseWAL`, which implements // WAL. It's flushed and synced to disk every 2s and once when stopped. -func NewWAL(walFile string, groupOptions ...func(*auto.Group)) (*baseWAL, error) { +func NewWAL(walFile string, groupOptions ...func(*auto.Group)) (*BaseWAL, error) { err := tmos.EnsureDir(filepath.Dir(walFile), 0700) if err != nil { return nil, errors.Wrap(err, "failed to ensure WAL directory is in place") @@ -103,7 +103,7 @@ func NewWAL(walFile string, groupOptions ...func(*auto.Group)) (*baseWAL, error) if err != nil { return nil, err } - wal := &baseWAL{ + wal := &BaseWAL{ group: group, enc: NewWALEncoder(group), flushInterval: walDefaultFlushInterval, @@ -113,20 +113,20 @@ func NewWAL(walFile string, groupOptions ...func(*auto.Group)) (*baseWAL, error) } // SetFlushInterval allows us to override the periodic flush interval for the WAL. -func (wal *baseWAL) SetFlushInterval(i time.Duration) { +func (wal *BaseWAL) SetFlushInterval(i time.Duration) { wal.flushInterval = i } -func (wal *baseWAL) Group() *auto.Group { +func (wal *BaseWAL) Group() *auto.Group { return wal.group } -func (wal *baseWAL) SetLogger(l log.Logger) { +func (wal *BaseWAL) SetLogger(l log.Logger) { wal.BaseService.Logger = l wal.group.SetLogger(l) } -func (wal *baseWAL) OnStart() error { +func (wal *BaseWAL) OnStart() error { size, err := wal.group.Head.Size() if err != nil { return err @@ -142,7 +142,7 @@ func (wal *baseWAL) OnStart() error { return nil } -func (wal *baseWAL) processFlushTicks() { +func (wal *BaseWAL) processFlushTicks() { for { select { case <-wal.flushTicker.C: @@ -157,14 +157,14 @@ func (wal *baseWAL) processFlushTicks() { // FlushAndSync flushes and fsync's the underlying group's data to disk. // See auto#FlushAndSync -func (wal *baseWAL) FlushAndSync() error { +func (wal *BaseWAL) FlushAndSync() error { return wal.group.FlushAndSync() } // Stop the underlying autofile group. // Use Wait() to ensure it's finished shutting down // before cleaning up files. -func (wal *baseWAL) OnStop() { +func (wal *BaseWAL) OnStop() { wal.flushTicker.Stop() wal.FlushAndSync() wal.group.Stop() @@ -173,14 +173,14 @@ func (wal *baseWAL) OnStop() { // Wait for the underlying autofile group to finish shutting down // so it's safe to cleanup files. -func (wal *baseWAL) Wait() { +func (wal *BaseWAL) Wait() { wal.group.Wait() } // Write is called in newStep and for each receive on the // peerMsgQueue and the timeoutTicker. // NOTE: does not call fsync() -func (wal *baseWAL) Write(msg WALMessage) error { +func (wal *BaseWAL) Write(msg WALMessage) error { if wal == nil { return nil } @@ -197,7 +197,7 @@ func (wal *baseWAL) Write(msg WALMessage) error { // WriteSync is called when we receive a msg from ourselves // so that we write to disk before sending signed messages. // NOTE: calls fsync() -func (wal *baseWAL) WriteSync(msg WALMessage) error { +func (wal *BaseWAL) WriteSync(msg WALMessage) error { if wal == nil { return nil } @@ -227,7 +227,7 @@ type WALSearchOptions struct { // Group reader will be nil if found equals false. // // CONTRACT: caller must close group reader. -func (wal *baseWAL) SearchForEndHeight( +func (wal *BaseWAL) SearchForEndHeight( height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) { var ( diff --git a/lite/multiprovider.go b/lite/multiprovider.go index b10c1c3cb..364647a40 100644 --- a/lite/multiprovider.go +++ b/lite/multiprovider.go @@ -16,7 +16,7 @@ type multiProvider struct { } // NewMultiProvider returns a new provider which wraps multiple other providers. -func NewMultiProvider(providers ...PersistentProvider) *multiProvider { +func NewMultiProvider(providers ...PersistentProvider) PersistentProvider { return &multiProvider{ logger: log.NewNopLogger(), providers: providers, diff --git a/p2p/conn_set.go b/p2p/conn_set.go index d64622783..a889ad5e1 100644 --- a/p2p/conn_set.go +++ b/p2p/conn_set.go @@ -26,7 +26,7 @@ type connSet struct { } // NewConnSet returns a ConnSet implementation. -func NewConnSet() *connSet { +func NewConnSet() ConnSet { return &connSet{ conns: map[string]connSetItem{}, } diff --git a/p2p/pex/addrbook.go b/p2p/pex/addrbook.go index 4efb4d1e0..dbba71345 100644 --- a/p2p/pex/addrbook.go +++ b/p2p/pex/addrbook.go @@ -102,7 +102,7 @@ type addrBook struct { // NewAddrBook creates a new address book. // Use Start to begin processing asynchronous address updates. -func NewAddrBook(filePath string, routabilityStrict bool) *addrBook { +func NewAddrBook(filePath string, routabilityStrict bool) AddrBook { am := &addrBook{ rand: tmrand.NewRand(), ourAddrs: make(map[string]struct{}), diff --git a/p2p/pex/addrbook_test.go b/p2p/pex/addrbook_test.go index 0bcda730e..4942b403b 100644 --- a/p2p/pex/addrbook_test.go +++ b/p2p/pex/addrbook_test.go @@ -17,6 +17,8 @@ import ( "github.com/tendermint/tendermint/p2p" ) +// FIXME These tests should not rely on .(*addrBook) assertions + func TestAddrBookPickAddress(t *testing.T) { fname := createTempFileName("addrbook_test") defer deleteTempFile(fname) @@ -58,11 +60,11 @@ func TestAddrBookSaveLoad(t *testing.T) { defer deleteTempFile(fname) // 0 addresses - book := NewAddrBook(fname, true) + book := NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) book.saveToFile(fname) - book = NewAddrBook(fname, true) + book = NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) book.loadFromFile(fname) @@ -78,7 +80,7 @@ func TestAddrBookSaveLoad(t *testing.T) { assert.Equal(t, 100, book.Size()) book.saveToFile(fname) - book = NewAddrBook(fname, true) + book = NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) book.loadFromFile(fname) @@ -91,7 +93,7 @@ func TestAddrBookLookup(t *testing.T) { randAddrs := randNetAddressPairs(t, 100) - book := NewAddrBook(fname, true) + book := NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) for _, addrSrc := range randAddrs { addr := addrSrc.addr @@ -575,7 +577,7 @@ func deleteTempFile(fname string) { func createAddrBookWithMOldAndNNewAddrs(t *testing.T, nOld, nNew int) (book *addrBook, fname string) { fname = createTempFileName("addrbook_test") - book = NewAddrBook(fname, true) + book = NewAddrBook(fname, true).(*addrBook) book.SetLogger(log.TestingLogger()) assert.Zero(t, book.Size()) diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 5e174765a..4cddf6352 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -74,7 +74,7 @@ func TestPEXReactorRunning(t *testing.T) { require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - books := make([]*addrBook, N) + books := make([]AddrBook, N) logger := log.TestingLogger() // create switches @@ -404,7 +404,7 @@ func TestPEXReactorSeedModeFlushStop(t *testing.T) { require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - books := make([]*addrBook, N) + books := make([]AddrBook, N) logger := log.TestingLogger() // create switches @@ -631,7 +631,7 @@ func testCreatePeerWithSeed(dir string, id int, seed *p2p.Switch) *p2p.Switch { return testCreatePeerWithConfig(dir, id, conf) } -func createReactor(conf *ReactorConfig) (r *Reactor, book *addrBook) { +func createReactor(conf *ReactorConfig) (r *Reactor, book AddrBook) { // directory to store address book dir, err := ioutil.TempDir("", "pex_reactor") if err != nil { @@ -645,8 +645,9 @@ func createReactor(conf *ReactorConfig) (r *Reactor, book *addrBook) { return } -func teardownReactor(book *addrBook) { - err := os.RemoveAll(filepath.Dir(book.FilePath())) +func teardownReactor(book AddrBook) { + // FIXME Shouldn't rely on .(*addrBook) assertion + err := os.RemoveAll(filepath.Dir(book.(*addrBook).FilePath())) if err != nil { panic(err) } diff --git a/privval/socket_listeners.go b/privval/socket_listeners.go index f4d875e71..0e12e4268 100644 --- a/privval/socket_listeners.go +++ b/privval/socket_listeners.go @@ -23,26 +23,26 @@ type timeoutError interface { // TCP Listener // TCPListenerOption sets an optional parameter on the tcpListener. -type TCPListenerOption func(*tcpListener) +type TCPListenerOption func(*TCPListener) // TCPListenerTimeoutAccept sets the timeout for the listener. // A zero time value disables the timeout. func TCPListenerTimeoutAccept(timeout time.Duration) TCPListenerOption { - return func(tl *tcpListener) { tl.timeoutAccept = timeout } + return func(tl *TCPListener) { tl.timeoutAccept = timeout } } // TCPListenerTimeoutReadWrite sets the read and write timeout for connections // from external signing processes. func TCPListenerTimeoutReadWrite(timeout time.Duration) TCPListenerOption { - return func(tl *tcpListener) { tl.timeoutReadWrite = timeout } + return func(tl *TCPListener) { tl.timeoutReadWrite = timeout } } // tcpListener implements net.Listener. -var _ net.Listener = (*tcpListener)(nil) +var _ net.Listener = (*TCPListener)(nil) -// tcpListener wraps a *net.TCPListener to standardise protocol timeouts +// TCPListener wraps a *net.TCPListener to standardise protocol timeouts // and potentially other tuning parameters. It also returns encrypted connections. -type tcpListener struct { +type TCPListener struct { *net.TCPListener secretConnKey ed25519.PrivKeyEd25519 @@ -53,8 +53,8 @@ type tcpListener struct { // NewTCPListener returns a listener that accepts authenticated encrypted connections // using the given secretConnKey and the default timeout values. -func NewTCPListener(ln net.Listener, secretConnKey ed25519.PrivKeyEd25519) *tcpListener { - return &tcpListener{ +func NewTCPListener(ln net.Listener, secretConnKey ed25519.PrivKeyEd25519) *TCPListener { + return &TCPListener{ TCPListener: ln.(*net.TCPListener), secretConnKey: secretConnKey, timeoutAccept: time.Second * defaultTimeoutAcceptSeconds, @@ -63,7 +63,7 @@ func NewTCPListener(ln net.Listener, secretConnKey ed25519.PrivKeyEd25519) *tcpL } // Accept implements net.Listener. -func (ln *tcpListener) Accept() (net.Conn, error) { +func (ln *TCPListener) Accept() (net.Conn, error) { deadline := time.Now().Add(ln.timeoutAccept) err := ln.SetDeadline(deadline) if err != nil { @@ -89,25 +89,25 @@ func (ln *tcpListener) Accept() (net.Conn, error) { // Unix Listener // unixListener implements net.Listener. -var _ net.Listener = (*unixListener)(nil) +var _ net.Listener = (*UnixListener)(nil) -type UnixListenerOption func(*unixListener) +type UnixListenerOption func(*UnixListener) // UnixListenerTimeoutAccept sets the timeout for the listener. // A zero time value disables the timeout. func UnixListenerTimeoutAccept(timeout time.Duration) UnixListenerOption { - return func(ul *unixListener) { ul.timeoutAccept = timeout } + return func(ul *UnixListener) { ul.timeoutAccept = timeout } } // UnixListenerTimeoutReadWrite sets the read and write timeout for connections // from external signing processes. func UnixListenerTimeoutReadWrite(timeout time.Duration) UnixListenerOption { - return func(ul *unixListener) { ul.timeoutReadWrite = timeout } + return func(ul *UnixListener) { ul.timeoutReadWrite = timeout } } -// unixListener wraps a *net.UnixListener to standardise protocol timeouts +// UnixListener wraps a *net.UnixListener to standardise protocol timeouts // and potentially other tuning parameters. It returns unencrypted connections. -type unixListener struct { +type UnixListener struct { *net.UnixListener timeoutAccept time.Duration @@ -116,8 +116,8 @@ type unixListener struct { // NewUnixListener returns a listener that accepts unencrypted connections // using the default timeout values. -func NewUnixListener(ln net.Listener) *unixListener { - return &unixListener{ +func NewUnixListener(ln net.Listener) *UnixListener { + return &UnixListener{ UnixListener: ln.(*net.UnixListener), timeoutAccept: time.Second * defaultTimeoutAcceptSeconds, timeoutReadWrite: time.Second * defaultTimeoutReadWriteSeconds, @@ -125,7 +125,7 @@ func NewUnixListener(ln net.Listener) *unixListener { } // Accept implements net.Listener. -func (ln *unixListener) Accept() (net.Conn, error) { +func (ln *UnixListener) Accept() (net.Conn, error) { deadline := time.Now().Add(ln.timeoutAccept) err := ln.SetDeadline(deadline) if err != nil { diff --git a/proxy/app_conn.go b/proxy/app_conn.go index 1698ab52f..066d17295 100644 --- a/proxy/app_conn.go +++ b/proxy/app_conn.go @@ -47,7 +47,7 @@ type appConnConsensus struct { appConn abcicli.Client } -func NewAppConnConsensus(appConn abcicli.Client) *appConnConsensus { +func NewAppConnConsensus(appConn abcicli.Client) AppConnConsensus { return &appConnConsensus{ appConn: appConn, } @@ -88,7 +88,7 @@ type appConnMempool struct { appConn abcicli.Client } -func NewAppConnMempool(appConn abcicli.Client) *appConnMempool { +func NewAppConnMempool(appConn abcicli.Client) AppConnMempool { return &appConnMempool{ appConn: appConn, } @@ -121,7 +121,7 @@ type appConnQuery struct { appConn abcicli.Client } -func NewAppConnQuery(appConn abcicli.Client) *appConnQuery { +func NewAppConnQuery(appConn abcicli.Client) AppConnQuery { return &appConnQuery{ appConn: appConn, } diff --git a/proxy/multi_app_conn.go b/proxy/multi_app_conn.go index 44ff242b1..364114115 100644 --- a/proxy/multi_app_conn.go +++ b/proxy/multi_app_conn.go @@ -30,15 +30,15 @@ func NewAppConns(clientCreator ClientCreator) AppConns { type multiAppConn struct { service.BaseService - mempoolConn *appConnMempool - consensusConn *appConnConsensus - queryConn *appConnQuery + mempoolConn AppConnMempool + consensusConn AppConnConsensus + queryConn AppConnQuery clientCreator ClientCreator } // Make all necessary abci connections to the application -func NewMultiAppConn(clientCreator ClientCreator) *multiAppConn { +func NewMultiAppConn(clientCreator ClientCreator) AppConns { multiAppConn := &multiAppConn{ clientCreator: clientCreator, } diff --git a/rpc/lib/server/ws_handler.go b/rpc/lib/server/ws_handler.go index 07f424b48..e7048db79 100644 --- a/rpc/lib/server/ws_handler.go +++ b/rpc/lib/server/ws_handler.go @@ -92,7 +92,7 @@ func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Requ }() // register connection - con := NewWSConnection(wsConn, wm.funcMap, wm.cdc, wm.wsConnOptions...) + con := newWSConnection(wsConn, wm.funcMap, wm.cdc, wm.wsConnOptions...) con.SetLogger(wm.logger.With("remote", wsConn.RemoteAddr())) wm.logger.Info("New websocket connection", "remote", con.remoteAddr) err = con.Start() // BLOCKING @@ -154,7 +154,7 @@ type wsConnection struct { // description of how to configure ping period and pong wait time. NOTE: if the // write buffer is full, pongs may be dropped, which may cause clients to // disconnect. see https://github.com/gorilla/websocket/issues/97 -func NewWSConnection( +func newWSConnection( baseConn *websocket.Conn, funcMap map[string]*RPCFunc, cdc *amino.Codec, From f934ca82fc0ace327ca34bc8593b21789c202109 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Mon, 24 Feb 2020 11:10:41 +0100 Subject: [PATCH 27/74] lite2: don't save intermediate headers (#4452) closes #4426 The sequence and bisection methods no longer save the intermediate headers and validator sets that they require to verify a currently untrusted header. ## Commits: * sequence and bisection don't save intermediate headers and vals * check the next validator hash matches the header * check expired header at start of backwards verification * added tests * handled cleanup warning * lint fix * removed redundant code * tweaked minor errors * avoided premature trusting of nextVals * fix test error * updated trustedHeader and Vals together * fixed bisection error * fixed sequence error for different vals and made test * fixes after my own review * reorder vars to be consistent with the rest of the code Co-authored-by: Anton Kaliaev --- lite2/client.go | 50 +++++++++++++++++++++----------------------- lite2/client_test.go | 37 ++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 61559a694..65040c133 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -703,13 +703,14 @@ func (c *Client) sequence( newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { + // 1) Verify any intermediate headers. var ( interimHeader *types.SignedHeader interimNextVals *types.ValidatorSet err error ) - for height := trustedHeader.Height + 1; height < newHeader.Height; height++ { + for height := trustedHeader.Height + 1; height <= newHeader.Height; height++ { interimHeader, err = c.signedHeaderFromPrimary(height) if err != nil { return errors.Wrapf(err, "failed to obtain the header #%d", height) @@ -734,16 +735,17 @@ func (c *Client) sequence( if err != nil { return errors.Wrapf(err, "failed to obtain the vals #%d", height+1) } - } - err = c.updateTrustedHeaderAndNextVals(interimHeader, interimNextVals) - if err != nil { - return errors.Wrapf(err, "failed to update trusted state #%d", height) + if !bytes.Equal(interimHeader.NextValidatorsHash, interimNextVals.Hash()) { + return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", + interimHeader.NextValidatorsHash, + interimNextVals.Hash(), + interimHeader.Height) + } } trustedHeader, trustedNextVals = interimHeader, interimNextVals } - // 2) Verify the new header. - return VerifyAdjacent(c.chainID, c.latestTrustedHeader, newHeader, newVals, c.trustingPeriod, now) + return nil } // see VerifyHeader @@ -757,7 +759,7 @@ func (c *Client) bisection( interimVals := newVals interimHeader := newHeader - for trustedHeader.Height < newHeader.Height { + for { c.logger.Debug("Verify newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), @@ -767,24 +769,22 @@ func (c *Client) bisection( c.trustLevel) switch err.(type) { case nil: + if interimHeader.Height == newHeader.Height { + return nil + } + // Update the lower bound to the previous upper bound - trustedHeader = interimHeader - trustedNextVals, err = c.validatorSetFromPrimary(interimHeader.Height + 1) + interimNextVals, err := c.validatorSetFromPrimary(interimHeader.Height + 1) if err != nil { return err } - if !bytes.Equal(trustedHeader.NextValidatorsHash, trustedNextVals.Hash()) { + if !bytes.Equal(interimHeader.NextValidatorsHash, interimNextVals.Hash()) { return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", - trustedHeader.NextValidatorsHash, - trustedNextVals.Hash(), - trustedHeader.Height) + interimHeader.NextValidatorsHash, + interimNextVals.Hash(), + interimHeader.Height) } - - err = c.updateTrustedHeaderAndNextVals(trustedHeader, trustedNextVals) - if err != nil { - return err - } - + trustedHeader, trustedNextVals = interimHeader, interimNextVals // Update the upper bound to the untrustedHeader interimHeader, interimVals = newHeader, newVals @@ -799,8 +799,6 @@ func (c *Client) bisection( return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) } } - - return nil } // persist header and next validators to trustedStore. @@ -841,6 +839,10 @@ func (c *Client) backwards(trustedHeader *types.SignedHeader, newHeader *types.S err error ) + if HeaderExpired(newHeader, c.trustingPeriod, now) { + return ErrOldHeaderExpired{newHeader.Time.Add(c.trustingPeriod), now} + } + for trustedHeader.Height > newHeader.Height { interimHeader, err = c.signedHeaderFromPrimary(trustedHeader.Height - 1) if err != nil { @@ -857,10 +859,6 @@ func (c *Client) backwards(trustedHeader *types.SignedHeader, newHeader *types.S trustedHeader.Time) } - if HeaderExpired(interimHeader, c.trustingPeriod, now) { - return ErrOldHeaderExpired{interimHeader.Time.Add(c.trustingPeriod), now} - } - if !bytes.Equal(interimHeader.Hash(), trustedHeader.LastBlockID.Hash) { return errors.Errorf("older header hash %X does not match trusted header's last block %X", interimHeader.Hash(), diff --git a/lite2/client_test.go b/lite2/client_test.go index a6ffc31ab..a6b615227 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -57,6 +57,9 @@ var ( ) func TestClient_SequentialVerification(t *testing.T) { + newKeys := genPrivKeys(4) + newVals := newKeys.ToValidators(10, 1) + testCases := []struct { name string otherHeaders map[int64]*types.SignedHeader // all except ^ @@ -138,6 +141,25 @@ func TestClient_SequentialVerification(t *testing.T) { false, true, }, + { + "bad: different validator set at height 3", + map[int64]*types.SignedHeader{ + // trusted header + 1: h1, + // interim header (3/3 signed) + 2: h2, + // last header (3/3 signed) + 3: h3, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: newVals, + 4: newVals, + }, + false, + true, + }, } for _, tc := range testCases { @@ -368,7 +390,8 @@ func TestClient_Cleanup(t *testing.T) { require.NoError(t, err) c.Stop() - c.Cleanup() + err = c.Cleanup() + require.NoError(t, err) // Check no headers exist after Cleanup. h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) @@ -747,9 +770,6 @@ func TestClient_BackwardsVerification(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // 1) header is missing => expect no error h, err := c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) @@ -762,6 +782,15 @@ func TestClient_BackwardsVerification(t *testing.T) { h, err = c.VerifyHeaderAtHeight(1, bTime.Add(1*time.Hour).Add(1*time.Second)) assert.Error(t, err) assert.NotNil(t, h) + + // 3) already stored headers should return the header without error + h, err = c.VerifyHeaderAtHeight(3, bTime.Add(1*time.Hour).Add(1*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, h) + + // 4) cannot verify a header in the future + _, err = c.VerifyHeaderAtHeight(4, bTime.Add(1*time.Hour).Add(1*time.Second)) + assert.Error(t, err) } func TestClient_NewClientFromTrustedStore(t *testing.T) { From 25d92d05f8734d32ad32569820eb2f112ddf0239 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Tue, 25 Feb 2020 10:04:24 +0100 Subject: [PATCH 28/74] types: VerifyCommitX return when +2/3 sigs are verified (#4445) Closes #4417 --- CHANGELOG_PENDING.md | 2 ++ types/validator_set.go | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index a2fc85ee0..ebc087136 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -19,6 +19,8 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: +- [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) VerifyCommitX() functions should return as soon as +2/3 threashold is reached. + ### BUG FIXES: - [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) diff --git a/types/validator_set.go b/types/validator_set.go index 390df09cf..04de50646 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -636,6 +636,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, } talliedVotingPower := int64(0) + votingPowerNeeded := vals.TotalVotingPower() * 2 / 3 for idx, commitSig := range commit.Signatures { if commitSig.Absent() { continue // OK, some signatures can be absent. @@ -658,13 +659,15 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, // It's OK that the BlockID doesn't match. We include stray // signatures (~votes for nil) to measure validator availability. // } + + // return as soon as +2/3 of the signatures are verified + if talliedVotingPower > votingPowerNeeded { + return nil + } } - if got, needed := talliedVotingPower, vals.TotalVotingPower()*2/3; got <= needed { - return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} - } - - return nil + // talliedVotingPower <= needed, thus return error + return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded} } // VerifyFutureCommit will check to see if the set would be valid with a different @@ -762,6 +765,7 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID, var ( talliedVotingPower int64 seenVals = make(map[int]int, len(commit.Signatures)) // validator index -> commit index + votingPowerNeeded = (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator ) for idx, commitSig := range commit.Signatures { @@ -795,16 +799,14 @@ func (vals *ValidatorSet) VerifyCommitTrusting(chainID string, blockID BlockID, // It's OK that the BlockID doesn't match. We include stray // signatures (~votes for nil) to measure validator availability. // } + + if talliedVotingPower > votingPowerNeeded { + return nil + } } } - got := talliedVotingPower - needed := (vals.TotalVotingPower() * trustLevel.Numerator) / trustLevel.Denominator - if got <= needed { - return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} - } - - return nil + return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded} } func verifyCommitBasic(commit *Commit, height int64, blockID BlockID) error { From c4f7256766fd4cd46ac89c5259e77fe5b0a0bf45 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 26 Feb 2020 10:20:51 +0100 Subject: [PATCH 29/74] lite2: store current validator set (#4472) Before we were storing trustedHeader (height=1) and trustedNextVals (height=2). After this change, we will be storing trustedHeader (height=1) and trustedVals (height=1). This a) simplifies the code b) fixes #4399 inconsistent pairing issue c) gives a relayer access to the current validator set #4470. The only downside is more jumps during bisection. If validator set changes between trustedHeader and the next header (by 2/3 or more), the light client will be forced to download the next header and check that 2/3+ signed the transition. But we don't expect validator set change too much and too often, so it's an acceptable compromise. Closes #4470 and #4399 --- .../adr-046-light-client-implementation.md | 4 +- lite2/client.go | 175 ++++++++---------- lite2/client_test.go | 31 +--- lite2/store/db/db.go | 17 +- lite2/store/db/db_test.go | 18 +- lite2/store/errors.go | 1 + lite2/store/store.go | 12 +- lite2/verifier.go | 14 +- 8 files changed, 119 insertions(+), 153 deletions(-) diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index 11dc79fb9..071149512 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -91,8 +91,8 @@ The light client stores headers & validators in the trusted store: ```go type Store interface { - SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error - DeleteSignedHeaderAndNextValidatorSet(height int64) error + SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error + DeleteSignedHeaderAndValidatorSet(height int64) error SignedHeader(height int64) (*types.SignedHeader, error) ValidatorSet(height int64) (*types.ValidatorSet, error) diff --git a/lite2/client.go b/lite2/client.go index 65040c133..36b366b93 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -122,8 +122,8 @@ type Client struct { trustedStore store.Store // Highest trusted header from the store (height=H). latestTrustedHeader *types.SignedHeader - // Highest next validator set from the store (height=H+1). - latestTrustedNextVals *types.ValidatorSet + // Highest validator set from the store (height=H). + latestTrustedVals *types.ValidatorSet // See UpdatePeriod option updatePeriod time.Duration @@ -228,15 +228,16 @@ func NewClientFromTrustedStore( return nil, err } - if err := c.restoreTrustedHeaderAndNextVals(); err != nil { + if err := c.restoreTrustedHeaderAndVals(); err != nil { return nil, err } return c, nil } -// Load trustedHeader and trustedNextVals from trustedStore. -func (c *Client) restoreTrustedHeaderAndNextVals() error { +// restoreTrustedHeaderAndVals loads trustedHeader and trustedVals from +// trustedStore. +func (c *Client) restoreTrustedHeaderAndVals() error { lastHeight, err := c.trustedStore.LastSignedHeaderHeight() if err != nil { return errors.Wrap(err, "can't get last trusted header height") @@ -248,15 +249,15 @@ func (c *Client) restoreTrustedHeaderAndNextVals() error { return errors.Wrap(err, "can't get last trusted header") } - trustedNextVals, err := c.trustedStore.ValidatorSet(lastHeight + 1) + trustedVals, err := c.trustedStore.ValidatorSet(lastHeight) if err != nil { - return errors.Wrap(err, "can't get last trusted next validators") + return errors.Wrap(err, "can't get last trusted validators") } c.latestTrustedHeader = trustedHeader - c.latestTrustedNextVals = trustedNextVals + c.latestTrustedVals = trustedVals - c.logger.Debug("Restored trusted header and next vals", lastHeight) + c.logger.Debug("Restored trusted header and vals", "height", lastHeight) } return nil @@ -334,7 +335,9 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { return nil } -// Fetch trustedHeader and trustedNextVals from primary provider. +// initializeWithTrustOptions fetches the weakly-trusted header and vals from +// primary provider. The header is cross-checked with witnesses for additional +// security. func (c *Client) initializeWithTrustOptions(options TrustOptions) error { // 1) Fetch and verify the header. h, err := c.signedHeaderFromPrimary(options.Height) @@ -372,15 +375,8 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { return errors.Wrap(err, "invalid commit") } - // 3) Fetch and verify the next vals (verification happens in - // updateTrustedHeaderAndNextVals). - nextVals, err := c.validatorSetFromPrimary(options.Height + 1) - if err != nil { - return err - } - - // 4) Persist both of them and continue. - return c.updateTrustedHeaderAndNextVals(h, nextVals) + // 3) Persist both of them and continue. + return c.updateTrustedHeaderAndVals(h, vals) } // Start starts two processes: 1) auto updating 2) removing outdated headers. @@ -463,9 +459,7 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader // TrustedValidatorSet returns a trusted validator set at the given height. If // a validator set is missing in trustedStore (e.g. the associated header was -// skipped during bisection), it will be downloaded from primary. The second -// return parameter is height validator set corresponds to (useful when you -// pass 0). +// skipped during bisection), it will be downloaded from primary. // // height must be >= 0. // @@ -482,10 +476,10 @@ func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader // // Safe for concurrent use by multiple goroutines. func (c *Client) TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error) { - // Checks height is positive and header (note: height - 1) is not expired. + // Checks height is positive and header is not expired. // Additionally, it fetches validator set from primary if it's missing in // store. - _, err := c.TrustedHeader(height-1, now) + _, err := c.TrustedHeader(height, now) if err != nil { return nil, err } @@ -528,10 +522,12 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe return nil, errors.New("negative or zero height") } + // Check if header already verified. h, err := c.TrustedHeader(height, now) switch err.(type) { - case nil: // Return already trusted header + case nil: c.logger.Info("Header has already been verified", "height", height, "hash", hash2str(h.Hash())) + // Return already trusted header return h, nil case ErrOldHeaderExpired: return nil, err @@ -565,14 +561,11 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // If, at any moment, SignedHeader or ValidatorSet are not found by the primary // provider, provider.ErrSignedHeaderNotFound / // provider.ErrValidatorSetNotFound error is returned. -// -// NOTE: although newVals is entered as input, trustedStore will only store the -// validator set at height newHeader.Height+1 (i.e. -// newHeader.NextValidatorsHash). func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { + // Check if newHeader already verified. h, err := c.TrustedHeader(newHeader.Height, now) switch err.(type) { - case nil: // Return already trusted header + case nil: // Make sure it's the same header. if !bytes.Equal(h.Hash(), newHeader.Hash()) { return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) @@ -597,9 +590,9 @@ func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.Vali if newHeader.Height >= c.latestTrustedHeader.Height { switch c.verificationMode { case sequential: - err = c.sequence(c.latestTrustedHeader, c.latestTrustedNextVals, newHeader, newVals, now) + err = c.sequence(c.latestTrustedHeader, newHeader, newVals, now) case skipping: - err = c.bisection(c.latestTrustedHeader, c.latestTrustedNextVals, newHeader, newVals, now) + err = c.bisection(c.latestTrustedHeader, c.latestTrustedVals, newHeader, newVals, now) default: panic(fmt.Sprintf("Unknown verification mode: %b", c.verificationMode)) } @@ -624,13 +617,7 @@ func (c *Client) verifyHeader(newHeader *types.SignedHeader, newVals *types.Vali return err } - // Update trusted header and vals. - nextVals, err := c.validatorSetFromPrimary(newHeader.Height + 1) - if err != nil { - return err - } - - return c.updateTrustedHeaderAndNextVals(newHeader, nextVals) + return c.updateTrustedHeaderAndVals(newHeader, newVals) } // Primary returns the primary provider. @@ -679,7 +666,7 @@ func (c *Client) cleanup(stopHeight int64) error { stopHeight = oldestHeight } for height := stopHeight; height <= latestHeight; height++ { - err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height) + err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(height) if err != nil { c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) continue @@ -687,8 +674,8 @@ func (c *Client) cleanup(stopHeight int64) error { } c.latestTrustedHeader = nil - c.latestTrustedNextVals = nil - err = c.restoreTrustedHeaderAndNextVals() + c.latestTrustedVals = nil + err = c.restoreTrustedHeaderAndVals() if err != nil { return err } @@ -698,51 +685,46 @@ func (c *Client) cleanup(stopHeight int64) error { // see VerifyHeader func (c *Client) sequence( - trustedHeader *types.SignedHeader, - trustedNextVals *types.ValidatorSet, + initiallyTrustedHeader *types.SignedHeader, newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { - // 1) Verify any intermediate headers. var ( - interimHeader *types.SignedHeader - interimNextVals *types.ValidatorSet - err error + trustedHeader = initiallyTrustedHeader + + interimHeader *types.SignedHeader + interimVals *types.ValidatorSet + + err error ) - for height := trustedHeader.Height + 1; height <= newHeader.Height; height++ { - interimHeader, err = c.signedHeaderFromPrimary(height) - if err != nil { - return errors.Wrapf(err, "failed to obtain the header #%d", height) + + for height := initiallyTrustedHeader.Height + 1; height <= newHeader.Height; height++ { + // 1) Fetch interim headers and vals if needed. + if height == newHeader.Height { // last header + interimHeader, interimVals = newHeader, newVals + } else { // intermediate headers + interimHeader, interimVals, err = c.fetchHeaderAndValsAtHeight(height) + if err != nil { + return errors.Wrapf(err, "failed to obtain the header #%d", height) + } } + // 2) Verify them c.logger.Debug("Verify newHeader against trustedHeader", - "trustedHeight", c.latestTrustedHeader.Height, - "trustedHash", hash2str(c.latestTrustedHeader.Hash()), + "trustedHeight", trustedHeader.Height, + "trustedHash", hash2str(trustedHeader.Hash()), "newHeight", interimHeader.Height, "newHash", hash2str(interimHeader.Hash())) - err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, trustedNextVals, + + err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, interimVals, c.trustingPeriod, now) if err != nil { return errors.Wrapf(err, "failed to verify the header #%d", height) } - // Update trusted header and vals. - if height == newHeader.Height-1 { - interimNextVals = newVals - } else { - interimNextVals, err = c.validatorSetFromPrimary(height + 1) - if err != nil { - return errors.Wrapf(err, "failed to obtain the vals #%d", height+1) - } - if !bytes.Equal(interimHeader.NextValidatorsHash, interimNextVals.Hash()) { - return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", - interimHeader.NextValidatorsHash, - interimNextVals.Hash(), - interimHeader.Height) - } - } - trustedHeader, trustedNextVals = interimHeader, interimNextVals + // 3) Update trustedHeader + trustedHeader = interimHeader } return nil @@ -750,22 +732,28 @@ func (c *Client) sequence( // see VerifyHeader func (c *Client) bisection( - trustedHeader *types.SignedHeader, // height h - trustedNextVals *types.ValidatorSet, // height h + 1 - newHeader *types.SignedHeader, // height g - newVals *types.ValidatorSet, // height g + initiallyTrustedHeader *types.SignedHeader, + initiallyTrustedVals *types.ValidatorSet, + newHeader *types.SignedHeader, + newVals *types.ValidatorSet, now time.Time) error { - interimVals := newVals - interimHeader := newHeader + var ( + trustedHeader = initiallyTrustedHeader + trustedVals = initiallyTrustedVals + + interimHeader = newHeader + interimVals = newVals + ) for { c.logger.Debug("Verify newHeader against trustedHeader", "trustedHeight", trustedHeader.Height, "trustedHash", hash2str(trustedHeader.Hash()), - "newHeight", newHeader.Height, - "newHash", hash2str(newHeader.Hash())) - err := Verify(c.chainID, trustedHeader, trustedNextVals, interimHeader, interimVals, c.trustingPeriod, now, + "newHeight", interimHeader.Height, + "newHash", hash2str(interimHeader.Hash())) + + err := Verify(c.chainID, trustedHeader, trustedVals, interimHeader, interimVals, c.trustingPeriod, now, c.trustLevel) switch err.(type) { case nil: @@ -774,17 +762,7 @@ func (c *Client) bisection( } // Update the lower bound to the previous upper bound - interimNextVals, err := c.validatorSetFromPrimary(interimHeader.Height + 1) - if err != nil { - return err - } - if !bytes.Equal(interimHeader.NextValidatorsHash, interimNextVals.Hash()) { - return errors.Errorf("expected next validator's hash %X, but got %X (height #%d)", - interimHeader.NextValidatorsHash, - interimNextVals.Hash(), - interimHeader.Height) - } - trustedHeader, trustedNextVals = interimHeader, interimNextVals + trustedHeader, trustedVals = interimHeader, interimVals // Update the upper bound to the untrustedHeader interimHeader, interimVals = newHeader, newVals @@ -801,18 +779,17 @@ func (c *Client) bisection( } } -// persist header and next validators to trustedStore. -func (c *Client) updateTrustedHeaderAndNextVals(h *types.SignedHeader, nextVals *types.ValidatorSet) error { - if !bytes.Equal(h.NextValidatorsHash, nextVals.Hash()) { - return errors.Errorf("expected next validator's hash %X, but got %X", h.NextValidatorsHash, nextVals.Hash()) +func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.ValidatorSet) error { + if !bytes.Equal(h.ValidatorsHash, vals.Hash()) { + return errors.Errorf("expected validator's hash %X, but got %X", h.ValidatorsHash, vals.Hash()) } - if err := c.trustedStore.SaveSignedHeaderAndNextValidatorSet(h, nextVals); err != nil { + if err := c.trustedStore.SaveSignedHeaderAndValidatorSet(h, vals); err != nil { return errors.Wrap(err, "failed to save trusted header") } c.latestTrustedHeader = h - c.latestTrustedNextVals = nextVals + c.latestTrustedVals = vals return nil } @@ -898,7 +875,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { } if !bytes.Equal(h.Hash(), altH.Hash()) { - if err = c.latestTrustedNextVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, + if err = c.latestTrustedVals.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, altH.Height, altH.Commit, c.trustLevel); err != nil { c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) witnessesToRemove = append(witnessesToRemove, i) @@ -1002,7 +979,7 @@ func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { break } - err = c.trustedStore.DeleteSignedHeaderAndNextValidatorSet(height) + err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(height) if err != nil { c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) continue diff --git a/lite2/client_test.go b/lite2/client_test.go index a6b615227..3500c918d 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -50,7 +50,6 @@ var ( 1: vals, 2: vals, 3: vals, - 4: vals, }, ) deadNode = mockp.NewDeadMock(chainID) @@ -81,7 +80,6 @@ func TestClient_SequentialVerification(t *testing.T) { 1: vals, 2: vals, 3: vals, - 4: vals, }, false, false, @@ -115,7 +113,6 @@ func TestClient_SequentialVerification(t *testing.T) { 1: vals, 2: vals, 3: vals, - 4: vals, }, false, true, @@ -136,7 +133,6 @@ func TestClient_SequentialVerification(t *testing.T) { 1: vals, 2: vals, 3: vals, - 4: vals, }, false, true, @@ -155,7 +151,6 @@ func TestClient_SequentialVerification(t *testing.T) { 1: vals, 2: vals, 3: newVals, - 4: newVals, }, false, true, @@ -230,7 +225,6 @@ func TestClient_SkippingVerification(t *testing.T) { 1: vals, 2: vals, 3: vals, - 4: vals, }, false, false, @@ -247,7 +241,6 @@ func TestClient_SkippingVerification(t *testing.T) { 1: vals, 2: vals, 3: transitVals, - 4: transitVals, }, false, false, @@ -268,7 +261,6 @@ func TestClient_SkippingVerification(t *testing.T) { 1: vals, 2: vals, 3: newVals, - 4: newVals, }, false, false, @@ -289,7 +281,6 @@ func TestClient_SkippingVerification(t *testing.T) { 1: vals, 2: vals, 3: newVals, - 4: newVals, }, false, true, @@ -404,7 +395,7 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) c, err := NewClient( @@ -429,7 +420,7 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { // 2. options.Hash != trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) // header1 != header @@ -444,7 +435,6 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { }, map[int64]*types.ValidatorSet{ 1: vals, - 2: vals, }, ) @@ -477,7 +467,7 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) c, err := NewClient( @@ -508,7 +498,7 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { // This could happen if previous provider was lying to us. { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) // header1 != header @@ -527,7 +517,6 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { map[int64]*types.ValidatorSet{ 1: vals, 2: vals, - 3: vals, }, ) @@ -560,12 +549,12 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { // 1. options.Hash == trustedHeader.Hash { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) //header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, // []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) - err = trustedStore.SaveSignedHeaderAndNextValidatorSet(h2, vals) + err = trustedStore.SaveSignedHeaderAndValidatorSet(h2, vals) require.NoError(t, err) primary := mockp.New( @@ -577,7 +566,6 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { map[int64]*types.ValidatorSet{ 1: vals, 2: vals, - 3: vals, }, ) @@ -610,7 +598,7 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { // This could happen if previous provider was lying to us. { trustedStore := dbs.New(dbm.NewMemDB(), chainID) - err := trustedStore.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := trustedStore.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) // header1 != header @@ -619,7 +607,7 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { header2 := keys.GenSignedHeader(chainID, 2, bTime.Add(2*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)) - err = trustedStore.SaveSignedHeaderAndNextValidatorSet(header2, vals) + err = trustedStore.SaveSignedHeaderAndValidatorSet(header2, vals) require.NoError(t, err) primary := mockp.New( @@ -629,7 +617,6 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { }, map[int64]*types.ValidatorSet{ 1: vals, - 2: vals, }, ) @@ -796,7 +783,7 @@ func TestClient_BackwardsVerification(t *testing.T) { func TestClient_NewClientFromTrustedStore(t *testing.T) { // 1) Initiate DB and fill with a "trusted" header db := dbs.New(dbm.NewMemDB(), chainID) - err := db.SaveSignedHeaderAndNextValidatorSet(h1, vals) + err := db.SaveSignedHeaderAndValidatorSet(h1, vals) require.NoError(t, err) c, err := NewClientFromTrustedStore( diff --git a/lite2/store/db/db.go b/lite2/store/db/db.go index 89897ed7f..baae0d040 100644 --- a/lite2/store/db/db.go +++ b/lite2/store/db/db.go @@ -31,9 +31,9 @@ func New(db dbm.DB, prefix string) store.Store { return &dbs{db: db, prefix: prefix, cdc: cdc} } -// SaveSignedHeaderAndNextValidatorSet persists SignedHeader and ValidatorSet -// to the db. -func (s *dbs) SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error { +// SaveSignedHeaderAndValidatorSet persists SignedHeader and ValidatorSet to +// the db. +func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error { if sh.Height <= 0 { panic("negative or zero height") } @@ -42,6 +42,7 @@ func (s *dbs) SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet if err != nil { return errors.Wrap(err, "marshalling header") } + valSetBz, err := s.cdc.MarshalBinaryLengthPrefixed(valSet) if err != nil { return errors.Wrap(err, "marshalling validator set") @@ -49,22 +50,22 @@ func (s *dbs) SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet b := s.db.NewBatch() b.Set(s.shKey(sh.Height), shBz) - b.Set(s.vsKey(sh.Height+1), valSetBz) + b.Set(s.vsKey(sh.Height), valSetBz) err = b.WriteSync() b.Close() return err } -// DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader and ValidatorSet -// from the db. -func (s *dbs) DeleteSignedHeaderAndNextValidatorSet(height int64) error { +// DeleteSignedHeaderAndValidatorSet deletes SignedHeader and ValidatorSet from +// the db. +func (s *dbs) DeleteSignedHeaderAndValidatorSet(height int64) error { if height <= 0 { panic("negative or zero height") } b := s.db.NewBatch() b.Delete(s.shKey(height)) - b.Delete(s.vsKey(height + 1)) + b.Delete(s.vsKey(height)) err := b.WriteSync() b.Close() return err diff --git a/lite2/store/db/db_test.go b/lite2/store/db/db_test.go index a1dc3000e..8e317d29b 100644 --- a/lite2/store/db/db_test.go +++ b/lite2/store/db/db_test.go @@ -24,7 +24,7 @@ func TestLast_FirstSignedHeaderHeight(t *testing.T) { assert.EqualValues(t, -1, height) // 1 key - err = dbStore.SaveSignedHeaderAndNextValidatorSet( + err = dbStore.SaveSignedHeaderAndValidatorSet( &types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{}) require.NoError(t, err) @@ -37,20 +37,20 @@ func TestLast_FirstSignedHeaderHeight(t *testing.T) { assert.EqualValues(t, 1, height) } -func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) { - dbStore := New(dbm.NewMemDB(), "Test_SaveSignedHeaderAndNextValidatorSet") +func Test_SaveSignedHeaderAndValidatorSet(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "Test_SaveSignedHeaderAndValidatorSet") // Empty store h, err := dbStore.SignedHeader(1) require.Error(t, err) assert.Nil(t, h) - valSet, err := dbStore.ValidatorSet(2) + valSet, err := dbStore.ValidatorSet(1) require.Error(t, err) assert.Nil(t, valSet) // 1 key - err = dbStore.SaveSignedHeaderAndNextValidatorSet( + err = dbStore.SaveSignedHeaderAndValidatorSet( &types.SignedHeader{Header: &types.Header{Height: 1}}, &types.ValidatorSet{}) require.NoError(t, err) @@ -58,19 +58,19 @@ func Test_SaveSignedHeaderAndNextValidatorSet(t *testing.T) { require.NoError(t, err) assert.NotNil(t, h) - valSet, err = dbStore.ValidatorSet(2) + valSet, err = dbStore.ValidatorSet(1) require.NoError(t, err) assert.NotNil(t, valSet) // Empty store - err = dbStore.DeleteSignedHeaderAndNextValidatorSet(1) + err = dbStore.DeleteSignedHeaderAndValidatorSet(1) require.NoError(t, err) h, err = dbStore.SignedHeader(1) require.Error(t, err) assert.Nil(t, h) - valSet, err = dbStore.ValidatorSet(2) + valSet, err = dbStore.ValidatorSet(1) require.Error(t, err) assert.Nil(t, valSet) } @@ -83,7 +83,7 @@ func Test_SignedHeaderAfter(t *testing.T) { dbStore.SignedHeaderAfter(100) }) - err := dbStore.SaveSignedHeaderAndNextValidatorSet( + err := dbStore.SaveSignedHeaderAndValidatorSet( &types.SignedHeader{Header: &types.Header{Height: 2}}, &types.ValidatorSet{}) require.NoError(t, err) diff --git a/lite2/store/errors.go b/lite2/store/errors.go index 8a587047b..2f77bc893 100644 --- a/lite2/store/errors.go +++ b/lite2/store/errors.go @@ -6,6 +6,7 @@ var ( // ErrSignedHeaderNotFound is returned when a store does not have the // requested header. ErrSignedHeaderNotFound = errors.New("signed header not found") + // ErrValidatorSetNotFound is returned when a store does not have the // requested validator set. ErrValidatorSetNotFound = errors.New("validator set not found") diff --git a/lite2/store/store.go b/lite2/store/store.go index 33b94821d..65cd82731 100644 --- a/lite2/store/store.go +++ b/lite2/store/store.go @@ -4,17 +4,17 @@ import "github.com/tendermint/tendermint/types" // Store is anything that can persistenly store headers. type Store interface { - // SaveSignedHeaderAndNextValidatorSet saves a SignedHeader (h: sh.Height) - // and a ValidatorSet (h: sh.Height+1). + // SaveSignedHeaderAndValidatorSet saves a SignedHeader (h: sh.Height) and a + // ValidatorSet (h: sh.Height). // // height must be > 0. - SaveSignedHeaderAndNextValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error + SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error - // DeleteSignedHeaderAndNextValidatorSet deletes SignedHeader (h: height) and - // ValidatorSet (h: height+1). + // DeleteSignedHeaderAndValidatorSet deletes SignedHeader (h: height) and + // ValidatorSet (h: height). // // height must be > 0. - DeleteSignedHeaderAndNextValidatorSet(height int64) error + DeleteSignedHeaderAndValidatorSet(height int64) error // SignedHeader returns the SignedHeader that corresponds to the given // height. diff --git a/lite2/verifier.go b/lite2/verifier.go index 5491c8d91..c0732a69c 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -21,15 +21,15 @@ var ( // // a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); // b) untrustedHeader is valid; -// c) trustLevel ([1/3, 1]) of trustedHeaderNextVals signed correctly -// (if not, ErrNewValSetCantBeTrusted is returned); +// c) trustLevel ([1/3, 1]) of trustedHeaderVals (or trustedHeaderNextVals) +// signed correctly (if not, ErrNewValSetCantBeTrusted is returned); // d) more than 2/3 of untrustedVals have signed h2 (if not, // ErrNotEnoughVotingPowerSigned is returned); // e) headers are non-adjacent. func VerifyNonAdjacent( chainID string, trustedHeader *types.SignedHeader, // height=X - trustedNextVals *types.ValidatorSet, // height=X+1 + trustedVals *types.ValidatorSet, // height=X or height=X+1 untrustedHeader *types.SignedHeader, // height=Y untrustedVals *types.ValidatorSet, // height=Y trustingPeriod time.Duration, @@ -49,7 +49,7 @@ func VerifyNonAdjacent( } // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. - err := trustedNextVals.VerifyCommitTrusting(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, + err := trustedVals.VerifyCommitTrusting(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, untrustedHeader.Commit, trustLevel) if err != nil { switch e := err.(type) { @@ -78,7 +78,7 @@ func VerifyNonAdjacent( // // a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); // b) untrustedHeader is valid; -// c) untrustedHeader.ValidatorsHash equals trustedHeaderNextVals.Hash() +// c) untrustedHeader.ValidatorsHash equals trustedHeader.NextValidatorsHash; // d) more than 2/3 of new validators (untrustedVals) have signed h2 (if not, // ErrNotEnoughVotingPowerSigned is returned); // e) headers are adjacent. @@ -124,7 +124,7 @@ func VerifyAdjacent( func Verify( chainID string, trustedHeader *types.SignedHeader, // height=X - trustedNextVals *types.ValidatorSet, // height=X+1 + trustedVals *types.ValidatorSet, // height=X or height=X+1 untrustedHeader *types.SignedHeader, // height=Y untrustedVals *types.ValidatorSet, // height=Y trustingPeriod time.Duration, @@ -132,7 +132,7 @@ func Verify( trustLevel tmmath.Fraction) error { if untrustedHeader.Height != trustedHeader.Height+1 { - return VerifyNonAdjacent(chainID, trustedHeader, trustedNextVals, untrustedHeader, untrustedVals, + return VerifyNonAdjacent(chainID, trustedHeader, trustedVals, untrustedHeader, untrustedVals, trustingPeriod, now, trustLevel) } From 9231b52e0dbd0301f2e075bb70a3a9175f058b0c Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Wed, 26 Feb 2020 12:56:20 +0100 Subject: [PATCH 30/74] lite2: cross-check first header and update tests (#4471) closes #4464 --- .../adr-046-light-client-implementation.md | 4 +- lite2/client.go | 5 ++ lite2/client_test.go | 54 ++++++++++++------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index 071149512..71a505b2c 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -2,6 +2,7 @@ ## Changelog * 13-02-2020: Initial draft +* 26-02-2020: Cross-checking the first header ## Context @@ -62,7 +63,8 @@ func NewClient( made to increase security by default. At least one witness is required, although, right now, the light client does not check that primary != witness. When cross-checking a new header with witnesses, minimum number of witnesses -required to respond: 1. +required to respond: 1. Note the very first header (`TrustOptions.Hash`) is +also cross-checked with witnesses for additional security. Due to bisection algorithm nature, some headers might be skipped. If the light client does not have a header for height `X` and `TrustedHeader(X)` or diff --git a/lite2/client.go b/lite2/client.go index 36b366b93..776c758bc 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -356,6 +356,11 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { return errors.Errorf("expected header's hash %X, but got %X", options.Hash, h.Hash()) } + err = c.compareNewHeaderWithWitnesses(h) + if err != nil { + return err + } + // 2) Fetch and verify the vals. vals, err := c.validatorSetFromPrimary(options.Height) if err != nil { diff --git a/lite2/client_test.go b/lite2/client_test.go index 3500c918d..9052d0230 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -802,44 +802,49 @@ func TestClient_NewClientFromTrustedStore(t *testing.T) { assert.EqualValues(t, 1, h.Height) } -func TestClientUpdateErrorsIfAllWitnessesUnavailable(t *testing.T) { - c, err := NewClient( +func TestNewClientErrorsIfAllWitnessesUnavailable(t *testing.T) { + _, err := NewClient( chainID, trustOptions, fullNode, - []provider.Provider{deadNode, deadNode, deadNode}, + []provider.Provider{deadNode, deadNode}, dbs.New(dbm.NewMemDB(), chainID), UpdatePeriod(0), Logger(log.TestingLogger()), MaxRetryAttempts(1), ) - require.NoError(t, err) - - err = c.Update(bTime.Add(2 * time.Hour)) if assert.Error(t, err) { assert.Contains(t, err.Error(), "awaiting response from all witnesses exceeded dropout time") } } func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { - // straight invalid header + // different headers hash then primary plus less than 1/3 signed (no fork) badProvider1 := mockp.New( chainID, map[int64]*types.SignedHeader{ - 3: {Header: nil, Commit: nil}, + 1: h1, + 2: keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, + []byte("app_hash2"), []byte("cons_hash"), []byte("results_hash"), + len(keys), len(keys), types.BlockID{Hash: h1.Hash()}), + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, }, - map[int64]*types.ValidatorSet{}, ) - - // less than 1/3 signed + // header is empty badProvider2 := mockp.New( chainID, map[int64]*types.SignedHeader{ - 3: keys.GenSignedHeaderLastBlockID(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, - []byte("app_hash2"), []byte("cons_hash"), []byte("results_hash"), - len(keys), len(keys), types.BlockID{Hash: h2.Hash()}), + 1: h1, + 2: h2, + 3: {Header: nil, Commit: nil}, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, }, - map[int64]*types.ValidatorSet{}, ) c, err := NewClient( @@ -850,12 +855,21 @@ func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { dbs.New(dbm.NewMemDB(), chainID), UpdatePeriod(0), Logger(log.TestingLogger()), + MaxRetryAttempts(1), ) + // witness should have behaved properly -> no error require.NoError(t, err) + assert.EqualValues(t, 2, len(c.Witnesses())) - err = c.Update(bTime.Add(2 * time.Hour)) - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "could not find any witnesses") - } - assert.Zero(t, 0, len(c.Witnesses())) + // witness behaves incorrectly -> removed from list, no error + h, err := c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour)) + assert.NoError(t, err) + assert.EqualValues(t, 1, len(c.Witnesses())) + // header should still be verified + assert.EqualValues(t, 2, h.Height) + + // no witnesses left to verify -> error + _, err = c.VerifyHeaderAtHeight(3, bTime.Add(2*time.Hour)) + assert.Error(t, err) + assert.EqualValues(t, 0, len(c.Witnesses())) } From 6daea31f50b94e25fd06e99793fb3e4d08284570 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 26 Feb 2020 16:15:06 +0100 Subject: [PATCH 31/74] lite2: remove expiration checks on functions that don't require them (#4477) closes: #4455 Verifying backwards checks that the trustedHeader hasn't expired both before and after the loop in case of verifying many headers (a longer operation), but not during the loop itself. TrustedHeader() no longer checks whether the header saved in the store has expired. Tests have been updated to reflect the changes ## Commits: * verify headers backwards out of trust period * removed expiration check in trusted header func * modified tests to reflect changes * wrote new tests for backwards verification * modified TrustedHeader and TrustedValSet functions * condensed test functions * condensed test functions further * fix build error * update doc * add comments * remove unnecessary declaration * extract latestHeight check into a separate func Co-authored-by: Callum Waters --- .../adr-046-light-client-implementation.md | 4 +- lite2/client.go | 121 +++++---- lite2/client_test.go | 245 ++++++++++-------- lite2/example_test.go | 4 +- lite2/rpc/client.go | 4 +- 5 files changed, 202 insertions(+), 176 deletions(-) diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index 71a505b2c..dbae7e641 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -27,8 +27,8 @@ type Client interface { Cleanup() error // get trusted headers & validators - TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) - TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error) + TrustedHeader(height int64) (*types.SignedHeader, error) + TrustedValidatorSet(height int64) (*types.ValidatorSet, error) LastTrustedHeight() (int64, error) FirstTrustedHeight() (int64, error) diff --git a/lite2/client.go b/lite2/client.go index 776c758bc..1def6195b 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -411,8 +411,6 @@ func (c *Client) Stop() { } // TrustedHeader returns a trusted header at the given height (0 - the latest). -// If a header is missing in trustedStore (e.g. it was skipped during -// bisection), it will be downloaded from primary. // // Headers along with validator sets, which can't be trusted anymore, are // removed once a day (can be changed with RemoveNoLongerTrustedHeadersPeriod @@ -421,77 +419,65 @@ func (c *Client) Stop() { // height must be >= 0. // // It returns an error if: -// - header expired, therefore can't be trusted (ErrOldHeaderExpired); // - there are some issues with the trusted store, although that should not // happen normally; // - negative height is passed; -// - header has not been verified yet +// - header has not been verified yet and is therefore not in the store // // Safe for concurrent use by multiple goroutines. -func (c *Client) TrustedHeader(height int64, now time.Time) (*types.SignedHeader, error) { - if height < 0 { - return nil, errors.New("negative height") - } - - // 1) Get latest height. - latestHeight, err := c.LastTrustedHeight() +func (c *Client) TrustedHeader(height int64) (*types.SignedHeader, error) { + height, err := c.compareWithLatestHeight(height) if err != nil { return nil, err } - if latestHeight == -1 { - return nil, errors.New("no headers exist") - } - if height > latestHeight { - return nil, errors.Errorf("unverified header requested (latest: %d)", latestHeight) - } - if height == 0 { - height = latestHeight - } - - // 2) Get header from store. - h, err := c.trustedStore.SignedHeader(height) - if err != nil { - return nil, err - } - - // 3) Ensure header can still be trusted. - if HeaderExpired(h, c.trustingPeriod, now) { - return nil, ErrOldHeaderExpired{h.Time.Add(c.trustingPeriod), now} - } - - return h, nil + return c.trustedStore.SignedHeader(height) } -// TrustedValidatorSet returns a trusted validator set at the given height. If -// a validator set is missing in trustedStore (e.g. the associated header was -// skipped during bisection), it will be downloaded from primary. +// TrustedValidatorSet returns a trusted validator set at the given height (0 - +// latest). // // height must be >= 0. // -// Headers along with validator sets, which can't be trusted anymore, are +// Headers along with validator sets are // removed once a day (can be changed with RemoveNoLongerTrustedHeadersPeriod // option). // -// It returns an error if: -// - header signed by that validator set expired (ErrOldHeaderExpired) +// Function returns an error if: // - there are some issues with the trusted store, although that should not // happen normally; // - negative height is passed; // - header signed by that validator set has not been verified yet // // Safe for concurrent use by multiple goroutines. -func (c *Client) TrustedValidatorSet(height int64, now time.Time) (*types.ValidatorSet, error) { - // Checks height is positive and header is not expired. - // Additionally, it fetches validator set from primary if it's missing in - // store. - _, err := c.TrustedHeader(height, now) +func (c *Client) TrustedValidatorSet(height int64) (*types.ValidatorSet, error) { + height, err := c.compareWithLatestHeight(height) if err != nil { return nil, err } - return c.trustedStore.ValidatorSet(height) } +func (c *Client) compareWithLatestHeight(height int64) (int64, error) { + latestHeight, err := c.LastTrustedHeight() + if err != nil { + return 0, err + } + if latestHeight == -1 { + return 0, errors.New("no headers exist") + } + + switch { + case height > latestHeight: + return 0, errors.Errorf("unverified header/valset requested (latest: %d)", latestHeight) + case height == 0: + return latestHeight, nil + case height < 0: + return 0, errors.New("negative height") + } + + return height, nil +} + // LastTrustedHeight returns a last trusted height. -1 and nil are returned if // there are no trusted headers. // @@ -528,14 +514,11 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe } // Check if header already verified. - h, err := c.TrustedHeader(height, now) - switch err.(type) { - case nil: + h, err := c.TrustedHeader(height) + if err == nil { c.logger.Info("Header has already been verified", "height", height, "hash", hash2str(h.Hash())) // Return already trusted header return h, nil - case ErrOldHeaderExpired: - return nil, err } // Request the header and the vals. @@ -567,19 +550,19 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // provider, provider.ErrSignedHeaderNotFound / // provider.ErrValidatorSetNotFound error is returned. func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error { + if newHeader.Height <= 0 { + return errors.New("negative or zero height") + } + // Check if newHeader already verified. - h, err := c.TrustedHeader(newHeader.Height, now) - switch err.(type) { - case nil: - // Make sure it's the same header. + h, err := c.TrustedHeader(newHeader.Height) + if err == nil { if !bytes.Equal(h.Hash(), newHeader.Hash()) { return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) } c.logger.Info("Header has already been verified", "height", newHeader.Height, "hash", hash2str(newHeader.Hash())) return nil - case ErrOldHeaderExpired: - return err } return c.verifyHeader(newHeader, newVals, now) @@ -793,8 +776,11 @@ func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.V return errors.Wrap(err, "failed to save trusted header") } - c.latestTrustedHeader = h - c.latestTrustedVals = vals + // Only update latestTrustedHeader if we move forward (not backwards). + if c.latestTrustedHeader == nil || h.Height > c.latestTrustedHeader.Height { + c.latestTrustedHeader = h + c.latestTrustedVals = vals + } return nil } @@ -814,17 +800,21 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, } // Backwards verification (see VerifyHeaderBackwards func in the spec) -func (c *Client) backwards(trustedHeader *types.SignedHeader, newHeader *types.SignedHeader, +func (c *Client) backwards( + initiallyTrustedHeader *types.SignedHeader, + newHeader *types.SignedHeader, now time.Time) error { + + if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { + return ErrOldHeaderExpired{initiallyTrustedHeader.Time.Add(c.trustingPeriod), now} + } + var ( + trustedHeader = initiallyTrustedHeader interimHeader *types.SignedHeader err error ) - if HeaderExpired(newHeader, c.trustingPeriod, now) { - return ErrOldHeaderExpired{newHeader.Time.Add(c.trustingPeriod), now} - } - for trustedHeader.Height > newHeader.Height { interimHeader, err = c.signedHeaderFromPrimary(trustedHeader.Height - 1) if err != nil { @@ -850,6 +840,11 @@ func (c *Client) backwards(trustedHeader *types.SignedHeader, newHeader *types.S trustedHeader = interimHeader } + // Initially trusted header might have expired at this point. + if HeaderExpired(initiallyTrustedHeader, c.trustingPeriod, now) { + return ErrOldHeaderExpired{initiallyTrustedHeader.Time.Add(c.trustingPeriod), now} + } + return nil } diff --git a/lite2/client_test.go b/lite2/client_test.go index 9052d0230..179cf03e4 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -39,18 +39,21 @@ var ( Height: 1, Hash: h1.Hash(), } + valSet = map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + 3: vals, + 4: vals, + } + headerSet = map[int64]*types.SignedHeader{ + 1: h1, + 2: h2, + 3: h3, + } fullNode = mockp.New( chainID, - map[int64]*types.SignedHeader{ - 1: h1, - 2: h2, - 3: h3, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, + headerSet, + valSet, ) deadNode = mockp.NewDeadMock(chainID) ) @@ -76,11 +79,7 @@ func TestClient_SequentialVerification(t *testing.T) { // last header (3/3 signed) 3: h3, }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, + valSet, false, false, }, @@ -109,11 +108,7 @@ func TestClient_SequentialVerification(t *testing.T) { 3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, + valSet, false, true, }, @@ -129,24 +124,13 @@ func TestClient_SequentialVerification(t *testing.T) { 3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), len(keys)-1, len(keys)), }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, + valSet, false, true, }, { "bad: different validator set at height 3", - map[int64]*types.SignedHeader{ - // trusted header - 1: h1, - // interim header (3/3 signed) - 2: h2, - // last header (3/3 signed) - 3: h3, - }, + headerSet, map[int64]*types.ValidatorSet{ 1: vals, 2: vals, @@ -221,11 +205,7 @@ func TestClient_SkippingVerification(t *testing.T) { // last header (3/3 signed) 3: h3, }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - 3: vals, - }, + valSet, false, false, }, @@ -357,12 +337,12 @@ func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { c.RemoveNoLongerTrustedHeaders(now) // Check expired headers are no longer available. - h, err := c.TrustedHeader(1, now) + h, err := c.TrustedHeader(1) assert.Error(t, err) assert.Nil(t, h) // Check not expired headers are available. - h, err = c.TrustedHeader(2, now) + h, err = c.TrustedHeader(2) assert.NoError(t, err) assert.NotNil(t, h) } @@ -385,7 +365,7 @@ func TestClient_Cleanup(t *testing.T) { require.NoError(t, err) // Check no headers exist after Cleanup. - h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.Error(t, err) assert.Nil(t, h) } @@ -411,7 +391,7 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { require.NoError(t, err) defer c.Stop() - h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) @@ -433,9 +413,7 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { // trusted header 1: header1, }, - map[int64]*types.ValidatorSet{ - 1: vals, - }, + valSet, ) c, err := NewClient( @@ -455,7 +433,7 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { require.NoError(t, err) defer c.Stop() - h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), header1.Hash()) @@ -488,7 +466,7 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { defer c.Stop() // Check we still have the 1st header (+header+). - h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) @@ -514,10 +492,7 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { 1: diffHeader1, 2: diffHeader2, }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, + valSet, ) c, err := NewClient( @@ -538,7 +513,7 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { defer c.Stop() // Check we no longer have the invalid 1st header (+header+). - h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.Error(t, err) assert.Nil(t, h) } @@ -557,23 +532,11 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { err = trustedStore.SaveSignedHeaderAndValidatorSet(h2, vals) require.NoError(t, err) - primary := mockp.New( - chainID, - map[int64]*types.SignedHeader{ - 1: h1, - 2: h2, - }, - map[int64]*types.ValidatorSet{ - 1: vals, - 2: vals, - }, - ) - c, err := NewClient( chainID, trustOptions, - primary, - []provider.Provider{primary}, + fullNode, + []provider.Provider{fullNode}, trustedStore, Logger(log.TestingLogger()), ) @@ -583,13 +546,13 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { defer c.Stop() // Check we still have the 1st header (+header+). - h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) // Check we no longer have 2nd header (+header2+). - h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err = c.TrustedHeader(2) assert.Error(t, err) assert.Nil(t, h) } @@ -615,9 +578,7 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { map[int64]*types.SignedHeader{ 1: header1, }, - map[int64]*types.ValidatorSet{ - 1: vals, - }, + valSet, ) c, err := NewClient( @@ -638,13 +599,13 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { defer c.Stop() // Check we have swapped invalid 1st header (+header+) with correct one (+header1+). - h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), header1.Hash()) // Check we no longer have invalid 2nd header (+header2+). - h, err = c.TrustedHeader(2, bTime.Add(2*time.Hour).Add(1*time.Second)) + h, err = c.TrustedHeader(2) assert.Error(t, err) assert.Nil(t, h) } @@ -668,7 +629,7 @@ func TestClient_Update(t *testing.T) { err = c.Update(bTime.Add(2 * time.Hour)) require.NoError(t, err) - h, err := c.TrustedHeader(3, bTime.Add(2*time.Hour)) + h, err := c.TrustedHeader(3) assert.NoError(t, err) require.NotNil(t, h) assert.EqualValues(t, 3, h.Height) @@ -709,11 +670,11 @@ func TestClient_Concurrency(t *testing.T) { _, err = c.FirstTrustedHeight() assert.NoError(t, err) - h, err := c.TrustedHeader(1, bTime.Add(2*time.Hour)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.NotNil(t, h) - vals, err := c.TrustedValidatorSet(2, bTime.Add(2*time.Hour)) + vals, err := c.TrustedValidatorSet(2) assert.NoError(t, err) assert.NotNil(t, vals) }() @@ -743,41 +704,111 @@ func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { } func TestClient_BackwardsVerification(t *testing.T) { - c, err := NewClient( - chainID, - TrustOptions{ - Period: 1 * time.Hour, - Height: 3, - Hash: h3.Hash(), - }, - fullNode, - []provider.Provider{fullNode}, - dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), - Logger(log.TestingLogger()), - ) - require.NoError(t, err) + { + c, err := NewClient( + chainID, + TrustOptions{ + Period: 1 * time.Hour, + Height: 3, + Hash: h3.Hash(), + }, + fullNode, + []provider.Provider{fullNode}, + dbs.New(dbm.NewMemDB(), chainID), + UpdatePeriod(0), + Logger(log.TestingLogger()), + ) + require.NoError(t, err) - // 1) header is missing => expect no error - h, err := c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) - require.NoError(t, err) - if assert.NotNil(t, h) { - assert.EqualValues(t, 2, h.Height) + // 1) header is missing => expect no error + h, err := c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + require.NoError(t, err) + if assert.NotNil(t, h) { + assert.EqualValues(t, 2, h.Height) + } + + // 2) untrusted header is expired but trusted header is not => expect no error + h, err = c.VerifyHeaderAtHeight(1, bTime.Add(1*time.Hour).Add(1*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, h) + + // 3) already stored headers should return the header without error + h, err = c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, h) } + { + c, err := NewClient( + chainID, + TrustOptions{ + Period: 1 * time.Hour, + Height: 3, + Hash: h3.Hash(), + }, + fullNode, + []provider.Provider{fullNode}, + dbs.New(dbm.NewMemDB(), chainID), + UpdatePeriod(0), + Logger(log.TestingLogger()), + ) + require.NoError(t, err) - // 2) header is missing, but it's expired => expect error - h, err = c.VerifyHeaderAtHeight(1, bTime.Add(1*time.Hour).Add(1*time.Second)) - assert.Error(t, err) - assert.NotNil(t, h) + // 3) trusted header has expired => expect error + _, err = c.VerifyHeaderAtHeight(1, bTime.Add(4*time.Hour).Add(1*time.Second)) + assert.Error(t, err) + } + { + testCases := []struct { + provider provider.Provider + }{ + { + // provides incorrect height + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: h1, + 2: keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + 3: h3, + }, + valSet, + ), + }, + { + // provides incorrect hash + mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: h1, + 2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + 3: h3, + }, + valSet, + ), + }, + } - // 3) already stored headers should return the header without error - h, err = c.VerifyHeaderAtHeight(3, bTime.Add(1*time.Hour).Add(1*time.Second)) - assert.NoError(t, err) - assert.NotNil(t, h) + for _, tc := range testCases { + c, err := NewClient( + chainID, + TrustOptions{ + Period: 1 * time.Hour, + Height: 3, + Hash: h3.Hash(), + }, + tc.provider, + []provider.Provider{tc.provider}, + dbs.New(dbm.NewMemDB(), chainID), + UpdatePeriod(0), + Logger(log.TestingLogger()), + ) + require.NoError(t, err) - // 4) cannot verify a header in the future - _, err = c.VerifyHeaderAtHeight(4, bTime.Add(1*time.Hour).Add(1*time.Second)) - assert.Error(t, err) + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(1*time.Hour).Add(1*time.Second)) + assert.Error(t, err) + } + } } func TestClient_NewClientFromTrustedStore(t *testing.T) { @@ -797,7 +828,7 @@ func TestClient_NewClientFromTrustedStore(t *testing.T) { // 2) Check header exists (deadNode is being used to ensure we're not getting // it from primary) - h, err := c.TrustedHeader(1, bTime.Add(1*time.Second)) + h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.EqualValues(t, 1, h.Height) } diff --git a/lite2/example_test.go b/lite2/example_test.go index 87e839d9b..c8d883958 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -76,7 +76,7 @@ func TestExample_Client_AutoUpdate(t *testing.T) { time.Sleep(2 * time.Second) - h, err := c.TrustedHeader(0, time.Now()) + h, err := c.TrustedHeader(0) if err != nil { stdlog.Fatal(err) } @@ -146,7 +146,7 @@ func TestExample_Client_ManualUpdate(t *testing.T) { stdlog.Fatal(err) } - h, err := c.TrustedHeader(3, time.Now()) + h, err := c.TrustedHeader(3) if err != nil { stdlog.Fatal(err) } diff --git a/lite2/rpc/client.go b/lite2/rpc/client.go index 9778878ac..b59d459bc 100644 --- a/lite2/rpc/client.go +++ b/lite2/rpc/client.go @@ -191,7 +191,7 @@ func (c *Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlock // Verify each of the BlockMetas. for _, meta := range res.BlockMetas { - h, err := c.lc.TrustedHeader(meta.Header.Height, time.Now()) + h, err := c.lc.TrustedHeader(meta.Header.Height) if err != nil { return nil, errors.Wrapf(err, "TrustedHeader(%d)", meta.Header.Height) } @@ -331,7 +331,7 @@ func (c *Client) updateLiteClientIfNeededTo(height int64) (*types.SignedHeader, return c.lc.VerifyHeaderAtHeight(height, time.Now()) } - h, err := c.lc.TrustedHeader(height, time.Now()) + h, err := c.lc.TrustedHeader(height) if err != nil { return nil, errors.Wrapf(err, "TrustedHeader(#%d)", height) } From b5ce8f206989f56398d5282a5c6c04a34f8a0306 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2020 16:51:05 +0100 Subject: [PATCH 32/74] deps: bump github.com/tendermint/tm-db from 0.4.0 to 0.4.1 (#4476) Bumps [github.com/tendermint/tm-db](https://github.com/tendermint/tm-db) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/tendermint/tm-db/releases) - [Changelog](https://github.com/tendermint/tm-db/blob/master/CHANGELOG.md) - [Commits](https://github.com/tendermint/tm-db/compare/v0.4.0...v0.4.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: Anton Kaliaev --- go.mod | 2 +- go.sum | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 52d2d285d..a5faa59b1 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/spf13/viper v1.6.2 github.com/stretchr/testify v1.5.1 github.com/tendermint/go-amino v0.14.1 - github.com/tendermint/tm-db v0.4.0 + github.com/tendermint/tm-db v0.4.1 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 google.golang.org/grpc v1.27.1 diff --git a/go.sum b/go.sum index 3146cb41f..0e05ecad9 100644 --- a/go.sum +++ b/go.sum @@ -390,10 +390,14 @@ github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+ github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= github.com/tecbot/gorocksdb v0.0.0-20191017175515-d217d93fd4c5 h1:gVwAW5OwaZlDB5/CfqcGFM9p9C+KxvQKyNOltQ8orj0= github.com/tecbot/gorocksdb v0.0.0-20191017175515-d217d93fd4c5/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= github.com/tendermint/tm-db v0.4.0 h1:iPbCcLbf4nwDFhS39Zo1lpdS1X/cT9CkTlUx17FHQgA= github.com/tendermint/tm-db v0.4.0/go.mod h1:+Cwhgowrf7NBGXmsqFMbwEtbo80XmyrlY5Jsk95JubQ= +github.com/tendermint/tm-db v0.4.1 h1:TvX7JWjJOVZ+N3y+I86wddrGttOdMmmBxXcu0/Y7ZJ0= +github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= From b9535a2cfc9dea99455a42cbc303c15ac42c5cbb Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 26 Feb 2020 20:47:16 +0100 Subject: [PATCH 33/74] blockchain (v2): comment out TestReactorTerminationScenarios (#4480) Refs #4442 --- blockchain/v2/reactor_test.go | 333 +++++++++++++++++----------------- 1 file changed, 166 insertions(+), 167 deletions(-) diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go index 10457cdb0..3a5ef5a89 100644 --- a/blockchain/v2/reactor_test.go +++ b/blockchain/v2/reactor_test.go @@ -6,7 +6,6 @@ import ( "sort" "sync" "testing" - "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -166,179 +165,179 @@ func newTestReactor(p testReactorParams) *BlockchainReactor { return r } -func TestReactorTerminationScenarios(t *testing.T) { +// func TestReactorTerminationScenarios(t *testing.T) { - config := cfg.ResetTestRoot("blockchain_reactor_v2_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) - refStore, _, _ := newReactorStore(genDoc, privVals, 20) +// config := cfg.ResetTestRoot("blockchain_reactor_v2_test") +// defer os.RemoveAll(config.RootDir) +// genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) +// refStore, _, _ := newReactorStore(genDoc, privVals, 20) - params := testReactorParams{ - logger: log.TestingLogger(), - genDoc: genDoc, - privVals: privVals, - startHeight: 10, - bufferSize: 100, - mockA: true, - } +// params := testReactorParams{ +// logger: log.TestingLogger(), +// genDoc: genDoc, +// privVals: privVals, +// startHeight: 10, +// bufferSize: 100, +// mockA: true, +// } - type testEvent struct { - evType string - peer string - height int64 - } +// type testEvent struct { +// evType string +// peer string +// height int64 +// } - tests := []struct { - name string - params testReactorParams - msgs []testEvent - }{ - { - name: "simple termination on max peer height - one peer", - params: params, - msgs: []testEvent{ - {evType: "AddPeer", peer: "P1"}, - {evType: "ReceiveS", peer: "P1", height: 13}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P1", height: 11}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P1", height: 12}, - {evType: "Process"}, - {evType: "ReceiveB", peer: "P1", height: 13}, - {evType: "Process"}, - }, - }, - { - name: "simple termination on max peer height - two peers", - params: params, - msgs: []testEvent{ - {evType: "AddPeer", peer: "P1"}, - {evType: "AddPeer", peer: "P2"}, - {evType: "ReceiveS", peer: "P1", height: 13}, - {evType: "ReceiveS", peer: "P2", height: 15}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P1", height: 11}, - {evType: "ReceiveB", peer: "P2", height: 12}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P1", height: 13}, - {evType: "Process"}, - {evType: "ReceiveB", peer: "P2", height: 14}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 15}, - {evType: "Process"}, - }, - }, - { - name: "termination on max peer height - two peers, noBlock error", - params: params, - msgs: []testEvent{ - {evType: "AddPeer", peer: "P1"}, - {evType: "AddPeer", peer: "P2"}, - {evType: "ReceiveS", peer: "P1", height: 13}, - {evType: "ReceiveS", peer: "P2", height: 15}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveNB", peer: "P1", height: 11}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 12}, - {evType: "ReceiveB", peer: "P2", height: 11}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 13}, - {evType: "Process"}, - {evType: "ReceiveB", peer: "P2", height: 14}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 15}, - {evType: "Process"}, - }, - }, - { - name: "termination on max peer height - two peers, remove one peer", - params: params, - msgs: []testEvent{ - {evType: "AddPeer", peer: "P1"}, - {evType: "AddPeer", peer: "P2"}, - {evType: "ReceiveS", peer: "P1", height: 13}, - {evType: "ReceiveS", peer: "P2", height: 15}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "RemovePeer", peer: "P1"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 12}, - {evType: "ReceiveB", peer: "P2", height: 11}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 13}, - {evType: "Process"}, - {evType: "ReceiveB", peer: "P2", height: 14}, - {evType: "Process"}, - {evType: "BlockReq"}, - {evType: "ReceiveB", peer: "P2", height: 15}, - {evType: "Process"}, - }, - }, - } +// tests := []struct { +// name string +// params testReactorParams +// msgs []testEvent +// }{ +// { +// name: "simple termination on max peer height - one peer", +// params: params, +// msgs: []testEvent{ +// {evType: "AddPeer", peer: "P1"}, +// {evType: "ReceiveS", peer: "P1", height: 13}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P1", height: 11}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P1", height: 12}, +// {evType: "Process"}, +// {evType: "ReceiveB", peer: "P1", height: 13}, +// {evType: "Process"}, +// }, +// }, +// { +// name: "simple termination on max peer height - two peers", +// params: params, +// msgs: []testEvent{ +// {evType: "AddPeer", peer: "P1"}, +// {evType: "AddPeer", peer: "P2"}, +// {evType: "ReceiveS", peer: "P1", height: 13}, +// {evType: "ReceiveS", peer: "P2", height: 15}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P1", height: 11}, +// {evType: "ReceiveB", peer: "P2", height: 12}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P1", height: 13}, +// {evType: "Process"}, +// {evType: "ReceiveB", peer: "P2", height: 14}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 15}, +// {evType: "Process"}, +// }, +// }, +// { +// name: "termination on max peer height - two peers, noBlock error", +// params: params, +// msgs: []testEvent{ +// {evType: "AddPeer", peer: "P1"}, +// {evType: "AddPeer", peer: "P2"}, +// {evType: "ReceiveS", peer: "P1", height: 13}, +// {evType: "ReceiveS", peer: "P2", height: 15}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveNB", peer: "P1", height: 11}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 12}, +// {evType: "ReceiveB", peer: "P2", height: 11}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 13}, +// {evType: "Process"}, +// {evType: "ReceiveB", peer: "P2", height: 14}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 15}, +// {evType: "Process"}, +// }, +// }, +// { +// name: "termination on max peer height - two peers, remove one peer", +// params: params, +// msgs: []testEvent{ +// {evType: "AddPeer", peer: "P1"}, +// {evType: "AddPeer", peer: "P2"}, +// {evType: "ReceiveS", peer: "P1", height: 13}, +// {evType: "ReceiveS", peer: "P2", height: 15}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "RemovePeer", peer: "P1"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 12}, +// {evType: "ReceiveB", peer: "P2", height: 11}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 13}, +// {evType: "Process"}, +// {evType: "ReceiveB", peer: "P2", height: 14}, +// {evType: "Process"}, +// {evType: "BlockReq"}, +// {evType: "ReceiveB", peer: "P2", height: 15}, +// {evType: "Process"}, +// }, +// }, +// } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - reactor := newTestReactor(params) - reactor.Start() - reactor.reporter = behaviour.NewMockReporter() - mockSwitch := &mockSwitchIo{switchedToConsensus: false} - reactor.io = mockSwitch - // time for go routines to start - time.Sleep(time.Millisecond) +// for _, tt := range tests { +// tt := tt +// t.Run(tt.name, func(t *testing.T) { +// reactor := newTestReactor(params) +// reactor.Start() +// reactor.reporter = behaviour.NewMockReporter() +// mockSwitch := &mockSwitchIo{switchedToConsensus: false} +// reactor.io = mockSwitch +// // time for go routines to start +// time.Sleep(time.Millisecond) - for _, step := range tt.msgs { - switch step.evType { - case "AddPeer": - reactor.scheduler.send(bcAddNewPeer{peerID: p2p.ID(step.peer)}) - case "RemovePeer": - reactor.scheduler.send(bcRemovePeer{peerID: p2p.ID(step.peer)}) - case "ReceiveS": - reactor.scheduler.send(bcStatusResponse{ - peerID: p2p.ID(step.peer), - height: step.height, - time: time.Now(), - }) - case "ReceiveB": - reactor.scheduler.send(bcBlockResponse{ - peerID: p2p.ID(step.peer), - block: refStore.LoadBlock(step.height), - size: 10, - time: time.Now(), - }) - case "ReceiveNB": - reactor.scheduler.send(bcNoBlockResponse{ - peerID: p2p.ID(step.peer), - height: step.height, - time: time.Now(), - }) - case "BlockReq": - reactor.scheduler.send(rTrySchedule{time: time.Now()}) - case "Process": - reactor.processor.send(rProcessBlock{}) - } - // give time for messages to propagate between routines - time.Sleep(time.Millisecond) - } +// for _, step := range tt.msgs { +// switch step.evType { +// case "AddPeer": +// reactor.scheduler.send(bcAddNewPeer{peerID: p2p.ID(step.peer)}) +// case "RemovePeer": +// reactor.scheduler.send(bcRemovePeer{peerID: p2p.ID(step.peer)}) +// case "ReceiveS": +// reactor.scheduler.send(bcStatusResponse{ +// peerID: p2p.ID(step.peer), +// height: step.height, +// time: time.Now(), +// }) +// case "ReceiveB": +// reactor.scheduler.send(bcBlockResponse{ +// peerID: p2p.ID(step.peer), +// block: refStore.LoadBlock(step.height), +// size: 10, +// time: time.Now(), +// }) +// case "ReceiveNB": +// reactor.scheduler.send(bcNoBlockResponse{ +// peerID: p2p.ID(step.peer), +// height: step.height, +// time: time.Now(), +// }) +// case "BlockReq": +// reactor.scheduler.send(rTrySchedule{time: time.Now()}) +// case "Process": +// reactor.processor.send(rProcessBlock{}) +// } +// // give time for messages to propagate between routines +// time.Sleep(time.Millisecond) +// } - // time for processor to finish and reactor to switch to consensus - time.Sleep(20 * time.Millisecond) - assert.True(t, mockSwitch.hasSwitchedToConsensus()) - reactor.Stop() - }) - } -} +// // time for processor to finish and reactor to switch to consensus +// time.Sleep(20 * time.Millisecond) +// assert.True(t, mockSwitch.hasSwitchedToConsensus()) +// reactor.Stop() +// }) +// } +// } func TestReactorHelperMode(t *testing.T) { var ( From c45baa04027b555196031420574342a54888eed1 Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Thu, 27 Feb 2020 11:25:21 +0100 Subject: [PATCH 34/74] blockchain (v2): async testing notes (#4484) Add some inline notes for context of why test are commented out and not deleted. Ref. #4482 --- blockchain/v2/reactor_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go index 3a5ef5a89..c187f954b 100644 --- a/blockchain/v2/reactor_test.go +++ b/blockchain/v2/reactor_test.go @@ -165,6 +165,8 @@ func newTestReactor(p testReactorParams) *BlockchainReactor { return r } +// This test is left here and not deleted to retain the termination cases for +// future improvement in [#4482](https://github.com/tendermint/tendermint/issues/4482). // func TestReactorTerminationScenarios(t *testing.T) { // config := cfg.ResetTestRoot("blockchain_reactor_v2_test") From 875b106a172df215a6d88450ecf0c0fb29e33ab2 Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 27 Feb 2020 11:34:56 +0100 Subject: [PATCH 35/74] github: edit templates for use in issues and pull requests (#4483) * github: edit templates for use in issues and pull requests Signed-off-by: Marko Baricevic * fix sdk to tendermint --- .github/ISSUE_TEMPLATE/feature-request.md | 35 +++++++++++++++++++---- .github/PULL_REQUEST_TEMPLATE.md | 29 +++++++++++++------ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index e88fe61d4..62c3e4f3a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -3,11 +3,34 @@ name: Feature Request about: Create a proposal to request a feature --- - -Word of caution: poorly thought out proposals may be rejected without deliberation ---> +## Summary + + + +## Problem Definition + + + +## Proposal + + + +____ + +#### For Admin Use + +- [ ] Not duplicate issue +- [ ] Appropriate labels applied +- [ ] Appropriate contributors tagged +- [ ] Contributor assigned/self-assigned diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e1863c783..8590275a7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,24 @@ - -Thanks for filing a PR! Before hitting the button, please check the following items. -Please note that every non-trivial PR must reference an issue that explains the -changes in the PR. +Closes: #XXX +## Description + + -* [ ] Referenced an issue explaining the need for the change -* [ ] Updated all relevant documentation in docs -* [ ] Updated all code comments where relevant -* [ ] Wrote tests -* [ ] Updated CHANGELOG_PENDING.md + +______ + +For contributor use: + +- [ ] Wrote tests +- [ ] Updated CHANGELOG_PENDING.md +- [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. +- [ ] Updated relevant documentation (`docs/`) and code comments +- [ ] Re-reviewed `Files changed` in the Github PR explorer From 6be0e138233a9d42667dcc8884fef098e3e733e6 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Thu, 27 Feb 2020 14:57:17 +0100 Subject: [PATCH 36/74] lite2: prune-headers (#4478) closes #4469 Improved speed of cleanup by using SignedHeaderAfter instead of TrustedHeader to jump from header to header. Prune() is now called when a new header and validator set are saved and is a function dealt by the database itself ## Commits: * prune headers and vals * modified cleanup and tests * fixes after my own review * implement Prune func * make db ops concurrently safe * use Iterator in SignedHeaderAfter we should iterate from height+1, not from the end! * simplify cleanup Co-authored-by: Anton Kaliaev --- lite2/client.go | 179 ++++++++++++-------------------------- lite2/client_test.go | 55 +----------- lite2/store/db/db.go | 137 ++++++++++++++++++++++++++++- lite2/store/db/db_test.go | 67 ++++++++++++++ lite2/store/store.go | 7 ++ 5 files changed, 269 insertions(+), 176 deletions(-) diff --git a/lite2/client.go b/lite2/client.go index 1def6195b..c52ffe832 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -22,9 +22,9 @@ const ( sequential mode = iota + 1 skipping - defaultUpdatePeriod = 5 * time.Second - defaultRemoveNoLongerTrustedHeadersPeriod = 24 * time.Hour - defaultMaxRetryAttempts = 10 + defaultUpdatePeriod = 5 * time.Second + defaultPruningSize = 1000 + defaultMaxRetryAttempts = 10 ) // Option sets a parameter for the light client. @@ -62,13 +62,14 @@ func UpdatePeriod(d time.Duration) Option { } } -// RemoveNoLongerTrustedHeadersPeriod option can be used to define how often -// the routine, which cleans up no longer trusted headers (outside of trusting -// period), is run. Default: once a day. When set to zero, the routine won't be -// started. -func RemoveNoLongerTrustedHeadersPeriod(d time.Duration) Option { +// PruningSize option sets the maximum amount of headers & validator set pairs +// that the light client stores. When Prune() is run, all headers (along with +// the associated validator sets) that are earlier than the h amount of headers +// will be removed from the store. Default: 1000. A pruning size of 0 will not +// prune the lite client at all. +func PruningSize(h uint16) Option { return func(c *Client) { - c.removeNoLongerTrustedHeadersPeriod = d + c.pruningSize = h } } @@ -128,7 +129,7 @@ type Client struct { // See UpdatePeriod option updatePeriod time.Duration // See RemoveNoLongerTrustedHeadersPeriod option - removeNoLongerTrustedHeadersPeriod time.Duration + pruningSize uint16 // See ConfirmationFunction option confirmationFn func(action string) bool @@ -191,19 +192,19 @@ func NewClientFromTrustedStore( options ...Option) (*Client, error) { c := &Client{ - chainID: chainID, - trustingPeriod: trustingPeriod, - verificationMode: skipping, - trustLevel: DefaultTrustLevel, - maxRetryAttempts: defaultMaxRetryAttempts, - primary: primary, - witnesses: witnesses, - trustedStore: trustedStore, - updatePeriod: defaultUpdatePeriod, - removeNoLongerTrustedHeadersPeriod: defaultRemoveNoLongerTrustedHeadersPeriod, - confirmationFn: func(action string) bool { return true }, - quit: make(chan struct{}), - logger: log.NewNopLogger(), + chainID: chainID, + trustingPeriod: trustingPeriod, + verificationMode: skipping, + trustLevel: DefaultTrustLevel, + maxRetryAttempts: defaultMaxRetryAttempts, + primary: primary, + witnesses: witnesses, + trustedStore: trustedStore, + updatePeriod: defaultUpdatePeriod, + pruningSize: defaultPruningSize, + confirmationFn: func(action string) bool { return true }, + quit: make(chan struct{}), + logger: log.NewNopLogger(), } for _, o := range options { @@ -304,7 +305,10 @@ func (c *Client) checkTrustedHeaderUsingOptions(options TrustOptions) error { c.latestTrustedHeader.Height, c.latestTrustedHeader.Hash()) if c.confirmationFn(action) { // remove all the headers (options.Height, trustedHeader.Height] - c.cleanup(options.Height + 1) + err := c.cleanupAfter(options.Height) + if err != nil { + return errors.Wrapf(err, "cleanupAfter(%d)", options.Height) + } c.logger.Info("Rolled back to older header (newer headers were removed)", "old", options.Height) @@ -388,11 +392,6 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { func (c *Client) Start() error { c.logger.Info("Starting light client") - if c.removeNoLongerTrustedHeadersPeriod > 0 { - c.routinesWaitGroup.Add(1) - go c.removeNoLongerTrustedHeadersRoutine() - } - if c.updatePeriod > 0 { c.routinesWaitGroup.Add(1) go c.autoUpdateRoutine() @@ -557,6 +556,7 @@ func (c *Client) VerifyHeader(newHeader *types.SignedHeader, newVals *types.Vali // Check if newHeader already verified. h, err := c.TrustedHeader(newHeader.Height) if err == nil { + // Make sure it's the same header. if !bytes.Equal(h.Hash(), newHeader.Hash()) { return errors.Errorf("existing trusted header %X does not match newHeader %X", h.Hash(), newHeader.Hash()) } @@ -630,40 +630,36 @@ func (c *Client) Witnesses() []provider.Provider { // client must be stopped at this point. func (c *Client) Cleanup() error { c.logger.Info("Removing all the data") - return c.cleanup(0) + c.latestTrustedHeader = nil + c.latestTrustedVals = nil + return c.trustedStore.Prune(0) } -// cleanup deletes all headers & validator sets between +stopHeight+ and latest -// height included. It also sets trustedHeader (vals) to the latest header -// (vals) if such exists. -func (c *Client) cleanup(stopHeight int64) error { - // 1) Get the oldest height. - oldestHeight, err := c.trustedStore.FirstSignedHeaderHeight() - if err != nil { - return errors.Wrap(err, "can't get first trusted height") - } +// cleanupAfter deletes all headers & validator sets after +height+. It also +// resets latestTrustedHeader to the latest header. +func (c *Client) cleanupAfter(height int64) error { + nextHeight := height - // 2) Get the latest height. - latestHeight, err := c.trustedStore.LastSignedHeaderHeight() - if err != nil { - return errors.Wrap(err, "can't get last trusted height") - } - - // 3) Remove all headers and validator sets. - if stopHeight < oldestHeight { - stopHeight = oldestHeight - } - for height := stopHeight; height <= latestHeight; height++ { - err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(height) - if err != nil { - c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) - continue + for { + h, err := c.trustedStore.SignedHeaderAfter(nextHeight) + if err == store.ErrSignedHeaderNotFound { + break + } else if err != nil { + return errors.Wrapf(err, "failed to get header after %d", nextHeight) } + + err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(h.Height) + if err != nil { + c.logger.Error("can't remove a trusted header & validator set", "err", err, + "height", h.Height) + } + + nextHeight = h.Height } c.latestTrustedHeader = nil c.latestTrustedVals = nil - err = c.restoreTrustedHeaderAndVals() + err := c.restoreTrustedHeaderAndVals() if err != nil { return err } @@ -776,7 +772,12 @@ func (c *Client) updateTrustedHeaderAndVals(h *types.SignedHeader, vals *types.V return errors.Wrap(err, "failed to save trusted header") } - // Only update latestTrustedHeader if we move forward (not backwards). + if c.pruningSize > 0 { + if err := c.trustedStore.Prune(c.pruningSize); err != nil { + return errors.Wrap(err, "prune") + } + } + if c.latestTrustedHeader == nil || h.Height > c.latestTrustedHeader.Height { c.latestTrustedHeader = h c.latestTrustedVals = vals @@ -921,72 +922,6 @@ func (c *Client) removeWitness(idx int) { } } -func (c *Client) removeNoLongerTrustedHeadersRoutine() { - defer c.routinesWaitGroup.Done() - - ticker := time.NewTicker(c.removeNoLongerTrustedHeadersPeriod) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - c.RemoveNoLongerTrustedHeaders(time.Now()) - case <-c.quit: - return - } - } -} - -// RemoveNoLongerTrustedHeaders removes no longer trusted headers (due to -// expiration). -// -// Exposed for testing. -func (c *Client) RemoveNoLongerTrustedHeaders(now time.Time) { - // 1) Get the oldest height. - oldestHeight, err := c.FirstTrustedHeight() - if err != nil { - c.logger.Error("can't get first trusted height", "err", err) - return - } - if oldestHeight == -1 { // no headers yet => wait - return - } - - // 2) Get the latest height. - latestHeight, err := c.LastTrustedHeight() - if err != nil { - c.logger.Error("can't get last trusted height", "err", err) - return - } - if latestHeight == -1 { // no headers yet => wait - return - } - - // 3) Remove all headers that are outside of the trusting period. - // - // NOTE: even the latest header can be removed. it's okay because - // c.latestTrustedHeader will retain it in memory so other funcs like VerifyHeader - // don't crash. - for height := oldestHeight; height <= latestHeight; height++ { - h, err := c.trustedStore.SignedHeader(height) - if err != nil { - c.logger.Error("can't get a trusted header", "err", err, "height", height) - continue - } - - // Stop if the header is within the trusting period. - if !HeaderExpired(h, c.trustingPeriod, now) { - break - } - - err = c.trustedStore.DeleteSignedHeaderAndValidatorSet(height) - if err != nil { - c.logger.Error("can't remove a trusted header & validator set", "err", err, "height", height) - continue - } - } -} - func (c *Client) autoUpdateRoutine() { defer c.routinesWaitGroup.Done() diff --git a/lite2/client_test.go b/lite2/client_test.go index 179cf03e4..ad9088e8c 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -306,47 +306,6 @@ func TestClient_SkippingVerification(t *testing.T) { } } -func TestClientRemovesNoLongerTrustedHeaders(t *testing.T) { - c, err := NewClient( - chainID, - trustOptions, - fullNode, - []provider.Provider{fullNode}, - dbs.New(dbm.NewMemDB(), chainID), - Logger(log.TestingLogger()), - ) - - assert.NotPanics(t, func() { - now := bTime.Add(4 * time.Hour).Add(1 * time.Second) - c.RemoveNoLongerTrustedHeaders(now) - }) - - require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() - - // Verify new headers. - _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second)) - require.NoError(t, err) - now := bTime.Add(4 * time.Hour).Add(1 * time.Second) - _, err = c.VerifyHeaderAtHeight(3, now) - require.NoError(t, err) - - // Remove expired headers. - c.RemoveNoLongerTrustedHeaders(now) - - // Check expired headers are no longer available. - h, err := c.TrustedHeader(1) - assert.Error(t, err) - assert.Nil(t, h) - - // Check not expired headers are available. - h, err = c.TrustedHeader(2) - assert.NoError(t, err) - assert.NotNil(t, h) -} - func TestClient_Cleanup(t *testing.T) { c, err := NewClient( chainID, @@ -357,10 +316,9 @@ func TestClient_Cleanup(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() + _, err = c.TrustedHeader(1) require.NoError(t, err) - c.Stop() err = c.Cleanup() require.NoError(t, err) @@ -387,9 +345,6 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() h, err := c.TrustedHeader(1) assert.NoError(t, err) @@ -429,14 +384,12 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() h, err := c.TrustedHeader(1) assert.NoError(t, err) - assert.NotNil(t, h) - assert.Equal(t, h.Hash(), header1.Hash()) + if assert.NotNil(t, h) { + assert.Equal(t, h.Hash(), header1.Hash()) + } } } diff --git a/lite2/store/db/db.go b/lite2/store/db/db.go index baae0d040..d405b9865 100644 --- a/lite2/store/db/db.go +++ b/lite2/store/db/db.go @@ -1,9 +1,11 @@ package db import ( + "encoding/binary" "fmt" "regexp" "strconv" + "sync" "github.com/pkg/errors" "github.com/tendermint/go-amino" @@ -14,10 +16,17 @@ import ( "github.com/tendermint/tendermint/types" ) +var ( + sizeKey = []byte("size") +) + type dbs struct { db dbm.DB prefix string + mtx sync.RWMutex + size uint16 + cdc *amino.Codec } @@ -28,11 +37,20 @@ type dbs struct { func New(db dbm.DB, prefix string) store.Store { cdc := amino.NewCodec() cryptoAmino.RegisterAmino(cdc) - return &dbs{db: db, prefix: prefix, cdc: cdc} + + size := uint16(0) + bz, err := db.Get(sizeKey) + if err == nil && len(bz) > 0 { + size = unmarshalSize(bz) + } + + return &dbs{db: db, prefix: prefix, cdc: cdc, size: size} } // SaveSignedHeaderAndValidatorSet persists SignedHeader and ValidatorSet to // the db. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *types.ValidatorSet) error { if sh.Height <= 0 { panic("negative or zero height") @@ -48,30 +66,54 @@ func (s *dbs) SaveSignedHeaderAndValidatorSet(sh *types.SignedHeader, valSet *ty return errors.Wrap(err, "marshalling validator set") } + s.mtx.Lock() + defer s.mtx.Unlock() + b := s.db.NewBatch() b.Set(s.shKey(sh.Height), shBz) b.Set(s.vsKey(sh.Height), valSetBz) + b.Set(sizeKey, marshalSize(s.size+1)) + err = b.WriteSync() b.Close() + + if err == nil { + s.size++ + } + return err } // DeleteSignedHeaderAndValidatorSet deletes SignedHeader and ValidatorSet from // the db. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) DeleteSignedHeaderAndValidatorSet(height int64) error { if height <= 0 { panic("negative or zero height") } + s.mtx.Lock() + defer s.mtx.Unlock() + b := s.db.NewBatch() b.Delete(s.shKey(height)) b.Delete(s.vsKey(height)) + b.Set(sizeKey, marshalSize(s.size-1)) + err := b.WriteSync() b.Close() + + if err == nil { + s.size-- + } + return err } // SignedHeader loads SignedHeader at the given height. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) { if height <= 0 { panic("negative or zero height") @@ -91,6 +133,8 @@ func (s *dbs) SignedHeader(height int64) (*types.SignedHeader, error) { } // ValidatorSet loads ValidatorSet at the given height. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) { if height <= 0 { panic("negative or zero height") @@ -110,6 +154,8 @@ func (s *dbs) ValidatorSet(height int64) (*types.ValidatorSet, error) { } // LastSignedHeaderHeight returns the last SignedHeader height stored. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) LastSignedHeaderHeight() (int64, error) { itr, err := s.db.ReverseIterator( s.shKey(1), @@ -133,6 +179,8 @@ func (s *dbs) LastSignedHeaderHeight() (int64, error) { } // FirstSignedHeaderHeight returns the first SignedHeader height stored. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) FirstSignedHeaderHeight() (int64, error) { itr, err := s.db.Iterator( s.shKey(1), @@ -155,12 +203,16 @@ func (s *dbs) FirstSignedHeaderHeight() (int64, error) { return -1, nil } +// SignedHeaderAfter iterates over headers until it finds a header after one at +// height. It returns ErrSignedHeaderNotFound if no such header exists. +// +// Safe for concurrent use by multiple goroutines. func (s *dbs) SignedHeaderAfter(height int64) (*types.SignedHeader, error) { if height <= 0 { panic("negative or zero height") } - itr, err := s.db.ReverseIterator( + itr, err := s.db.Iterator( s.shKey(height+1), append(s.shKey(1<<63-1), byte(0x00)), ) @@ -178,7 +230,76 @@ func (s *dbs) SignedHeaderAfter(height int64) (*types.SignedHeader, error) { itr.Next() } - panic(fmt.Sprintf("no header after height %d. make sure height is not greater than latest existing height", height)) + return nil, store.ErrSignedHeaderNotFound +} + +// Prune prunes header & validator set pairs until there are only size pairs +// left. +// +// Safe for concurrent use by multiple goroutines. +func (s *dbs) Prune(size uint16) error { + // 1) Check how many we need to prune. + s.mtx.RLock() + sSize := s.size + s.mtx.RUnlock() + + if sSize <= size { // nothing to prune + return nil + } + numToPrune := sSize - size + + // 2) Iterate over headers and perform a batch operation. + itr, err := s.db.Iterator( + s.shKey(1), + append(s.shKey(1<<63-1), byte(0x00)), + ) + if err != nil { + panic(err) + } + + b := s.db.NewBatch() + + pruned := 0 + for itr.Valid() && numToPrune > 0 { + key := itr.Key() + _, height, ok := parseShKey(key) + if ok { + b.Delete(s.shKey(height)) + b.Delete(s.vsKey(height)) + } + itr.Next() + numToPrune-- + pruned++ + } + + itr.Close() + + err = b.WriteSync() + b.Close() + if err != nil { + return err + } + + // 3) Update size. + s.mtx.Lock() + defer s.mtx.Unlock() + + s.size -= uint16(pruned) + + if wErr := s.db.SetSync(sizeKey, marshalSize(s.size)); wErr != nil { + return errors.Wrap(wErr, "failed to persist size") + } + + return nil +} + +// Size returns the number of header & validator set pairs. +// +// Safe for concurrent use by multiple goroutines. +func (s *dbs) Size() uint16 { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.size } func (s *dbs) shKey(height int64) []byte { @@ -214,3 +335,13 @@ func parseShKey(key []byte) (prefix string, height int64, ok bool) { } return } + +func marshalSize(size uint16) []byte { + bs := make([]byte, 2) + binary.LittleEndian.PutUint16(bs, size) + return bs +} + +func unmarshalSize(bz []byte) uint16 { + return binary.LittleEndian.Uint16(bz) +} diff --git a/lite2/store/db/db_test.go b/lite2/store/db/db_test.go index 8e317d29b..2b82de8f3 100644 --- a/lite2/store/db/db_test.go +++ b/lite2/store/db/db_test.go @@ -1,6 +1,7 @@ package db import ( + "sync" "testing" "github.com/stretchr/testify/assert" @@ -93,3 +94,69 @@ func Test_SignedHeaderAfter(t *testing.T) { assert.EqualValues(t, 2, h.Height) } } + +func Test_Prune(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "Test_Prune") + + // Empty store + assert.EqualValues(t, 0, dbStore.Size()) + err := dbStore.Prune(0) + require.NoError(t, err) + + // One header + err = dbStore.SaveSignedHeaderAndValidatorSet( + &types.SignedHeader{Header: &types.Header{Height: 2}}, &types.ValidatorSet{}) + require.NoError(t, err) + + assert.EqualValues(t, 1, dbStore.Size()) + + err = dbStore.Prune(1) + require.NoError(t, err) + assert.EqualValues(t, 1, dbStore.Size()) + + err = dbStore.Prune(0) + require.NoError(t, err) + assert.EqualValues(t, 0, dbStore.Size()) + + // Multiple headers + for i := 1; i <= 10; i++ { + err = dbStore.SaveSignedHeaderAndValidatorSet( + &types.SignedHeader{Header: &types.Header{Height: int64(i)}}, &types.ValidatorSet{}) + require.NoError(t, err) + } + + err = dbStore.Prune(11) + require.NoError(t, err) + assert.EqualValues(t, 10, dbStore.Size()) + + err = dbStore.Prune(7) + require.NoError(t, err) + assert.EqualValues(t, 7, dbStore.Size()) +} + +func Test_Concurrency(t *testing.T) { + dbStore := New(dbm.NewMemDB(), "Test_Prune") + + var wg sync.WaitGroup + for i := 1; i <= 100; i++ { + wg.Add(1) + go func(i int64) { + defer wg.Done() + + dbStore.SaveSignedHeaderAndValidatorSet( + &types.SignedHeader{Header: &types.Header{Height: i}}, &types.ValidatorSet{}) + + dbStore.SignedHeader(i) + dbStore.ValidatorSet(i) + dbStore.LastSignedHeaderHeight() + dbStore.FirstSignedHeaderHeight() + + dbStore.Prune(2) + _ = dbStore.Size() + + dbStore.DeleteSignedHeaderAndValidatorSet(1) + }(int64(i)) + } + + wg.Wait() +} diff --git a/lite2/store/store.go b/lite2/store/store.go index 65cd82731..7ea6b9c6b 100644 --- a/lite2/store/store.go +++ b/lite2/store/store.go @@ -45,4 +45,11 @@ type Store interface { // // height must be > 0 && <= LastSignedHeaderHeight. SignedHeaderAfter(height int64) (*types.SignedHeader, error) + + // Prune removes headers & the associated validator sets when Store reaches a + // defined size (number of header & validator set pairs). + Prune(size uint16) error + + // Size returns a number of currently existing header & validator set pairs. + Size() uint16 } From b5f6bfa4f97e6f027f45938bb38238ff820f9543 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 27 Feb 2020 16:10:01 +0100 Subject: [PATCH 37/74] lite2: return height as 2nd return param in TrustedValidatorSet (#4479) Closes #4473 --- .../adr-046-light-client-implementation.md | 2 +- lite2/client.go | 17 +-- lite2/client_test.go | 101 ++++++++++++++++-- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index dbae7e641..dca1c6692 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -28,7 +28,7 @@ type Client interface { // get trusted headers & validators TrustedHeader(height int64) (*types.SignedHeader, error) - TrustedValidatorSet(height int64) (*types.ValidatorSet, error) + TrustedValidatorSet(height int64) (valSet *types.ValidatorSet, heightUsed int64, err error) LastTrustedHeight() (int64, error) FirstTrustedHeight() (int64, error) diff --git a/lite2/client.go b/lite2/client.go index c52ffe832..b70123099 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -433,7 +433,8 @@ func (c *Client) TrustedHeader(height int64) (*types.SignedHeader, error) { } // TrustedValidatorSet returns a trusted validator set at the given height (0 - -// latest). +// latest). The second return parameter is the height used (useful if 0 was +// passed; otherwise can be ignored). // // height must be >= 0. // @@ -448,18 +449,22 @@ func (c *Client) TrustedHeader(height int64) (*types.SignedHeader, error) { // - header signed by that validator set has not been verified yet // // Safe for concurrent use by multiple goroutines. -func (c *Client) TrustedValidatorSet(height int64) (*types.ValidatorSet, error) { - height, err := c.compareWithLatestHeight(height) +func (c *Client) TrustedValidatorSet(height int64) (valSet *types.ValidatorSet, heightUsed int64, err error) { + heightUsed, err = c.compareWithLatestHeight(height) if err != nil { - return nil, err + return nil, heightUsed, err } - return c.trustedStore.ValidatorSet(height) + valSet, err = c.trustedStore.ValidatorSet(heightUsed) + if err != nil { + return nil, heightUsed, err + } + return valSet, heightUsed, err } func (c *Client) compareWithLatestHeight(height int64) (int64, error) { latestHeight, err := c.LastTrustedHeight() if err != nil { - return 0, err + return 0, errors.Wrap(err, "can't get last trusted height") } if latestHeight == -1 { return 0, errors.New("no headers exist") diff --git a/lite2/client_test.go b/lite2/client_test.go index ad9088e8c..b1079390a 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -47,7 +47,9 @@ var ( } headerSet = map[int64]*types.SignedHeader{ 1: h1, + // interim header (3/3 signed) 2: h2, + // last header (3/3 signed) 3: h3, } fullNode = mockp.New( @@ -71,14 +73,7 @@ func TestClient_SequentialVerification(t *testing.T) { }{ { "good", - map[int64]*types.SignedHeader{ - // trusted header - 1: h1, - // interim header (3/3 signed) - 2: h2, - // last header (3/3 signed) - 3: h3, - }, + headerSet, valSet, false, false, @@ -322,10 +317,14 @@ func TestClient_Cleanup(t *testing.T) { err = c.Cleanup() require.NoError(t, err) - // Check no headers exist after Cleanup. + // Check no headers/valsets exist after Cleanup. h, err := c.TrustedHeader(1) assert.Error(t, err) assert.Nil(t, h) + + valSet, _, err := c.TrustedValidatorSet(1) + assert.Error(t, err) + assert.Nil(t, valSet) } // trustedHeader.Height == options.Height @@ -350,6 +349,13 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) + + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } } // 2. options.Hash != trustedHeader.Hash @@ -390,6 +396,13 @@ func TestClientRestoresTrustedHeaderAfterStartup1(t *testing.T) { if assert.NotNil(t, h) { assert.Equal(t, h.Hash(), header1.Hash()) } + + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } } } @@ -423,6 +436,13 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) + + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } } // 2. options.Hash != trustedHeader.Hash @@ -469,6 +489,10 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { h, err := c.TrustedHeader(1) assert.Error(t, err) assert.Nil(t, h) + + valSet, _, err := c.TrustedValidatorSet(1) + assert.Error(t, err) + assert.Nil(t, valSet) } } @@ -504,10 +528,21 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { assert.NotNil(t, h) assert.Equal(t, h.Hash(), h1.Hash()) + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } + // Check we no longer have 2nd header (+header2+). h, err = c.TrustedHeader(2) assert.Error(t, err) assert.Nil(t, h) + + valSet, _, err = c.TrustedValidatorSet(2) + assert.Error(t, err) + assert.Nil(t, valSet) } // 2. options.Hash != trustedHeader.Hash @@ -557,10 +592,21 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { assert.NotNil(t, h) assert.Equal(t, h.Hash(), header1.Hash()) + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } + // Check we no longer have invalid 2nd header (+header2+). h, err = c.TrustedHeader(2) assert.Error(t, err) assert.Nil(t, h) + + valSet, _, err = c.TrustedValidatorSet(2) + assert.Error(t, err) + assert.Nil(t, valSet) } } @@ -586,6 +632,12 @@ func TestClient_Update(t *testing.T) { assert.NoError(t, err) require.NotNil(t, h) assert.EqualValues(t, 3, h.Height) + + valSet, _, err := c.TrustedValidatorSet(3) + assert.NoError(t, err) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } } func TestClient_Concurrency(t *testing.T) { @@ -627,7 +679,7 @@ func TestClient_Concurrency(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, h) - vals, err := c.TrustedValidatorSet(2) + vals, _, err := c.TrustedValidatorSet(2) assert.NoError(t, err) assert.NotNil(t, vals) }() @@ -784,6 +836,13 @@ func TestClient_NewClientFromTrustedStore(t *testing.T) { h, err := c.TrustedHeader(1) assert.NoError(t, err) assert.EqualValues(t, 1, h.Height) + + valSet, _, err := c.TrustedValidatorSet(1) + assert.NoError(t, err) + assert.NotNil(t, valSet) + if assert.NotNil(t, valSet) { + assert.Equal(t, h.ValidatorsHash.Bytes(), valSet.Hash()) + } } func TestNewClientErrorsIfAllWitnessesUnavailable(t *testing.T) { @@ -857,3 +916,25 @@ func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { assert.Error(t, err) assert.EqualValues(t, 0, len(c.Witnesses())) } + +func TestClientTrustedValidatorSet(t *testing.T) { + c, err := NewClient( + chainID, + trustOptions, + fullNode, + []provider.Provider{fullNode}, + dbs.New(dbm.NewMemDB(), chainID), + UpdatePeriod(0), + Logger(log.TestingLogger()), + ) + + require.NoError(t, err) + + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour).Add(1*time.Second)) + require.NoError(t, err) + + valSet, height, err := c.TrustedValidatorSet(0) + assert.NoError(t, err) + assert.NotNil(t, valSet) + assert.EqualValues(t, 2, height) +} From 3f883bb80afdf9b758cf74929156e9b27c4e2512 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 27 Feb 2020 17:26:28 +0100 Subject: [PATCH 38/74] lite2: actually run example tests + clock drift (#4487) Closes: #4488 ## Commits: * run example tests * introduce max clock drift clockDrift variable from the spec. 10s should cover most of the clients. References: - http://vancouver-webpages.com/time/web.html - https://blog.codinghorror.com/keeping-time-on-the-pc/ * fix ExampleClient_Update * add test * increase clock drift * fix linter warning --- lite2/example_test.go | 36 +++++++++++++++++++++--------------- lite2/verifier.go | 11 ++++++++--- lite2/verifier_test.go | 23 +++++++++++++++++------ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/lite2/example_test.go b/lite2/example_test.go index c8d883958..65997f906 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -11,7 +11,6 @@ import ( dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/abci/example/kvstore" - "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/lite2/provider" httpp "github.com/tendermint/tendermint/lite2/provider/http" dbs "github.com/tendermint/tendermint/lite2/store/db" @@ -19,7 +18,7 @@ import ( ) // Automatically getting new headers and verifying them. -func TestExample_Client_AutoUpdate(t *testing.T) { +func ExampleClient_Update() { // give Tendermint time to generate some blocks time.Sleep(5 * time.Second) @@ -57,10 +56,10 @@ func TestExample_Client_AutoUpdate(t *testing.T) { Hash: header.Hash(), }, primary, - []provider.Provider{primary}, // TODO: primary should not be used here + []provider.Provider{primary}, // NOTE: primary should not be used here dbs.New(db, chainID), - UpdatePeriod(1*time.Second), - Logger(log.TestingLogger()), + UpdatePeriod(0), // NOTE: value should be greater than zero + // Logger(log.TestingLogger()), ) if err != nil { stdlog.Fatal(err) @@ -76,17 +75,29 @@ func TestExample_Client_AutoUpdate(t *testing.T) { time.Sleep(2 * time.Second) + // XXX: 30 * time.Minute clock drift is needed because a) Tendermint strips + // monotonic component (see types/time/time.go) b) single instance is being + // run. + err = c.Update(time.Now().Add(30 * time.Minute)) + if err != nil { + stdlog.Fatal(err) + } + h, err := c.TrustedHeader(0) if err != nil { stdlog.Fatal(err) } - fmt.Println("got header", h.Height) - // Output: got header 3 + if h.Height > 2 { + fmt.Println("successful update") + } else { + fmt.Println("update failed") + } + // Output: successful update } // Manually getting headers and verifying them. -func TestExample_Client_ManualUpdate(t *testing.T) { +func ExampleClient_VerifyHeaderAtHeight() { // give Tendermint time to generate some blocks time.Sleep(5 * time.Second) @@ -127,17 +138,12 @@ func TestExample_Client_ManualUpdate(t *testing.T) { []provider.Provider{primary}, // TODO: primary should not be used here dbs.New(db, chainID), UpdatePeriod(0), - Logger(log.TestingLogger()), + // Logger(log.TestingLogger()), ) if err != nil { stdlog.Fatal(err) } - err = c.Start() - if err != nil { - stdlog.Fatal(err) - } defer func() { - c.Stop() c.Cleanup() }() @@ -158,7 +164,7 @@ func TestExample_Client_ManualUpdate(t *testing.T) { func TestMain(m *testing.M) { // start a tendermint node (and kvstore) in the background to test against app := kvstore.NewApplication() - node := rpctest.StartTendermint(app) + node := rpctest.StartTendermint(app, rpctest.SuppressStdout) code := m.Run() diff --git a/lite2/verifier.go b/lite2/verifier.go index c0732a69c..250c8b204 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -10,6 +10,10 @@ import ( "github.com/tendermint/tendermint/types" ) +const ( + maxClockDrift = 10 * time.Second +) + var ( // DefaultTrustLevel - new header can be trusted if at least one correct // validator signed it. @@ -162,10 +166,11 @@ func verifyNewHeaderAndVals( trustedHeader.Time) } - if !untrustedHeader.Time.Before(now) { - return errors.Errorf("new header has a time from the future %v (now: %v)", + if !untrustedHeader.Time.Before(now.Add(maxClockDrift)) { + return errors.Errorf("new header has a time from the future %v (now: %v; max clock drift: %v)", untrustedHeader.Time, - now) + now, + maxClockDrift) } if !bytes.Equal(untrustedHeader.ValidatorsHash, untrustedVals.Hash()) { diff --git a/lite2/verifier_test.go b/lite2/verifier_test.go index 6e118d7ae..241d33b05 100644 --- a/lite2/verifier_test.go +++ b/lite2/verifier_test.go @@ -75,8 +75,19 @@ func TestVerifyAdjacentHeaders(t *testing.T) { nil, "new header has a time from the future", }, - // 3/3 signed -> no error + // new header's time is from the future, but it's acceptable (< maxClockDrift) -> no error 4: { + keys.GenSignedHeader(chainID, nextHeight, + bTime.Add(2*time.Hour).Add(maxClockDrift).Add(-1*time.Millisecond), nil, vals, vals, + []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), + vals, + 3 * time.Hour, + bTime.Add(2 * time.Hour), + nil, + "", + }, + // 3/3 signed -> no error + 5: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), vals, @@ -86,7 +97,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { "", }, // 2/3 signed -> no error - 5: { + 6: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 1, len(keys)), vals, @@ -96,7 +107,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { "", }, // 1/3 signed -> error - 6: { + 7: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), len(keys)-1, len(keys)), vals, @@ -106,7 +117,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { "", }, // vals does not match with what we have -> error - 7: { + 8: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, keys.ToValidators(10, 1), vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), @@ -116,7 +127,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { "to match those from new header", }, // vals are inconsistent with newHeader -> error - 8: { + 9: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), @@ -126,7 +137,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { "to match those that were supplied", }, // old header has expired -> error - 9: { + 10: { keys.GenSignedHeader(chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, []byte("app_hash"), []byte("cons_hash"), []byte("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), From 78144306dd98157cffc17d896cdd01ea08d4d131 Mon Sep 17 00:00:00 2001 From: Shivani Joshi <46731446+Shivani912@users.noreply.github.com> Date: Fri, 28 Feb 2020 03:57:00 -0500 Subject: [PATCH 39/74] JSON tests related changes (#4461) * test functions take time.Now and other minor changes * updated remaining test files * Update validation_test.go * fix typo * go fmt * import time Co-authored-by: Marko --- blockchain/v0/reactor_test.go | 4 +++- blockchain/v2/reactor_test.go | 5 ++++- consensus/byzantine_test.go | 2 +- consensus/replay_test.go | 3 ++- libs/math/fraction.go | 4 ++-- state/helpers_test.go | 2 +- state/validation_test.go | 9 ++++++++- types/block.go | 1 - types/block_test.go | 20 ++++++++++---------- types/priv_validator.go | 31 ++++++++++++++++--------------- types/test_util.go | 9 +++++---- 11 files changed, 52 insertions(+), 38 deletions(-) diff --git a/blockchain/v0/reactor_test.go b/blockchain/v0/reactor_test.go index 08ff10a93..5a97d3aed 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -97,7 +97,9 @@ func newBlockchainReactor( lastBlockMeta.BlockID, state.Validators, privVals[0], - lastBlock.Header.ChainID) + lastBlock.Header.ChainID, + time.Now(), + ) if err != nil { panic(err) } diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go index c187f954b..ad091f582 100644 --- a/blockchain/v2/reactor_test.go +++ b/blockchain/v2/reactor_test.go @@ -6,6 +6,7 @@ import ( "sort" "sync" "testing" + "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -494,7 +495,9 @@ func newReactorStore( lastBlockMeta.BlockID, state.Validators, privVals[0], - lastBlock.Header.ChainID) + lastBlock.Header.ChainID, + time.Now(), + ) if err != nil { panic(err) } diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index 65bd88f67..bbf9cbf8f 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -54,7 +54,7 @@ func TestByzantine(t *testing.T) { if i == 0 { // NOTE: Now, test validators are MockPV, which by default doesn't // do any safety checks. - css[i].privValidator.(*types.MockPV).DisableChecks() + css[i].privValidator.(types.MockPV).DisableChecks() css[i].decideProposal = func(j int) func(int64, int) { return func(height int64, round int) { byzantineDecideProposalFunc(t, height, round, css[j], switches[j]) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 6a33cf382..aca6a9f10 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -892,7 +892,8 @@ func makeBlock(state sm.State, lastBlock *types.Block, lastBlockMeta *types.Bloc lastBlockMeta.BlockID, state.Validators, privVal, - lastBlock.Header.ChainID) + lastBlock.Header.ChainID, + time.Now()) lastCommit = types.NewCommit(vote.Height, vote.Round, lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()}) } diff --git a/libs/math/fraction.go b/libs/math/fraction.go index dca8a8e79..6cc2ad23e 100644 --- a/libs/math/fraction.go +++ b/libs/math/fraction.go @@ -6,10 +6,10 @@ import "fmt" // format. type Fraction struct { // The portion of the denominator in the faction, e.g. 2 in 2/3. - Numerator int64 + Numerator int64 `json:"numerator"` // The value by which the numerator is divided, e.g. 3 in 2/3. Must be // positive. - Denominator int64 + Denominator int64 `json:"denominator"` } func (fr Fraction) String() string { diff --git a/state/helpers_test.go b/state/helpers_test.go index fec965016..f8758f987 100644 --- a/state/helpers_test.go +++ b/state/helpers_test.go @@ -82,7 +82,7 @@ func makeValidCommit( sigs := make([]types.CommitSig, 0) for i := 0; i < vals.Size(); i++ { _, val := vals.GetByIndex(i) - vote, err := types.MakeVote(height, blockID, vals, privVals[val.Address.String()], chainID) + vote, err := types.MakeVote(height, blockID, vals, privVals[val.Address.String()], chainID, time.Now()) if err != nil { return nil, err } diff --git a/state/validation_test.go b/state/validation_test.go index 9346a3bdd..da975351c 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -117,6 +117,7 @@ func TestValidateBlockCommit(t *testing.T) { state.Validators, privVals[proposerAddr.String()], chainID, + time.Now(), ) require.NoError(t, err, "height %d", height) wrongHeightCommit := types.NewCommit( @@ -162,7 +163,13 @@ func TestValidateBlockCommit(t *testing.T) { /* wrongSigsCommit is fine except for the extra bad precommit */ - goodVote, err := types.MakeVote(height, blockID, state.Validators, privVals[proposerAddr.String()], chainID) + goodVote, err := types.MakeVote(height, + blockID, + state.Validators, + privVals[proposerAddr.String()], + chainID, + time.Now(), + ) require.NoError(t, err, "height %d", height) badVote := &types.Vote{ ValidatorAddress: badPrivVal.GetPubKey().Address(), diff --git a/types/block.go b/types/block.go index 4f61a2bc9..61ae6719b 100644 --- a/types/block.go +++ b/types/block.go @@ -752,7 +752,6 @@ type SignedHeader struct { // ValidateBasic does basic consistency checks and makes sure the header // and commit are consistent. -// // NOTE: This does not actually check the cryptographic signatures. Make // sure to use a Verifier to validate the signatures actually provide a // significantly strong proof for this header's validity. diff --git a/types/block_test.go b/types/block_test.go index 5f2f2e8bc..a7ae31e36 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -37,7 +37,7 @@ func TestBlockAddEvidence(t *testing.T) { h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), 0, valSet.Validators[0].Address) @@ -57,7 +57,7 @@ func TestBlockValidateBasic(t *testing.T) { h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), 0, valSet.Validators[0].Address) @@ -120,7 +120,7 @@ func TestBlockMakePartSetWithEvidence(t *testing.T) { h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), 0, valSet.Validators[0].Address) @@ -137,7 +137,7 @@ func TestBlockHashesTo(t *testing.T) { lastID := makeBlockIDRandom() h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) ev := NewMockEvidence(h, time.Now(), 0, valSet.Validators[0].Address) @@ -210,7 +210,7 @@ func TestCommit(t *testing.T) { lastID := makeBlockIDRandom() h := int64(3) voteSet, _, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) require.NoError(t, err) assert.Equal(t, h-1, commit.Height) @@ -241,7 +241,7 @@ func TestCommitValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { - com := randCommit() + com := randCommit(time.Now()) tc.malleateCommit(com) assert.Equal(t, tc.expectErr, com.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) @@ -348,11 +348,11 @@ func TestMaxHeaderBytes(t *testing.T) { assert.EqualValues(t, MaxHeaderBytes, int64(len(bz))) } -func randCommit() *Commit { +func randCommit(now time.Time) *Commit { lastID := makeBlockIDRandom() h := int64(3) voteSet, _, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, now) if err != nil { panic(err) } @@ -431,7 +431,7 @@ func TestCommitToVoteSet(t *testing.T) { h := int64(3) voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) - commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now()) assert.NoError(t, err) chainID := voteSet.ChainID() @@ -506,7 +506,7 @@ func TestCommitToVoteSetWithVotesForNilBlock(t *testing.T) { } func TestSignedHeaderValidateBasic(t *testing.T) { - commit := randCommit() + commit := randCommit(time.Now()) chainID := "𠜎" timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) h := Header{ diff --git a/types/priv_validator.go b/types/priv_validator.go index bd35973ca..f2af88750 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -44,35 +44,35 @@ func (pvs PrivValidatorsByAddress) Swap(i, j int) { // MockPV implements PrivValidator without any safety or persistence. // Only use it for testing. type MockPV struct { - privKey crypto.PrivKey + PrivKey crypto.PrivKey breakProposalSigning bool breakVoteSigning bool } -func NewMockPV() *MockPV { - return &MockPV{ed25519.GenPrivKey(), false, false} +func NewMockPV() MockPV { + return MockPV{ed25519.GenPrivKey(), false, false} } // NewMockPVWithParams allows one to create a MockPV instance, but with finer // grained control over the operation of the mock validator. This is useful for // mocking test failures. -func NewMockPVWithParams(privKey crypto.PrivKey, breakProposalSigning, breakVoteSigning bool) *MockPV { - return &MockPV{privKey, breakProposalSigning, breakVoteSigning} +func NewMockPVWithParams(privKey crypto.PrivKey, breakProposalSigning, breakVoteSigning bool) MockPV { + return MockPV{privKey, breakProposalSigning, breakVoteSigning} } // Implements PrivValidator. -func (pv *MockPV) GetPubKey() crypto.PubKey { - return pv.privKey.PubKey() +func (pv MockPV) GetPubKey() crypto.PubKey { + return pv.PrivKey.PubKey() } // Implements PrivValidator. -func (pv *MockPV) SignVote(chainID string, vote *Vote) error { +func (pv MockPV) SignVote(chainID string, vote *Vote) error { useChainID := chainID if pv.breakVoteSigning { useChainID = "incorrect-chain-id" } signBytes := vote.SignBytes(useChainID) - sig, err := pv.privKey.Sign(signBytes) + sig, err := pv.PrivKey.Sign(signBytes) if err != nil { return err } @@ -81,13 +81,13 @@ func (pv *MockPV) SignVote(chainID string, vote *Vote) error { } // Implements PrivValidator. -func (pv *MockPV) SignProposal(chainID string, proposal *Proposal) error { +func (pv MockPV) SignProposal(chainID string, proposal *Proposal) error { useChainID := chainID if pv.breakProposalSigning { useChainID = "incorrect-chain-id" } signBytes := proposal.SignBytes(useChainID) - sig, err := pv.privKey.Sign(signBytes) + sig, err := pv.PrivKey.Sign(signBytes) if err != nil { return err } @@ -96,19 +96,19 @@ func (pv *MockPV) SignProposal(chainID string, proposal *Proposal) error { } // String returns a string representation of the MockPV. -func (pv *MockPV) String() string { +func (pv MockPV) String() string { addr := pv.GetPubKey().Address() return fmt.Sprintf("MockPV{%v}", addr) } // XXX: Implement. -func (pv *MockPV) DisableChecks() { +func (pv MockPV) DisableChecks() { // Currently this does nothing, // as MockPV has no safety checks at all. } type ErroringMockPV struct { - *MockPV + MockPV } var ErroringMockPVErr = errors.New("erroringMockPV always returns an error") @@ -124,6 +124,7 @@ func (pv *ErroringMockPV) SignProposal(chainID string, proposal *Proposal) error } // NewErroringMockPV returns a MockPV that fails on each signing request. Again, for testing only. + func NewErroringMockPV() *ErroringMockPV { - return &ErroringMockPV{&MockPV{ed25519.GenPrivKey(), false, false}} + return &ErroringMockPV{MockPV{ed25519.GenPrivKey(), false, false}} } diff --git a/types/test_util.go b/types/test_util.go index 3090f5f8b..48913f308 100644 --- a/types/test_util.go +++ b/types/test_util.go @@ -1,11 +1,11 @@ package types import ( - tmtime "github.com/tendermint/tendermint/types/time" + "time" ) func MakeCommit(blockID BlockID, height int64, round int, - voteSet *VoteSet, validators []PrivValidator) (*Commit, error) { + voteSet *VoteSet, validators []PrivValidator, now time.Time) (*Commit, error) { // all sign for i := 0; i < len(validators); i++ { @@ -17,7 +17,7 @@ func MakeCommit(blockID BlockID, height int64, round int, Round: round, Type: PrecommitType, BlockID: blockID, - Timestamp: tmtime.Now(), + Timestamp: now, } _, err := signAddVote(validators[i], vote, voteSet) @@ -43,6 +43,7 @@ func MakeVote( valSet *ValidatorSet, privVal PrivValidator, chainID string, + now time.Time, ) (*Vote, error) { addr := privVal.GetPubKey().Address() idx, _ := valSet.GetByAddress(addr) @@ -51,7 +52,7 @@ func MakeVote( ValidatorIndex: idx, Height: height, Round: 0, - Timestamp: tmtime.Now(), + Timestamp: now, Type: PrecommitType, BlockID: blockID, } From b73c0b75d51331b584168e31695d53716263ff93 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2020 10:24:25 +0100 Subject: [PATCH 40/74] build(deps): bump github.com/golang/protobuf from 1.3.3 to 1.3.4 (#4485) Bumps [github.com/golang/protobuf](https://github.com/golang/protobuf) from 1.3.3 to 1.3.4. - [Release notes](https://github.com/golang/protobuf/releases) - [Commits](https://github.com/golang/protobuf/compare/v1.3.3...v1.3.4) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a5faa59b1..dd2b444fc 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/go-kit/kit v0.10.0 github.com/go-logfmt/logfmt v0.5.0 github.com/gogo/protobuf v1.3.1 - github.com/golang/protobuf v1.3.3 + github.com/golang/protobuf v1.3.4 github.com/gorilla/websocket v1.4.1 github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f github.com/libp2p/go-buffer-pool v0.0.2 diff --git a/go.sum b/go.sum index 0e05ecad9..c8eb10010 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,8 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= From c837a57ddd5f1c05dcc822f3555e9a6202f05d8e Mon Sep 17 00:00:00 2001 From: Gregory Terzian <2792687+gterzian@users.noreply.github.com> Date: Fri, 28 Feb 2020 18:38:28 +0800 Subject: [PATCH 41/74] rpc: stop txSearch result processing if context is done (#4418) --- rpc/client/httpclient.go | 10 ++++ rpc/client/rpc_test.go | 22 ++++++++ rpc/core/tx.go | 2 +- state/txindex/indexer.go | 3 +- state/txindex/kv/kv.go | 66 ++++++++++++++++++++++-- state/txindex/kv/kv_bench_test.go | 5 +- state/txindex/kv/kv_test.go | 84 +++++++++++++++++++++++++++++-- state/txindex/null/null.go | 3 +- 8 files changed, 182 insertions(+), 13 deletions(-) diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index c844113c3..98875c91e 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -96,6 +96,16 @@ func NewHTTP(remote, wsEndpoint string) (*HTTP, error) { return NewHTTPWithClient(remote, wsEndpoint, httpClient) } +// Create timeout enabled http client +func NewHTTPWithTimeout(remote, wsEndpoint string, timeout uint) (*HTTP, error) { + httpClient, err := rpcclient.DefaultHTTPClient(remote) + if err != nil { + return nil, err + } + httpClient.Timeout = time.Duration(timeout) * time.Second + return NewHTTPWithClient(remote, wsEndpoint, httpClient) +} + // NewHTTPWithClient allows for setting a custom http client (See NewHTTP). // An error is returned on invalid remote. The function panics when remote is nil. func NewHTTPWithClient(remote, wsEndpoint string, client *http.Client) (*HTTP, error) { diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 90612d5e0..9196bead0 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -39,6 +39,16 @@ func getHTTPClient() *client.HTTP { return c } +func getHTTPClientWithTimeout(timeout uint) *client.HTTP { + rpcAddr := rpctest.GetConfig().RPC.ListenAddress + c, err := client.NewHTTPWithTimeout(rpcAddr, "/websocket", timeout) + if err != nil { + panic(err) + } + c.SetLogger(log.TestingLogger()) + return c +} + func getLocalClient() *client.Local { return client.NewLocal(node) } @@ -414,6 +424,18 @@ func TestTx(t *testing.T) { } } +func TestTxSearchWithTimeout(t *testing.T) { + // Get a client with a time-out of 10 secs. + timeoutClient := getHTTPClientWithTimeout(10) + + // query using a compositeKey (see kvstore application) + result, err := timeoutClient.TxSearch("app.creator='Cosmoshi Netowoko'", false, 1, 30, "asc") + require.Nil(t, err) + if len(result.Txs) == 0 { + t.Fatal("expected a lot of transactions") + } +} + func TestTxSearch(t *testing.T) { c := getHTTPClient() diff --git a/rpc/core/tx.go b/rpc/core/tx.go index f894424b7..e7e2582f6 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -67,7 +67,7 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int return nil, err } - results, err := txIndexer.Search(q) + results, err := txIndexer.Search(ctx.Context(), q) if err != nil { return nil, err } diff --git a/state/txindex/indexer.go b/state/txindex/indexer.go index c66f4322c..5f7ee7544 100644 --- a/state/txindex/indexer.go +++ b/state/txindex/indexer.go @@ -1,6 +1,7 @@ package txindex import ( + "context" "errors" "github.com/tendermint/tendermint/libs/pubsub/query" @@ -21,7 +22,7 @@ type TxIndexer interface { Get(hash []byte) (*types.TxResult, error) // Search allows you to query for transactions. - Search(q *query.Query) ([]*types.TxResult, error) + Search(ctx context.Context, q *query.Query) ([]*types.TxResult, error) } //---------------------------------------------------- diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 5fcd5ab73..c5ba99ca3 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -2,6 +2,7 @@ package kv import ( "bytes" + "context" "encoding/hex" "fmt" "strconv" @@ -167,7 +168,18 @@ func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.S // better for the client to provide both lower and upper bounds, so we are not // performing a full scan. Results from querying indexes are then intersected // and returned to the caller, in no particular order. -func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { +// +// Search will exit early and return any result fetched so far, +// when a message is received on the context chan. +func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*types.TxResult, error) { + // Potentially exit early. + select { + case <-ctx.Done(): + results := make([]*types.TxResult, 0) + return results, nil + default: + } + var hashesInitialized bool filteredHashes := make(map[string][]byte) @@ -205,7 +217,7 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { for _, r := range ranges { if !hashesInitialized { - filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, true) + filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, true) hashesInitialized = true // Ignore any remaining conditions if the first condition resulted @@ -214,7 +226,7 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { break } } else { - filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, false) + filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, false) } } } @@ -229,7 +241,7 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { } if !hashesInitialized { - filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, true) + filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, true) hashesInitialized = true // Ignore any remaining conditions if the first condition resulted @@ -238,7 +250,7 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { break } } else { - filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, false) + filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, false) } } @@ -249,6 +261,13 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { return nil, errors.Wrapf(err, "failed to get Tx{%X}", h) } results = append(results, res) + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } return results, nil @@ -374,6 +393,7 @@ func isRangeOperation(op query.Operator) bool { // // NOTE: filteredHashes may be empty if no previous condition has matched. func (txi *TxIndex) match( + ctx context.Context, c query.Condition, startKeyBz []byte, filteredHashes map[string][]byte, @@ -397,6 +417,13 @@ func (txi *TxIndex) match( for ; it.Valid(); it.Next() { tmpHashes[string(it.Value())] = it.Value() + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } case c.Op == query.OpContains: @@ -417,6 +444,13 @@ func (txi *TxIndex) match( if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { tmpHashes[string(it.Value())] = it.Value() } + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } default: panic("other operators should be handled already") @@ -438,6 +472,13 @@ func (txi *TxIndex) match( for k := range filteredHashes { if tmpHashes[k] == nil { delete(filteredHashes, k) + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } } @@ -450,6 +491,7 @@ func (txi *TxIndex) match( // // NOTE: filteredHashes may be empty if no previous condition has matched. func (txi *TxIndex) matchRange( + ctx context.Context, r queryRange, startKey []byte, filteredHashes map[string][]byte, @@ -503,6 +545,13 @@ LOOP: // break // } } + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } if len(tmpHashes) == 0 || firstRun { @@ -521,6 +570,13 @@ LOOP: for k := range filteredHashes { if tmpHashes[k] == nil { delete(filteredHashes, k) + + // Potentially exit early. + select { + case <-ctx.Done(): + break + default: + } } } diff --git a/state/txindex/kv/kv_bench_test.go b/state/txindex/kv/kv_bench_test.go index 3e3bdced3..34d770040 100644 --- a/state/txindex/kv/kv_bench_test.go +++ b/state/txindex/kv/kv_bench_test.go @@ -1,6 +1,7 @@ package kv import ( + "context" "crypto/rand" "fmt" "io/ioutil" @@ -64,8 +65,10 @@ func BenchmarkTxSearch(b *testing.B) { b.ResetTimer() + ctx := context.Background() + for i := 0; i < b.N; i++ { - if _, err := indexer.Search(txQuery); err != nil { + if _, err := indexer.Search(ctx, txQuery); err != nil { b.Errorf("failed to query for txs: %s", err) } } diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index efb767f21..8756bc344 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -1,6 +1,7 @@ package kv import ( + "context" "fmt" "io/ioutil" "os" @@ -118,10 +119,12 @@ func TestTxSearch(t *testing.T) { {"account.number CONTAINS 'Iv'", 0}, } + ctx := context.Background() + for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { - results, err := indexer.Search(query.MustParse(tc.q)) + results, err := indexer.Search(ctx, query.MustParse(tc.q)) assert.NoError(t, err) assert.Len(t, results, tc.resultsLength) @@ -132,6 +135,73 @@ func TestTxSearch(t *testing.T) { } } +func TestTxSearchWithCancelation(t *testing.T) { + allowedKeys := []string{"account.number", "account.owner", "account.date"} + indexer := NewTxIndex(db.NewMemDB(), IndexEvents(allowedKeys)) + + txResult := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []kv.Pair{{Key: []byte("number"), Value: []byte("1")}}}, + {Type: "account", Attributes: []kv.Pair{{Key: []byte("owner"), Value: []byte("Ivan")}}}, + {Type: "", Attributes: []kv.Pair{{Key: []byte("not_allowed"), Value: []byte("Vlad")}}}, + }) + hash := txResult.Tx.Hash() + + err := indexer.Index(txResult) + require.NoError(t, err) + + testCases := []struct { + q string + resultsLength int + }{ + // search by hash + {fmt.Sprintf("tx.hash = '%X'", hash), 0}, + // search by exact match (one key) + {"account.number = 1", 0}, + // search by exact match (two keys) + {"account.number = 1 AND account.owner = 'Ivan'", 0}, + // search by exact match (two keys) + {"account.number = 1 AND account.owner = 'Vlad'", 0}, + {"account.owner = 'Vlad' AND account.number = 1", 0}, + {"account.number >= 1 AND account.owner = 'Vlad'", 0}, + {"account.owner = 'Vlad' AND account.number >= 1", 0}, + {"account.number <= 0", 0}, + {"account.number <= 0 AND account.owner = 'Ivan'", 0}, + // search using a prefix of the stored value + {"account.owner = 'Iv'", 0}, + // search by range + {"account.number >= 1 AND account.number <= 5", 0}, + // search by range (lower bound) + {"account.number >= 1", 0}, + // search by range (upper bound) + {"account.number <= 5", 0}, + // search using not allowed key + {"not_allowed = 'boom'", 0}, + // search for not existing tx result + {"account.number >= 2 AND account.number <= 5", 0}, + // search using not existing key + {"account.date >= TIME 2013-05-03T14:45:00Z", 0}, + // search using CONTAINS + {"account.owner CONTAINS 'an'", 0}, + // search for non existing value using CONTAINS + {"account.owner CONTAINS 'Vlad'", 0}, + // search using the wrong key (of numeric type) using CONTAINS + {"account.number CONTAINS 'Iv'", 0}, + } + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + for _, tc := range testCases { + tc := tc + t.Run(tc.q, func(t *testing.T) { + results, err := indexer.Search(ctx, query.MustParse(tc.q)) + assert.NoError(t, err) + assert.Len(t, results, tc.resultsLength) + }) + } +} + func TestTxSearchDeprecatedIndexing(t *testing.T) { allowedKeys := []string{"account.number", "sender"} indexer := NewTxIndex(db.NewMemDB(), IndexEvents(allowedKeys)) @@ -192,10 +262,12 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) { {"sender = 'addr1'", []*types.TxResult{txResult2}}, } + ctx := context.Background() + for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { - results, err := indexer.Search(query.MustParse(tc.q)) + results, err := indexer.Search(ctx, query.MustParse(tc.q)) require.NoError(t, err) require.Equal(t, results, tc.results) }) @@ -214,7 +286,9 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { err := indexer.Index(txResult) require.NoError(t, err) - results, err := indexer.Search(query.MustParse("account.number >= 1")) + ctx := context.Background() + + results, err := indexer.Search(ctx, query.MustParse("account.number >= 1")) assert.NoError(t, err) assert.Len(t, results, 1) @@ -268,7 +342,9 @@ func TestTxSearchMultipleTxs(t *testing.T) { err = indexer.Index(txResult4) require.NoError(t, err) - results, err := indexer.Search(query.MustParse("account.number >= 1")) + ctx := context.Background() + + results, err := indexer.Search(ctx, query.MustParse("account.number >= 1")) assert.NoError(t, err) require.Len(t, results, 3) diff --git a/state/txindex/null/null.go b/state/txindex/null/null.go index ae496a851..1ae7f7942 100644 --- a/state/txindex/null/null.go +++ b/state/txindex/null/null.go @@ -1,6 +1,7 @@ package null import ( + "context" "errors" "github.com/tendermint/tendermint/libs/pubsub/query" @@ -28,6 +29,6 @@ func (txi *TxIndex) Index(result *types.TxResult) error { return nil } -func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { +func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*types.TxResult, error) { return []*types.TxResult{}, nil } From a122a555de1d24866d21a48d60bc2b9e47651529 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 28 Feb 2020 11:53:06 +0100 Subject: [PATCH 42/74] docs: adr-046 add bisection algorithm details (#4496) * docs: adr-046 add bisection algorithm details Closes #4329 * format fig. 1 title * docs: adr-046 we no longer download headers in TrustedHeader https://github.com/tendermint/tendermint/pull/4496#issuecomment-592446054 --- .../adr-046-light-client-implementation.md | 26 ++++++++++++++++-- docs/architecture/img/adr-046-fig1.png | Bin 0 -> 13276 bytes 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 docs/architecture/img/adr-046-fig1.png diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index dca1c6692..6058da8f8 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -3,6 +3,7 @@ ## Changelog * 13-02-2020: Initial draft * 26-02-2020: Cross-checking the first header +* 28-02-2020: Bisection algorithm details ## Context @@ -67,9 +68,13 @@ required to respond: 1. Note the very first header (`TrustOptions.Hash`) is also cross-checked with witnesses for additional security. Due to bisection algorithm nature, some headers might be skipped. If the light -client does not have a header for height `X` and `TrustedHeader(X)` or -`TrustedValidatorSet(X)` methods are called, it will download the header from -primary provider and perform a backwards verification. +client does not have a header for height `X` and `VerifyHeaderAtHeight(X)` or +`VerifyHeader(H#X)` methods are called, it will perform a backwards +verification from the latest header back to the header at height `X`. + +`TrustedHeader`, `TrustedValidatorSet` only communicate with the trusted store. +If some header is not there, an error will be returned indicating that +verification is required. ```go type Provider interface { @@ -127,6 +132,21 @@ cases of adjacent and non-adjacent headers. In the former case, it compares the hashes directly (2/3+ signed transition). Otherwise, it verifies 1/3+ (`trustLevel`) of trusted validators are still present in new validators. +### Bisection algorithm details + +Non-recursive bisection algorithm was implemented despite the spec containing +the recursive version. There are two major reasons: + +1) Constant memory consumption => no risk of getting OOM (Out-Of-Memory) exceptions; +2) Faster finality (see Fig. 1). + +_Fig. 1: Differences between recursive and non-recursive bisections_ + +![Fig. 1](./img/adr-046-fig1.png) + +Specification of the non-recursive bisection can be found +[here](https://github.com/tendermint/spec/blob/zm_non-recursive-verification/spec/consensus/light-client/non-recursive-verification.md). + ## Status Accepted. diff --git a/docs/architecture/img/adr-046-fig1.png b/docs/architecture/img/adr-046-fig1.png new file mode 100644 index 0000000000000000000000000000000000000000..d68712e8a1a463647493abd5ce6787a5a8dd8b59 GIT binary patch literal 13276 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 CA6?b} literal 0 HcmV?d00001 From 6cebba5fd7baa7d8b83b00587833d61d58a7e777 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 28 Feb 2020 12:05:55 +0100 Subject: [PATCH 43/74] swagger: update swagger port (#4498) Signed-off-by: Marko Baricevic --- rpc/swagger/swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/swagger/swagger.yaml b/rpc/swagger/swagger.yaml index aa4d837ba..40b6e0169 100644 --- a/rpc/swagger/swagger.yaml +++ b/rpc/swagger/swagger.yaml @@ -7,7 +7,7 @@ info: name: Apache 2.0 url: https://github.com/tendermint/tendermint/blob/master/LICENSE servers: - - url: https://stargate.cosmos.network:26657 + - url: https://rpc.cosmos.network description: Cosmos mainnet node to interact with the Tendermint RPC - url: http://localhost:26657 description: Interact with the Tendermint RPC locally on your device From 6f6843e37aa2e391eb52185ae1d8255d26e31ebb Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 28 Feb 2020 12:21:12 +0100 Subject: [PATCH 44/74] circleci: fix reproducible builds test (#4497) --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8fe23501f..97b0653bc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -343,6 +343,7 @@ jobs: name: Build tendermint no_output_timeout: 20m command: | + sudo apt-get update sudo apt-get install -y ruby bash -x ./scripts/gitian-build.sh all for os in darwin linux windows; do From 6c9e9b2b93bf2e6b07aed9d759a74ac515cca3bf Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 28 Feb 2020 12:29:36 +0100 Subject: [PATCH 45/74] tool: add Mergify (#4490) --- .mergify.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .mergify.yml diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 000000000..e0491285d --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,10 @@ +pull_request_rules: + - name: automatic merge for master when CI passes and 1 reviews and has label S:automerge + conditions: + - "#approved-reviews-by>=1" + - "status-success=ci/circleci - Pull Request" + - base=master + - label=S:automerge + actions: + merge: + method: squash From d1f7ef673c0efebb5457eae94ea39e8a3581d270 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 28 Feb 2020 13:56:26 +0100 Subject: [PATCH 46/74] mergify: remove unnecessary conditions (#4501) --- .mergify.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index e0491285d..04e0bdfe6 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,8 +1,6 @@ pull_request_rules: - - name: automatic merge for master when CI passes and 1 reviews and has label S:automerge + - name: automerge to master with label S:automerge and branch protection passing conditions: - - "#approved-reviews-by>=1" - - "status-success=ci/circleci - Pull Request" - base=master - label=S:automerge actions: From 72488db676332368eababa85be593be73e5fd4e4 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 28 Feb 2020 14:21:50 +0100 Subject: [PATCH 47/74] mergify: use strict merges (#4502) --- .mergify.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.mergify.yml b/.mergify.yml index 04e0bdfe6..136bb1148 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,3 +6,4 @@ pull_request_rules: actions: merge: method: squash + strict: true From c5fe7334dc6166d9ada9818464f38734190a74ec Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Fri, 28 Feb 2020 14:29:48 +0100 Subject: [PATCH 48/74] test: simplified txsearch cancellation test (#4500) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- state/txindex/kv/kv_test.go | 54 +++---------------------------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index 8756bc344..fcd2eba13 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -144,62 +144,14 @@ func TestTxSearchWithCancelation(t *testing.T) { {Type: "account", Attributes: []kv.Pair{{Key: []byte("owner"), Value: []byte("Ivan")}}}, {Type: "", Attributes: []kv.Pair{{Key: []byte("not_allowed"), Value: []byte("Vlad")}}}, }) - hash := txResult.Tx.Hash() - err := indexer.Index(txResult) require.NoError(t, err) - testCases := []struct { - q string - resultsLength int - }{ - // search by hash - {fmt.Sprintf("tx.hash = '%X'", hash), 0}, - // search by exact match (one key) - {"account.number = 1", 0}, - // search by exact match (two keys) - {"account.number = 1 AND account.owner = 'Ivan'", 0}, - // search by exact match (two keys) - {"account.number = 1 AND account.owner = 'Vlad'", 0}, - {"account.owner = 'Vlad' AND account.number = 1", 0}, - {"account.number >= 1 AND account.owner = 'Vlad'", 0}, - {"account.owner = 'Vlad' AND account.number >= 1", 0}, - {"account.number <= 0", 0}, - {"account.number <= 0 AND account.owner = 'Ivan'", 0}, - // search using a prefix of the stored value - {"account.owner = 'Iv'", 0}, - // search by range - {"account.number >= 1 AND account.number <= 5", 0}, - // search by range (lower bound) - {"account.number >= 1", 0}, - // search by range (upper bound) - {"account.number <= 5", 0}, - // search using not allowed key - {"not_allowed = 'boom'", 0}, - // search for not existing tx result - {"account.number >= 2 AND account.number <= 5", 0}, - // search using not existing key - {"account.date >= TIME 2013-05-03T14:45:00Z", 0}, - // search using CONTAINS - {"account.owner CONTAINS 'an'", 0}, - // search for non existing value using CONTAINS - {"account.owner CONTAINS 'Vlad'", 0}, - // search using the wrong key (of numeric type) using CONTAINS - {"account.number CONTAINS 'Iv'", 0}, - } - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - for _, tc := range testCases { - tc := tc - t.Run(tc.q, func(t *testing.T) { - results, err := indexer.Search(ctx, query.MustParse(tc.q)) - assert.NoError(t, err) - assert.Len(t, results, tc.resultsLength) - }) - } + results, err := indexer.Search(ctx, query.MustParse("account.number = 1")) + assert.NoError(t, err) + assert.Empty(t, results) } func TestTxSearchDeprecatedIndexing(t *testing.T) { From 3f18e22c96853670f189da834e71ca5a80d353e5 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sat, 29 Feb 2020 13:22:19 -0800 Subject: [PATCH 49/74] rpc: keep the original subscription "id" field when new RPCs come in (#4493) --- CHANGELOG_PENDING.md | 2 ++ rpc/core/events.go | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index ebc087136..8877d31ec 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,6 +23,8 @@ program](https://hackerone.com/tendermint). ### BUG FIXES: +- [rpc] [\#4493](https://github.com/tendermint/tendermint/pull/4493) Keep the original subscription "id" field when new RPCs come in (@michaelfig) + - [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) - [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) diff --git a/rpc/core/events.go b/rpc/core/events.go index 2a4d042fc..165aa6e54 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -38,6 +38,8 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er return nil, err } + // Capture the current ID, since it can change in the future. + subscriptionID := ctx.JSONReq.ID go func() { for { select { @@ -46,7 +48,7 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er ctx.WSConn.TryWriteRPCResponse( rpctypes.NewRPCSuccessResponse( ctx.WSConn.Codec(), - ctx.JSONReq.ID, + subscriptionID, resultEvent, )) case <-sub.Cancelled(): @@ -59,7 +61,7 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er } ctx.WSConn.TryWriteRPCResponse( rpctypes.RPCServerError( - ctx.JSONReq.ID, + subscriptionID, fmt.Errorf("subscription was cancelled (reason: %s)", reason), )) } From c33576e72ae876291cb73570b4498d807596d1e2 Mon Sep 17 00:00:00 2001 From: Erik Grinaker Date: Sun, 1 Mar 2020 21:03:28 +0100 Subject: [PATCH 50/74] example/kvstore: return ABCI query height (#4509) * example/kvstore: return ABCI query height * CHANGELOG: added PR link * Updated tests --- CHANGELOG_PENDING.md | 5 +++-- abci/example/kvstore/kvstore.go | 2 ++ abci/example/kvstore/kvstore_test.go | 20 ++++++++++++++++++++ abci/tests/test_cli/ex1.abci.out | 4 ++-- docs/app-dev/abci-cli.md | 4 ++-- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8877d31ec..f85b63307 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -4,8 +4,7 @@ Special thanks to external contributors on this release: -Friendly reminder, we have a [bug bounty -program](https://hackerone.com/tendermint). +Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermint). ### BREAKING CHANGES: @@ -21,6 +20,8 @@ program](https://hackerone.com/tendermint). - [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) VerifyCommitX() functions should return as soon as +2/3 threashold is reached. +- [examples/kvstore] [\#4509](https://github.com/tendermint/tendermint/pull/4509) ABCI query now returns the proper height (@erikgrinaker) + ### BUG FIXES: - [rpc] [\#4493](https://github.com/tendermint/tendermint/pull/4493) Keep the original subscription "id" field when new RPCs come in (@michaelfig) diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index f856519f3..4e7449938 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -136,6 +136,7 @@ func (app *Application) Query(reqQuery types.RequestQuery) (resQuery types.Respo resQuery.Index = -1 // TODO make Proof return index resQuery.Key = reqQuery.Data resQuery.Value = value + resQuery.Height = app.state.Height return } @@ -151,6 +152,7 @@ func (app *Application) Query(reqQuery types.RequestQuery) (resQuery types.Respo resQuery.Log = "exists" } resQuery.Value = value + resQuery.Height = app.state.Height return resQuery } diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index e8dee67d9..4d8c829ad 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -30,6 +30,11 @@ func testKVStore(t *testing.T, app types.Application, tx []byte, key, value stri // repeating tx doesn't raise error ar = app.DeliverTx(req) require.False(t, ar.IsErr(), ar) + // commit + app.Commit() + + info := app.Info(types.RequestInfo{}) + require.NotZero(t, info.LastBlockHeight) // make sure query is fine resQuery := app.Query(types.RequestQuery{ @@ -37,7 +42,9 @@ func testKVStore(t *testing.T, app types.Application, tx []byte, key, value stri Data: []byte(key), }) require.Equal(t, code.CodeTypeOK, resQuery.Code) + require.Equal(t, key, string(resQuery.Key)) require.Equal(t, value, string(resQuery.Value)) + require.EqualValues(t, info.LastBlockHeight, resQuery.Height) // make sure proof is fine resQuery = app.Query(types.RequestQuery{ @@ -46,7 +53,9 @@ func testKVStore(t *testing.T, app types.Application, tx []byte, key, value stri Prove: true, }) require.EqualValues(t, code.CodeTypeOK, resQuery.Code) + require.Equal(t, key, string(resQuery.Key)) require.Equal(t, value, string(resQuery.Value)) + require.EqualValues(t, info.LastBlockHeight, resQuery.Height) } func TestKVStoreKV(t *testing.T) { @@ -300,6 +309,13 @@ func testClient(t *testing.T, app abcicli.Client, tx []byte, key, value string) ar, err = app.DeliverTxSync(types.RequestDeliverTx{Tx: tx}) require.NoError(t, err) require.False(t, ar.IsErr(), ar) + // commit + _, err = app.CommitSync() + require.NoError(t, err) + + info, err := app.InfoSync(types.RequestInfo{}) + require.NoError(t, err) + require.NotZero(t, info.LastBlockHeight) // make sure query is fine resQuery, err := app.QuerySync(types.RequestQuery{ @@ -308,7 +324,9 @@ func testClient(t *testing.T, app abcicli.Client, tx []byte, key, value string) }) require.Nil(t, err) require.Equal(t, code.CodeTypeOK, resQuery.Code) + require.Equal(t, key, string(resQuery.Key)) require.Equal(t, value, string(resQuery.Value)) + require.EqualValues(t, info.LastBlockHeight, resQuery.Height) // make sure proof is fine resQuery, err = app.QuerySync(types.RequestQuery{ @@ -318,5 +336,7 @@ func testClient(t *testing.T, app abcicli.Client, tx []byte, key, value string) }) require.Nil(t, err) require.Equal(t, code.CodeTypeOK, resQuery.Code) + require.Equal(t, key, string(resQuery.Key)) require.Equal(t, value, string(resQuery.Value)) + require.EqualValues(t, info.LastBlockHeight, resQuery.Height) } diff --git a/abci/tests/test_cli/ex1.abci.out b/abci/tests/test_cli/ex1.abci.out index 0cdd43df6..9e702b5ce 100644 --- a/abci/tests/test_cli/ex1.abci.out +++ b/abci/tests/test_cli/ex1.abci.out @@ -27,7 +27,7 @@ > query "abc" -> code: OK -> log: exists --> height: 0 +-> height: 2 -> key: abc -> key.hex: 616263 -> value: abc @@ -43,7 +43,7 @@ > query "def" -> code: OK -> log: exists --> height: 0 +-> height: 3 -> key: def -> key.hex: 646566 -> value: xyz diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index a72dc220f..e747e6d48 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -192,7 +192,7 @@ Try running these commands: > query "abc" -> code: OK -> log: exists --> height: 0 +-> height: 2 -> value: abc -> value.hex: 616263 @@ -206,7 +206,7 @@ Try running these commands: > query "def" -> code: OK -> log: exists --> height: 0 +-> height: 3 -> value: xyz -> value.hex: 78797A ``` From 9bf8f41ac93a64aef4fa0ff32456773bd86b4624 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 2 Mar 2020 10:51:36 +0400 Subject: [PATCH 51/74] lite2: fix tendermint lite sub command (#4505) * lite2: fix tendermint lite sub command - better logging - chainID as an argument - more examples * one more log msg * lite2: fire update right away after start * turn off auto update in verification tests Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- cmd/tendermint/commands/lite.go | 110 +++++++++++++----- docs/tendermint-core/light-client-protocol.md | 6 +- lite2/client.go | 11 +- lite2/client_test.go | 2 + 4 files changed, 94 insertions(+), 35 deletions(-) diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 8477b46ed..2d1798920 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -2,6 +2,7 @@ package commands import ( "net/http" + "os" "strings" "time" @@ -11,6 +12,7 @@ import ( amino "github.com/tendermint/go-amino" dbm "github.com/tendermint/tm-db" + "github.com/tendermint/tendermint/libs/log" tmos "github.com/tendermint/tendermint/libs/os" lite "github.com/tendermint/tendermint/lite2" "github.com/tendermint/tendermint/lite2/provider" @@ -24,15 +26,29 @@ import ( // LiteCmd represents the base command when called without any subcommands var LiteCmd = &cobra.Command{ - Use: "lite", + Use: "lite [chainID]", Short: "Run a light client proxy server, verifying Tendermint rpc", Long: `Run a light client proxy server, verifying Tendermint rpc. All calls that can be tracked back to a block header by a proof will be verified before passing them back to the caller. Other than -that, it will present the same interface as a full Tendermint node.`, - RunE: runProxy, - SilenceUsage: true, +that, it will present the same interface as a full Tendermint node. + +Example: + +start a fresh instance: + +lite cosmoshub-3 -p 52.57.29.196:26657 -w public-seed-node.cosmoshub.certus.one:26657 + --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD + +continue from latest state: + +lite cosmoshub-3 -p 52.57.29.196:26657 -w public-seed-node.cosmoshub.certus.one:26657 +`, + RunE: runProxy, + Args: cobra.ExactArgs(1), + Example: `lite cosmoshub-3 -p 52.57.29.196:26657 -w public-seed-node.cosmoshub.certus.one:26657 + --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948020CD`, } var ( @@ -46,15 +62,16 @@ var ( trustingPeriod time.Duration trustedHeight int64 trustedHash []byte + + verbose bool ) func init() { LiteCmd.Flags().StringVar(&listenAddr, "laddr", "tcp://localhost:8888", "Serve the proxy on the given address") - LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") - LiteCmd.Flags().StringVar(&primaryAddr, "primary", "tcp://localhost:26657", + LiteCmd.Flags().StringVarP(&primaryAddr, "primary", "p", "", "Connect to a Tendermint node at this address") - LiteCmd.Flags().StringVar(&witnessesAddrs, "witnesses", "", + LiteCmd.Flags().StringVarP(&witnessesAddrs, "witnesses", "w", "", "Tendermint nodes to cross-check the primary node, comma-separated") LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") LiteCmd.Flags().IntVar( @@ -64,21 +81,32 @@ func init() { "Maximum number of simultaneous connections (including WebSocket).") LiteCmd.Flags().DurationVar(&trustingPeriod, "trusting-period", 168*time.Hour, "Trusting period. Should be significantly less than the unbonding period") - LiteCmd.Flags().Int64Var(&trustedHeight, "trusted-height", 1, "Trusted header's height") - LiteCmd.Flags().BytesHexVar(&trustedHash, "trusted-hash", []byte{}, "Trusted header's hash") + LiteCmd.Flags().Int64Var(&trustedHeight, "height", 1, "Trusted header's height") + LiteCmd.Flags().BytesHexVar(&trustedHash, "hash", []byte{}, "Trusted header's hash") + LiteCmd.Flags().BoolVar(&verbose, "verbose", false, "Verbose output") } func runProxy(cmd *cobra.Command, args []string) error { - liteLogger := logger.With("module", "lite") + // Initialise logger. + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + var option log.Option + if verbose { + option, _ = log.AllowLevel("debug") + } else { + option, _ = log.AllowLevel("info") + } + logger = log.NewFilter(logger, option) - logger.Info("Connecting to the primary node...") - rpcClient, err := rpcclient.NewHTTP(chainID, primaryAddr) + chainID = args[0] + + logger.Info("Creating HTTP client for primary...", "addr", primaryAddr) + rpcClient, err := rpcclient.NewHTTP(primaryAddr, "/websocket") if err != nil { return errors.Wrapf(err, "http client for %s", primaryAddr) } primary := httpp.NewWithClient(chainID, rpcClient) - logger.Info("Connecting to the witness nodes...") + // TODO: use NewNetClient once we have it addrs := strings.Split(witnessesAddrs, ",") witnesses := make([]provider.Provider, len(addrs)) for i, addr := range addrs { @@ -89,40 +117,60 @@ func runProxy(cmd *cobra.Command, args []string) error { witnesses[i] = p } - logger.Info("Creating client...") + logger.Info("Creating client...", "chainID", chainID) db, err := dbm.NewGoLevelDB("lite-client-db", home) if err != nil { return err } - c, err := lite.NewClient( - chainID, - lite.TrustOptions{ - Period: trustingPeriod, - Height: trustedHeight, - Hash: trustedHash, - }, - primary, - witnesses, - dbs.New(db, chainID), - lite.Logger(liteLogger), - ) - if err != nil { - return err + + var c *lite.Client + if trustedHeight > 0 && len(trustedHash) > 0 { // fresh installation + c, err = lite.NewClient( + chainID, + lite.TrustOptions{ + Period: trustingPeriod, + Height: trustedHeight, + Hash: trustedHash, + }, + primary, + witnesses, + dbs.New(db, chainID), + lite.Logger(logger), + ) + } else { // continue from latest state + c, err = lite.NewClientFromTrustedStore( + chainID, + trustingPeriod, + primary, + witnesses, + dbs.New(db, chainID), + lite.Logger(logger), + ) } + if err != nil { + return errors.Wrap(err, "failed to create") + } + logger.Info("Starting client...") + err = c.Start() + if err != nil { + return errors.Wrap(err, "failed to start") + } + defer c.Stop() + p := lproxy.Proxy{ Addr: listenAddr, Config: &rpcserver.Config{MaxOpenConnections: maxOpenConnections}, Codec: amino.NewCodec(), Client: lrpc.NewClient(rpcClient, c), - Logger: liteLogger, + Logger: logger, } // Stop upon receiving SIGTERM or CTRL-C. - tmos.TrapSignal(liteLogger, func() { + tmos.TrapSignal(logger, func() { p.Listener.Close() }) - logger.Info("Starting proxy...") + logger.Info("Starting proxy...", "laddr", listenAddr) if err := p.ListenAndServe(); err != http.ErrServerClosed { // Error starting or closing listener: logger.Error("proxy ListenAndServe", "err", err) diff --git a/docs/tendermint-core/light-client-protocol.md b/docs/tendermint-core/light-client-protocol.md index 4537412c1..41b7a0e07 100644 --- a/docs/tendermint-core/light-client-protocol.md +++ b/docs/tendermint-core/light-client-protocol.md @@ -55,9 +55,9 @@ passing them back to the caller. Other than that, it will present the same interface as a full Tendermint node. ```sh -$ tendermint lite --chain-id=supernova --primary=tcp://233.123.0.140:26657 \ - --witnesses=tcp://179.63.29.15:26657,tcp://144.165.223.135:26657 \ - --trusted-height=10 --trusted-hash=37E9A6DD3FA25E83B22C18835401E8E56088D0D7ABC6FD99FCDC920DD76C1C57 +$ tendermint lite supernova -p tcp://233.123.0.140:26657 \ + -w tcp://179.63.29.15:26657,tcp://144.165.223.135:26657 \ + --height=10 --hash=37E9A6DD3FA25E83B22C18835401E8E56088D0D7ABC6FD99FCDC920DD76C1C57 ``` For additional options, run `tendermint lite --help`. diff --git a/lite2/client.go b/lite2/client.go index b70123099..5272f1b3a 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -166,12 +166,14 @@ func NewClient( } if c.latestTrustedHeader != nil { + c.logger.Info("Checking trusted header using options") if err := c.checkTrustedHeaderUsingOptions(trustOptions); err != nil { return nil, err } } if c.latestTrustedHeader == nil || c.latestTrustedHeader.Height < trustOptions.Height { + c.logger.Info("Downloading trusted header using options") if err := c.initializeWithTrustOptions(trustOptions); err != nil { return nil, err } @@ -258,7 +260,7 @@ func (c *Client) restoreTrustedHeaderAndVals() error { c.latestTrustedHeader = trustedHeader c.latestTrustedVals = trustedVals - c.logger.Debug("Restored trusted header and vals", "height", lastHeight) + c.logger.Info("Restored trusted header and vals", "height", lastHeight) } return nil @@ -930,6 +932,11 @@ func (c *Client) removeWitness(idx int) { func (c *Client) autoUpdateRoutine() { defer c.routinesWaitGroup.Done() + err := c.Update(time.Now()) + if err != nil { + c.logger.Error("Error during auto update", "err", err) + } + ticker := time.NewTicker(c.updatePeriod) defer ticker.Stop() @@ -1013,6 +1020,7 @@ func (c *Client) signedHeaderFromPrimary(height int64) (*types.SignedHeader, err if err == provider.ErrSignedHeaderNotFound { return nil, err } + c.logger.Error("Failed to get signed header from primary", "attempt", attempt, "err", err) time.Sleep(backoffTimeout(attempt)) } @@ -1036,6 +1044,7 @@ func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, err if err == nil || err == provider.ErrValidatorSetNotFound { return vals, err } + c.logger.Error("Failed to get validator set from primary", "attempt", attempt, "err", err) time.Sleep(backoffTimeout(attempt)) } diff --git a/lite2/client_test.go b/lite2/client_test.go index b1079390a..383bd4c60 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -154,6 +154,7 @@ func TestClient_SequentialVerification(t *testing.T) { )}, dbs.New(dbm.NewMemDB(), chainID), SequentialVerification(), + UpdatePeriod(0), ) if tc.initErr { @@ -280,6 +281,7 @@ func TestClient_SkippingVerification(t *testing.T) { )}, dbs.New(dbm.NewMemDB(), chainID), SkippingVerification(DefaultTrustLevel), + UpdatePeriod(0), ) if tc.initErr { require.Error(t, err) From 67224fd174eba42cb3efedea9474cb96bdbeab77 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 2 Mar 2020 17:09:55 +0400 Subject: [PATCH 52/74] docs: `tendermint node --help` dumps all supported flags (#4511) Closes #4211 --- docs/tendermint-core/using-tendermint.md | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 2c6370e84..b1098f54f 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -104,36 +104,37 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g ## Run -To run a Tendermint node, use +To run a Tendermint node, use: -``` +```sh tendermint node ``` By default, Tendermint will try to connect to an ABCI application on -[127.0.0.1:26658](127.0.0.1:26658). 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 of the `kvstore` app: +`127.0.0.1:26658`. 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 of +the `kvstore` app: -``` +```sh 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. +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 `counter`, `kvstore` and -`noop` apps that ship as examples with `abci-cli`. It's easy to compile -your own app in-process with Tendermint if it's written in Go. If your -app is not written in Go, simply run it in another process, and use the -`--proxy_app` flag to specify the address of the socket it is listening -on, for instance: +Tendermint supports in-process versions of the `counter`, `kvstore`, and `noop` +apps that ship as examples with `abci-cli`. It's easy to compile your app +in-process with Tendermint if it's written in Go. If your app is not written in +Go, run it in another process, and use the `--proxy_app` flag to specify the +address of the socket it is listening on, for instance: -``` +```sh tendermint node --proxy_app=/var/run/abci.sock ``` +You can find out what flags are supported by running `tendermint node --help`. + ## Transactions To send a transaction, use `curl` to make requests to the Tendermint RPC From 51e1b4e42acbb410265efd409d29ffecb4049e93 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 2 Mar 2020 17:55:50 +0400 Subject: [PATCH 53/74] rpc: remove BlockStoreRPC in favor of BlockStore (#4510) * rpc: use BlockStoreRPC instead of BlockStore BlockStoreRPC is a limited version of BlockStore interface, which does not include SaveBlock method. Closes #4159 * remove BlockStoreRPC interface in favor of single BlockStore interface Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- state/services.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/state/services.go b/state/services.go index bf9942811..d83a410c9 100644 --- a/state/services.go +++ b/state/services.go @@ -12,12 +12,15 @@ import ( //------------------------------------------------------ // blockstore -// BlockStoreRPC is the block store interface used by the RPC. -type BlockStoreRPC interface { +// BlockStore defines the interface used by the ConsensusState. +type BlockStore interface { Height() int64 LoadBlockMeta(height int64) *types.BlockMeta LoadBlock(height int64) *types.Block + + SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) + LoadBlockByHash(hash []byte) *types.Block LoadBlockPart(height int64, index int) *types.Part @@ -25,13 +28,7 @@ type BlockStoreRPC interface { LoadSeenCommit(height int64) *types.Commit } -// BlockStore defines the BlockStore interface used by the ConsensusState. -type BlockStore interface { - BlockStoreRPC - SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) -} - -//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // evidence pool // EvidencePool defines the EvidencePool interface used by the ConsensusState. From 3b2e1f22e4484092be26aa9a3911a710c270c981 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 2 Mar 2020 19:59:38 +0400 Subject: [PATCH 54/74] cmd: show useful error when tm not initialised (#4512) Closes #4303 Before: ``` TMHOME="/Users/daniel/go/src/github.com/tendermint/thome" tendermint node --proxy_app=unix:///Users/daniel/go/src/github.com/kvstore/example.sock ERROR: 1 error(s) decoding: * error decoding 'consensus.create_empty_blocks_interval': time: invalid duration ``` After: ``` $ TMHOME=/tmp ./build/tendermint node --proxy_app=kvstore ERROR: failed to create node: Couldn't read GenesisDoc file: open /tmp/config/genesis.json: no such file or directory ``` --- cmd/tendermint/commands/run_node.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index 6d9f575d0..628a0d173 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -78,7 +78,7 @@ func AddNodeFlags(cmd *cobra.Command) { "Set this to false to only produce blocks when there are txs or when the AppHash changes") cmd.Flags().String( "consensus.create_empty_blocks_interval", - string(config.Consensus.CreateEmptyBlocksInterval), + config.Consensus.CreateEmptyBlocksInterval.String(), "The possible interval between empty blocks") // db flags @@ -99,11 +99,21 @@ func NewRunNodeCmd(nodeProvider nm.Provider) *cobra.Command { Use: "node", Short: "Run the tendermint node", RunE: func(cmd *cobra.Command, args []string) error { + if err := checkGenesisHash(config); err != nil { + return err + } + n, err := nodeProvider(config, logger) if err != nil { - return fmt.Errorf("failed to create node: %v", err) + return fmt.Errorf("failed to create node: %w", err) } + if err := n.Start(); err != nil { + return fmt.Errorf("failed to start node: %w", err) + } + + logger.Info("Started node", "nodeInfo", n.Switch().NodeInfo()) + // Stop upon receiving SIGTERM or CTRL-C. tmos.TrapSignal(logger, func() { if n.IsRunning() { @@ -111,15 +121,6 @@ func NewRunNodeCmd(nodeProvider nm.Provider) *cobra.Command { } }) - if err := checkGenesisHash(config); err != nil { - return err - } - - if err := n.Start(); err != nil { - return fmt.Errorf("failed to start node: %v", err) - } - logger.Info("Started node", "nodeInfo", n.Switch().NodeInfo()) - // Run forever. select {} }, From a60d032b07fd6f5ee5687b0f7d206fb4ab16228d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 3 Mar 2020 12:37:29 +0400 Subject: [PATCH 55/74] docs: write about debug kill and dump (#4516) * docs: write about debug kill and dump Closes #4325 * wrap file tree in code blocks --- docs/tendermint-core/running-in-production.md | 62 +++++++++---------- docs/tools/README.md | 13 ++-- docs/tools/debugging.md | 57 +++++++++++++++++ 3 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 docs/tools/debugging.md diff --git a/docs/tendermint-core/running-in-production.md b/docs/tendermint-core/running-in-production.md index 7a436ec95..02a0eae58 100644 --- a/docs/tendermint-core/running-in-production.md +++ b/docs/tendermint-core/running-in-production.md @@ -111,54 +111,44 @@ to achieve the same things. ## Debugging Tendermint -If you ever have to debug Tendermint, the first thing you should -probably do is to check out the logs. See [How to read -logs](./how-to-read-logs.md), where we explain what certain log -statements mean. +If you ever have to debug Tendermint, the first thing you should probably do is +check out the logs. See [How to read logs](./how-to-read-logs.md), where we +explain what certain log statements mean. -If, after skimming through the logs, things are not clear still, the -next thing to try is query the /status RPC endpoint. It provides the -necessary info: whenever the node is syncing or not, what height it is -on, etc. +If, after skimming through the logs, things are not clear still, the next thing +to try is querying the `/status` RPC endpoint. It provides the necessary info: +whenever the node is syncing or not, what height it is on, etc. -``` +```sh curl http(s)://{ip}:{rpcPort}/status ``` -`dump_consensus_state` will give you a detailed overview of the -consensus state (proposer, lastest validators, peers states). From it, -you should be able to figure out why, for example, the network had -halted. +`/dump_consensus_state` will give you a detailed overview of the consensus +state (proposer, latest validators, peers states). From it, you should be able +to figure out why, for example, the network had halted. -``` +```sh curl http(s)://{ip}:{rpcPort}/dump_consensus_state ``` -There is a reduced version of this endpoint - `consensus_state`, which -returns just the votes seen at the current height. +There is a reduced version of this endpoint - `/consensus_state`, which returns +just the votes seen at the current height. -- [Github Issues](https://github.com/tendermint/tendermint/issues) -- [StackOverflow - questions](https://stackoverflow.com/questions/tagged/tendermint) +If, after consulting with the logs and above endpoints, you still have no idea +what's happening, consider using `tendermint debug kill` sub-command. This +command will scrap all the available info and kill the process. See +[Debugging](../tools/debugging.md) for the exact format. -### Debug Utility - -Tendermint also ships with a `debug` sub-command that allows you to kill a live -Tendermint process while collecting useful information in a compressed archive -such as the configuration used, consensus state, network state, the node' status, -the WAL, and even the stacktrace of the process before exit. These files can be -useful to examine when debugging a faulty Tendermint process. - -In addition, the `debug` sub-command also allows you to dump debugging data into -compressed archives at a regular interval. These archives contain the goroutine -and heap profiles in addition to the consensus state, network info, node status, -and even the WAL. +You can inspect the resulting archive yourself or create an issue on +[Github](https://github.com/tendermint/tendermint). Before opening an issue +however, be sure to check if there's [no existing +issue](https://github.com/tendermint/tendermint/issues) already. ## Monitoring Tendermint -Each Tendermint instance has a standard `/health` RPC endpoint, which -responds with 200 (OK) if everything is fine and 500 (or no response) - -if something is wrong. +Each Tendermint instance has a standard `/health` RPC endpoint, which responds +with 200 (OK) if everything is fine and 500 (or no response) - if something is +wrong. Other useful endpoints include mentioned earlier `/status`, `/net_info` and `/validators`. @@ -166,6 +156,10 @@ Other useful endpoints include mentioned earlier `/status`, `/net_info` and Tendermint also can report and serve Prometheus metrics. See [Metrics](./metrics.md). +`tendermint debug dump` sub-command can be used to periodically dump useful +information into an archive. See [Debugging](../tools/debugging.md) for more +information. + ## What happens when my app dies? You are supposed to run Tendermint under a [process diff --git a/docs/tools/README.md b/docs/tools/README.md index c326cde5b..bf9dd1f97 100644 --- a/docs/tools/README.md +++ b/docs/tools/README.md @@ -9,16 +9,21 @@ parent: Tendermint has some tools that are associated with it for: +- [Debugging](./debugging.md) - [Benchmarking](#benchmarking) -- [Validation of remote signers](./remote-signer-validation.md) - [Testnets](#testnets) - +- [Validation of remote signers](./remote-signer-validation.md) ## Benchmarking -Benchmarking is done with tm-load-test, for information on how to use the tool please visit the docs: https://github.com/interchainio/tm-load-test +- https://github.com/interchainio/tm-load-test +`tm-load-test` is a distributed load testing tool (and framework) for load +testing Tendermint networks. ## Testnets -The testnets tool is aimed at testing Tendermint with different configurations. For more information please visit: https://github.com/interchainio/testnets. +- https://github.com/interchainio/testnets + +This repository contains various different configurations of test networks for, +and relating to, Tendermint. diff --git a/docs/tools/debugging.md b/docs/tools/debugging.md new file mode 100644 index 000000000..50961dd3b --- /dev/null +++ b/docs/tools/debugging.md @@ -0,0 +1,57 @@ +# Debugging + +## tendermint debug kill + +Tendermint comes with a `debug` sub-command that allows you to kill a live +Tendermint process while collecting useful information in a compressed archive. +The information includes the configuration used, consensus state, network +state, the node' status, the WAL, and even the stack trace of the process +before exit. These files can be useful to examine when debugging a faulty +Tendermint process. + +```sh +tendermint debug kill --home= +``` + +will write debug info into a compressed archive. The archive will contain the +following: + +``` +├── config.toml +├── consensus_state.json +├── net_info.json +├── stacktrace.out +├── status.json +└── wal +``` + +Under the hood, `debug kill` fetches info from `/status`, `/net_info`, and +`/dump_consensus_state` HTTP endpoints, and kills the process with `-6`, which +catches the go-routine dump. + +## tendermint debug dump + +Also, the `debug dump` sub-command allows you to dump debugging data into +compressed archives at a regular interval. These archives contain the goroutine +and heap profiles in addition to the consensus state, network info, node +status, and even the WAL. + +```sh +tendermint debug dump --home= +``` + +will perform similarly to `kill` except it only polls the node and +dumps debugging data every frequency seconds to a compressed archive under a +given destination directory. Each archive will contain: + +``` +├── consensus_state.json +├── goroutine.out +├── heap.out +├── net_info.json +├── status.json +└── wal +``` + +Note: goroutine.out and heap.out will only be written if a profile address is +provided and is operational. This command is blocking and will log any error. From c518a062a7b39fe8427b47270bd71e34cb480576 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2020 15:52:36 +0400 Subject: [PATCH 56/74] deps: bump github.com/Workiva/go-datastructures (#4519) Bumps [github.com/Workiva/go-datastructures](https://github.com/Workiva/go-datastructures) from 1.0.50 to 1.0.51. - [Release notes](https://github.com/Workiva/go-datastructures/releases) - [Commits](https://github.com/Workiva/go-datastructures/compare/v1.0.50...v1.0.51) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index dd2b444fc..817b13cab 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f - github.com/Workiva/go-datastructures v1.0.50 + github.com/Workiva/go-datastructures v1.0.51 github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a github.com/fortytw2/leaktest v1.3.0 diff --git a/go.sum b/go.sum index c8eb10010..3833da4ca 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.50 h1:slDmfW6KCHcC7U+LP3DDBbm4fqTwZGn1beOFPfGaLvo= github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= +github.com/Workiva/go-datastructures v1.0.51 h1:LJHjjfcv+1gH+1D1SgrjcrF8iSZkgsAdCjclvHvVecQ= +github.com/Workiva/go-datastructures v1.0.51/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= From ff786515b8cc56dae299326040dd1bd65c829fbf Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 3 Mar 2020 13:00:06 +0100 Subject: [PATCH 57/74] lite: add helper functions for initiating the light client (#4486) * add new net client * hijack example test * lint fixes * reorganised file structure * renamed funcs and added documentation * implemented suggested changes * restored example tests * edited comments * Update lite2/setup.go Co-Authored-By: Bot from GolangCI <42910462+golangcibot@users.noreply.github.com> * Update lite2/client.go * lite cmds use HTTP client * better naming * Delete go.sum * Delete go.mod * restore go mod * restore go.sum * fix double import Co-authored-by: Anton Kaliaev Co-authored-by: Bot from GolangCI <42910462+golangcibot@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- cmd/tendermint/commands/lite.go | 46 +++++++------------- lite2/client.go | 7 +-- lite2/example_test.go | 2 +- lite2/setup.go | 77 +++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 lite2/setup.go diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 2d1798920..447c6d92b 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -9,14 +9,12 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - amino "github.com/tendermint/go-amino" + "github.com/tendermint/go-amino" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" tmos "github.com/tendermint/tendermint/libs/os" lite "github.com/tendermint/tendermint/lite2" - "github.com/tendermint/tendermint/lite2/provider" - httpp "github.com/tendermint/tendermint/lite2/provider/http" lproxy "github.com/tendermint/tendermint/lite2/proxy" lrpc "github.com/tendermint/tendermint/lite2/rpc" dbs "github.com/tendermint/tendermint/lite2/store/db" @@ -54,9 +52,9 @@ lite cosmoshub-3 -p 52.57.29.196:26657 -w public-seed-node.cosmoshub.certus.one: var ( listenAddr string primaryAddr string + witnessAddrsJoined string chainID string home string - witnessesAddrs string maxOpenConnections int trustingPeriod time.Duration @@ -71,7 +69,7 @@ func init() { "Serve the proxy on the given address") LiteCmd.Flags().StringVarP(&primaryAddr, "primary", "p", "", "Connect to a Tendermint node at this address") - LiteCmd.Flags().StringVarP(&witnessesAddrs, "witnesses", "w", "", + LiteCmd.Flags().StringVarP(&witnessAddrsJoined, "witnesses", "w", "", "Tendermint nodes to cross-check the primary node, comma-separated") LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") LiteCmd.Flags().IntVar( @@ -98,26 +96,10 @@ func runProxy(cmd *cobra.Command, args []string) error { logger = log.NewFilter(logger, option) chainID = args[0] - - logger.Info("Creating HTTP client for primary...", "addr", primaryAddr) - rpcClient, err := rpcclient.NewHTTP(primaryAddr, "/websocket") - if err != nil { - return errors.Wrapf(err, "http client for %s", primaryAddr) - } - primary := httpp.NewWithClient(chainID, rpcClient) - - // TODO: use NewNetClient once we have it - addrs := strings.Split(witnessesAddrs, ",") - witnesses := make([]provider.Provider, len(addrs)) - for i, addr := range addrs { - p, err := httpp.New(chainID, addr) - if err != nil { - return errors.Wrapf(err, "http provider for %s", addr) - } - witnesses[i] = p - } - logger.Info("Creating client...", "chainID", chainID) + + witnessesAddrs := strings.Split(witnessAddrsJoined, ",") + db, err := dbm.NewGoLevelDB("lite-client-db", home) if err != nil { return err @@ -125,24 +107,24 @@ func runProxy(cmd *cobra.Command, args []string) error { var c *lite.Client if trustedHeight > 0 && len(trustedHash) > 0 { // fresh installation - c, err = lite.NewClient( + c, err = lite.NewHTTPClient( chainID, lite.TrustOptions{ Period: trustingPeriod, Height: trustedHeight, Hash: trustedHash, }, - primary, - witnesses, + primaryAddr, + witnessesAddrs, dbs.New(db, chainID), lite.Logger(logger), ) } else { // continue from latest state - c, err = lite.NewClientFromTrustedStore( + c, err = lite.NewHTTPClientFromTrustedStore( chainID, trustingPeriod, - primary, - witnesses, + primaryAddr, + witnessesAddrs, dbs.New(db, chainID), lite.Logger(logger), ) @@ -158,6 +140,10 @@ func runProxy(cmd *cobra.Command, args []string) error { } defer c.Stop() + rpcClient, err := rpcclient.NewHTTP(primaryAddr, "/websocket") + if err != nil { + return errors.Wrapf(err, "http client for %s", primaryAddr) + } p := lproxy.Proxy{ Addr: listenAddr, Config: &rpcserver.Config{MaxOpenConnections: maxOpenConnections}, diff --git a/lite2/client.go b/lite2/client.go index 5272f1b3a..d65ed8ddb 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -953,11 +953,8 @@ func (c *Client) autoUpdateRoutine() { } } -// Update attempts to advance the state making exponential steps (note: -// when SequentialVerification is being used, the client will still be -// downloading all intermediate headers). -// -// Exposed for testing. +// Update attempts to advance the state by downloading the latest header and +// comparing it with the existing one. func (c *Client) Update(now time.Time) error { lastTrustedHeight, err := c.LastTrustedHeight() if err != nil { diff --git a/lite2/example_test.go b/lite2/example_test.go index 65997f906..e6839386f 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -135,7 +135,7 @@ func ExampleClient_VerifyHeaderAtHeight() { Hash: header.Hash(), }, primary, - []provider.Provider{primary}, // TODO: primary should not be used here + []provider.Provider{primary}, // NOTE: primary should not be used here dbs.New(db, chainID), UpdatePeriod(0), // Logger(log.TestingLogger()), diff --git a/lite2/setup.go b/lite2/setup.go new file mode 100644 index 000000000..50a4a9d21 --- /dev/null +++ b/lite2/setup.go @@ -0,0 +1,77 @@ +package lite + +import ( + "time" + + "github.com/tendermint/tendermint/lite2/provider" + "github.com/tendermint/tendermint/lite2/provider/http" + "github.com/tendermint/tendermint/lite2/store" +) + +// NewHTTPClient initiates an instance of a lite client using HTTP addresses +// for both the primary provider and witnesses of the lite client. A trusted +// header and hash must be passed to initialize the client. +// +// See all Option(s) for the additional configuration. +// See NewClient. +func NewHTTPClient( + chainID string, + trustOptions TrustOptions, + primaryAddress string, + witnessesAddresses []string, + trustedStore store.Store, + options ...Option) (*Client, error) { + + providers, err := providersFromAddresses(append(witnessesAddresses, primaryAddress), chainID) + if err != nil { + return nil, err + } + + return NewClient( + chainID, + trustOptions, + providers[len(providers)-1], + providers[:len(providers)-1], + trustedStore, + options...) +} + +// NewHTTPClientFromTrustedStore initiates an instance of a lite client using +// HTTP addresses for both the primary provider and witnesses and uses a +// trusted store as the root of trust. +// +// See all Option(s) for the additional configuration. +// See NewClientFromTrustedStore. +func NewHTTPClientFromTrustedStore( + chainID string, + trustingPeriod time.Duration, + primaryAddress string, + witnessesAddresses []string, + trustedStore store.Store, + options ...Option) (*Client, error) { + + providers, err := providersFromAddresses(append(witnessesAddresses, primaryAddress), chainID) + if err != nil { + return nil, err + } + + return NewClientFromTrustedStore( + chainID, + trustingPeriod, + providers[len(providers)-1], + providers[:len(providers)-1], + trustedStore, + options...) +} + +func providersFromAddresses(addrs []string, chainID string) ([]provider.Provider, error) { + providers := make([]provider.Provider, len(addrs)) + for idx, address := range addrs { + p, err := http.New(chainID, address) + if err != nil { + return nil, err + } + providers[idx] = p + } + return providers, nil +} From 891afc22352585697fb7baa6e448235bce4bfdb1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 3 Mar 2020 16:41:59 +0400 Subject: [PATCH 58/74] cmd: fix debug kill and change debug dump archive filename format (#4517) * cmd: change archive filename format before: Mar 3 08:44:43.zip after: 2020-03-03T08:44:43Z.zip Refs #4515 * cmd: kill pid, not the debug process itself Fixes #4515 * update changelog Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- CHANGELOG_PENDING.md | 5 ++--- cmd/tendermint/commands/debug/dump.go | 2 +- cmd/tendermint/commands/debug/kill.go | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f85b63307..47ff5dfb1 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -19,13 +19,12 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi ### IMPROVEMENTS: - [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) VerifyCommitX() functions should return as soon as +2/3 threashold is reached. - - [examples/kvstore] [\#4509](https://github.com/tendermint/tendermint/pull/4509) ABCI query now returns the proper height (@erikgrinaker) +- [cmd] \#4515 Change `tendermint debug dump` sub-command archives filename's format (@melekes) ### BUG FIXES: - [rpc] [\#4493](https://github.com/tendermint/tendermint/pull/4493) Keep the original subscription "id" field when new RPCs come in (@michaelfig) - - [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) - - [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) +- [cmd] \#4515 Fix `tendermint debug kill` sub-command (@melekes) diff --git a/cmd/tendermint/commands/debug/dump.go b/cmd/tendermint/commands/debug/dump.go index 80ca15c6b..33cb3e24d 100644 --- a/cmd/tendermint/commands/debug/dump.go +++ b/cmd/tendermint/commands/debug/dump.go @@ -126,7 +126,7 @@ func dumpDebugData(outDir string, conf *cfg.Config, rpc *rpcclient.HTTP) { } } - outFile := filepath.Join(outDir, fmt.Sprintf("%s.zip", start.Format(time.Stamp))) + outFile := filepath.Join(outDir, fmt.Sprintf("%s.zip", start.Format(time.RFC3339))) if err := zipDir(tmpDir, outFile); err != nil { logger.Error("failed to create and compress archive", "file", outFile, "error", err) } diff --git a/cmd/tendermint/commands/debug/kill.go b/cmd/tendermint/commands/debug/kill.go index 52defc69c..8d9df1161 100644 --- a/cmd/tendermint/commands/debug/kill.go +++ b/cmd/tendermint/commands/debug/kill.go @@ -124,7 +124,7 @@ func killProc(pid uint64, dir string) error { go func() { // Killing the Tendermint process with the '-ABRT|-6' signal will result in // a goroutine stacktrace. - p, err := os.FindProcess(os.Getpid()) + p, err := os.FindProcess(int(pid)) if err != nil { fmt.Fprintf(os.Stderr, "failed to find PID to kill Tendermint process: %s", err) } else if err = p.Signal(syscall.SIGABRT); err != nil { From 61380ffa3f43a88ae721fc205799d62c9c8dbcfc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2020 11:58:45 +0000 Subject: [PATCH 59/74] build(deps): bump github.com/prometheus/client_golang (#4525) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.4.1...v1.5.0) Signed-off-by: dependabot-preview[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 817b13cab..5af1190cf 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/libp2p/go-buffer-pool v0.0.2 github.com/magiconair/properties v1.8.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.4.1 + github.com/prometheus/client_golang v1.5.0 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a github.com/rs/cors v1.7.0 github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa diff --git a/go.sum b/go.sum index 3833da4ca..886eadee3 100644 --- a/go.sum +++ b/go.sum @@ -306,6 +306,8 @@ github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0 github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A= +github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= From 9e5b4afbf1da48d57643b3ccaeb6890b7c16af40 Mon Sep 17 00:00:00 2001 From: Tess Rinearson Date: Wed, 4 Mar 2020 15:48:17 +0100 Subject: [PATCH 60/74] .github: add markdown link checker (#4513) co-authored-by: Marko --- .github/workflows/action.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/action.yml diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml new file mode 100644 index 000000000..1b6f70730 --- /dev/null +++ b/.github/workflows/action.yml @@ -0,0 +1,10 @@ +name: Check Markdown links +on: push +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: gaurav-nelson/github-action-markdown-link-check@0.6.0 + with: + folder-path: "docs" From 4936b23dae289422171d0575eb6d18957b598372 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 5 Mar 2020 11:28:32 +0400 Subject: [PATCH 61/74] adr-047: evidence handling (#4429) Closes #4213 and #4182 --- docs/architecture/README.md | 1 + ...047-handling-evidence-from-light-client.md | 186 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 docs/architecture/adr-047-handling-evidence-from-light-client.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9b3c2f661..6eb6ebc4f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -67,5 +67,6 @@ Note the context/background should be written in the present tense. - [ADR-044-Lite-Client-With-Weak-Subjectivity](./adr-044-lite-client-with-weak-subjectivity.md) - [ADR-045-ABCI-Evidence](./adr-045-abci-evidence.md) - [ADR-046-Light-Client-Implementation](./adr-046-light-client-implementation.md) +- [ADR-047-Handling-Evidence-From-Light-Client](./adr-047-handling-evidence-from-light-client.md) - [ADR-051-Double-Signing-Risk-Reduction](./adr-051-double-signing-risk-reduction.md) - [ADR-052-Tendermint-Mode](./adr-052-tendermint-mode.md) diff --git a/docs/architecture/adr-047-handling-evidence-from-light-client.md b/docs/architecture/adr-047-handling-evidence-from-light-client.md new file mode 100644 index 000000000..8b3a850ba --- /dev/null +++ b/docs/architecture/adr-047-handling-evidence-from-light-client.md @@ -0,0 +1,186 @@ +# ADR 047: Handling evidence from light client + +## Changelog +* 18-02-2020: Initial draft +* 24-02-2020: Second version + +## Context + +If the light client is under attack, either directly -> lunatic/phantom +validators (light fork) or indirectly -> full fork, it's supposed to halt and +send evidence of misbehavior to a correct full node. Upon receiving an +evidence, the full node should punish malicious validators (if possible). + +## Decision + +When a light client sees two conflicting headers (`H1.Hash() != H2.Hash()`, +`H1.Height == H2.Height`), both having 1/3+ of the voting power of the +currently trusted validator set, it will submit a `ConflictingHeadersEvidence` +to all full nodes it's connected to. Evidence needs to be submitted to all full +nodes since there's no way to determine which full node is correct (honest). + +```go +type ConflictingHeadersEvidence struct { + H1 types.SignedHeader + H2 types.SignedHeader +} +``` + +When a full node receives the `ConflictingHeadersEvidence` evidence, it should +a) validate it b) figure out if malicious behaviour is obvious (immediately +slashable) or the fork accountability protocol needs to be started. + +### Validating headers + +Check both headers are valid (`ValidateBasic`), have the same height, and +signed by 1/3+ of the validator set that the full node had at height +`H1.Height-1`. + +- Q: What if light client validator set is not equal to full node's validator + set (i.e. from full node's point of view both headers are not properly signed; + this includes the case where none of the two headers were committed on the + main chain) + + Reject the evidence. It means light client is following a fork, but, hey, at + least it will halt. + +- Q: Don't we want to punish validators who signed something else even if they + have less or equal than 1/3? + + No consensus so far. Ethan said no, Zarko said yes. + https://github.com/tendermint/spec/pull/71#discussion_r374210533 + +### Figuring out if malicious behaviour is immediately slashable + +Let's say H1 was committed from this full node's perspective (see Appendix A). +Intersect validator sets of H1 and H2. + +* if there are signers(H2) that are not part of validators(H1), they misbehaved as +they are signing protocol messages in heights they are not validators => +immediately slashable (#F4). + +* if `H1.Round == H2.Round`, and some signers signed different precommit +messages in both commits, then it is an equivocation misbehavior => immediately +slashable (#F1). + +* if `H1.Round != H2.Round` we need to run full detection procedure => not +immediately slashable. + +* if `ValidatorsHash`, `NextValidatorsHash`, `ConsensusHash`, +`AppHash`, and `LastResultsHash` in H2 are different (incorrect application +state transition), then it is a lunatic misbehavior => immediately slashable (#F5). + +If evidence is not immediately slashable, fork accountability needs to invoked +(ADR does not yet exist). + +It's unclear if we should further break up `ConflictingHeadersEvidence` or +gossip and commit it directly. See +https://github.com/tendermint/tendermint/issues/4182#issuecomment-590339233 + +If we'd go without breaking evidence, all we'll need to do is to strip the +committed header from `ConflictingHeadersEvidence` (H1) and leave only the +uncommitted header (H2): + +```go +type ConflictingHeaderEvidence struct { + H types.SignedHeader +} +``` + +If we'd go with breaking evidence, here are the types we'll need: + +### F1. Equivocation + +Existing `DuplicateVoteEvidence` needs to be created and gossiped. + +### F4. Phantom validators + +A new type of evidence needs to be created: + +```go +type PhantomValidatorEvidence struct { + PubKey crypto.PubKey + Vote types.Vote +} +``` + +It contains a validator's public key and a vote for a block, where this +validator is not part of the validator set. + +### F5. Lunatic validator + +```go +type LunaticValidatorEvidence struct { + Header types.Header + Vote types.Vote +} +``` + +To punish this attack, we need support for a new Evidence type - +`LunaticValidatorEvidence`. This type includes a vote and a header. The header +must contain fields that are invalid with respect to the previous block, and a +vote for that header by a validator that was in a validator set within the +unbonding period. While the attack is only possible if +1/3 of some validator +set colludes, the evidence should be verifiable independently for each +individual validator. This means the total evidence can be split into one piece +of evidence per attacking validator and gossipped to nodes to be verified one +piece at a time, reducing the DoS attack surface at the peer layer. + +Note it is not sufficient to simply compare this header with that committed for +the corresponding height, as an honest node may vote for a header that is not +ultimately committed. Certain fields may also be variable, for instance the +`LastCommitHash` and the `Time` may depend on which votes the proposer includes. +Thus, the header must be explicitly checked for invalid data. + +For the attack to succeed, VC must sign a header that changes the validator set +to consist of something they control. Without doing this, they can not +otherwise attack the light client, since the client verifies commits according +to validator sets. Thus, it should be sufficient to check only that +`ValidatorsHash` and `NextValidatorsHash` are correct with respect to the +header that was committed at the corresponding height. + +That said, if the attack is conducted by +2/3 of the validator set, they don't +need to make an invalid change to the validator set, since they already control +it. Instead they would make invalid changes to the `AppHash`, or possibly other +fields. In order to punish them, then, we would have to check all header +fields. + +Note some header fields require the block itself to verify, which the light +client, by definition, does not possess, so it may not be possible to check +these fields. For now, then, `LunaticValidatorEvidence` must be checked against +all header fields which are a function of the application at previous blocks. +This includes `ValidatorsHash`, `NextValidatorsHash`, `ConsensusHash`, +`AppHash`, and `LastResultsHash`. These should all match what's in the header +for the block that was actually committed at the corresponding height, and +should thus be easy to check. + +## Status + +Proposed. + +## Consequences + +### Positive + +* Tendermint will be able to detect & punish new types of misbehavior +* light clients connected to multiple full nodes can help full nodes notice a + fork faster + +### Negative + +* Accepting `ConflictingHeadersEvidence` from light clients opens up a DDOS +attack vector (same is fair for any RPC endpoint open to public; remember that +RPC is not open by default). + +### Neutral + +## References + +* [Fork accountability spec](https://github.com/tendermint/spec/blob/master/spec/consensus/light-client/accountability.md) + +## Appendix A + +If there is an actual fork (full fork), a full node may follow either one or +another branch. So both H1 or H2 can be considered committed depending on which +branch the full node is following. It's supposed to halt if it notices an +actual fork, but there's a small chance it doesn't. From 4c8e3c814521bef5e9040fb1e9ae67907fd699e1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2020 08:41:36 +0000 Subject: [PATCH 62/74] fix: proto-breakage (#4506) * fix: fix proto-breakage - this is amed to fix proto breakage for consumers Signed-off-by: Marko Baricevic * fix for importing third_party everywhere * undo change * test breakage change * test ssh * test https * change ssh to https * fix phony --- .circleci/config.yml | 5 +---- Makefile | 7 ++++++- abci/types/types.pb.go | 2 +- buf.yaml | 1 - libs/kv/types.pb.go | 4 ++-- scripts/protocgen.sh | 2 +- tools.mk | 23 ++++------------------- 7 files changed, 15 insertions(+), 29 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 97b0653bc..b9d535da6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,7 +87,7 @@ jobs: steps: - checkout - run: - command: make proto-check-breaking + command: make proto-check-breaking-ci test_abci_apps: executor: golang @@ -378,13 +378,10 @@ jobs: # command: | # set -x # export PATH=~/.local/bin:$PATH - # # install node and dredd # ./scripts/get_nodejs.sh - # # build the binaries with a proper version of Go # docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux build-contract-tests-hooks - # # This docker image works with go 1.7, we can install here the hook handler that contract-tests is going to use # go get github.com/snikch/goodman/cmd/goodman # make contract-tests diff --git a/Makefile b/Makefile index 5557a87ae..da4d440e7 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ OUTPUT?=build/tendermint BUILD_TAGS?='tendermint' LD_FLAGS = -X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD` -s -w BUILD_FLAGS = -mod=readonly -ldflags "$(LD_FLAGS)" +HTTPS_GIT := https://github.com/tendermint/tendermint.git all: check build test install .PHONY: all @@ -57,9 +58,13 @@ proto-lint: .PHONY: proto-lint proto-check-breaking: - @buf check breaking --against-input '.git#branch=master' + @buf check breaking --against-input ".git#branch=master" .PHONY: proto-check-breaking +proto-check-breaking-ci: + @buf check breaking --against-input "$(HTTPS_GIT)#branch=master" +.PHONY: proto-check-breaking-ci + ############################################################################### ### Build ABCI ### ############################################################################### diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index d8445f469..6c18cfbd1 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -2571,7 +2571,7 @@ func (m *PartSetHeader) GetHash() []byte { // Validator type Validator struct { Address []byte `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - //PubKey pub_key = 2 [(gogoproto.nullable)=false]; + // PubKey pub_key = 2 [(gogoproto.nullable)=false]; Power int64 `protobuf:"varint,3,opt,name=power,proto3" json:"power,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` diff --git a/buf.yaml b/buf.yaml index 1b97487f4..7349a3516 100644 --- a/buf.yaml +++ b/buf.yaml @@ -13,4 +13,3 @@ lint: breaking: use: - FILE - - PACKAGE diff --git a/libs/kv/types.pb.go b/libs/kv/types.pb.go index b572ac205..c197d5791 100644 --- a/libs/kv/types.pb.go +++ b/libs/kv/types.pb.go @@ -26,7 +26,7 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// Define these here for compatibility but use tmlibs/common.KVPair. +// Define these here for compatibility but use tmlibs/kv.Pair. type Pair struct { Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` @@ -82,7 +82,7 @@ func (m *Pair) GetValue() []byte { return nil } -// Define these here for compatibility but use tmlibs/common.KI64Pair. +// Define these here for compatibility but use tmlibs/kv.KI64Pair. type KI64Pair struct { Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` diff --git a/scripts/protocgen.sh b/scripts/protocgen.sh index 922512ace..e6226dc1a 100644 --- a/scripts/protocgen.sh +++ b/scripts/protocgen.sh @@ -8,4 +8,4 @@ for dir in $proto_dirs; do -I. \ --gogo_out=Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration,plugins=grpc,paths=source_relative:. \ $(find "${dir}" -name '*.proto') -done \ No newline at end of file +done diff --git a/tools.mk b/tools.mk index 66c6f2978..516fc494e 100644 --- a/tools.mk +++ b/tools.mk @@ -77,29 +77,14 @@ $(PROTOBUF): @go get github.com/gogo/protobuf/protoc-gen-gogo@v1.3.1 .PHONY: protobuf -buf: protoc-gen-buf-check-breaking protoc-gen-buf-check-lint +buf: @echo "Installing buf..." @curl -sSL \ - "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-${UNAME_S}-${UNAME_M}" \ - -o "${BIN}/buf" && \ - chmod +x "${BIN}/buf" + "https://github.com/bufbuild/buf/releases/download/v$(BUF_VERSION)/buf-$(UNAME_S)-$(UNAME_M)" \ + -o "$(BIN)/buf" && \ + chmod +x "$(BIN)/buf" .PHONY: buf -protoc-gen-buf-check-breaking: - @echo "Installing protoc-gen-buf-check-breaking..." - @curl -sSL \ - "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/protoc-gen-buf-check-breaking-${UNAME_S}-${UNAME_M}" \ - -o "${BIN}/protoc-gen-buf-check-breaking" && \ - chmod +x "${BIN}/protoc-gen-buf-check-breaking" - -protoc-gen-buf-check-lint: - @echo "Installing protoc-gen-buf-check-lint..." - @curl -sSL \ - "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/protoc-gen-buf-check-lint-${UNAME_S}-${UNAME_M}" \ - -o "${BIN}/protoc-gen-buf-check-lint" && \ - chmod +x "${BIN}/protoc-gen-buf-check-lint" -.PHONY: protoc-gen-buf-check-lint - goodman: $(GOODMAN) $(GOODMAN): @echo "Get Goodman" From 632b53d6535e2edb4cefe84bdbecfa3736143b2d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2020 09:29:26 +0000 Subject: [PATCH 63/74] fix addrbook tests (#4526) Co-authored-by: Anton Kaliaev Co-authored-by: Erik Grinaker --- p2p/pex/addrbook_test.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/p2p/pex/addrbook_test.go b/p2p/pex/addrbook_test.go index 4942b403b..363958c44 100644 --- a/p2p/pex/addrbook_test.go +++ b/p2p/pex/addrbook_test.go @@ -60,15 +60,15 @@ func TestAddrBookSaveLoad(t *testing.T) { defer deleteTempFile(fname) // 0 addresses - book := NewAddrBook(fname, true).(*addrBook) + book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) - book.saveToFile(fname) + book.Save() - book = NewAddrBook(fname, true).(*addrBook) + book = NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) - book.loadFromFile(fname) + book.Start() - assert.Zero(t, book.Size()) + assert.True(t, book.Empty()) // 100 addresses randAddrs := randNetAddressPairs(t, 100) @@ -78,11 +78,11 @@ func TestAddrBookSaveLoad(t *testing.T) { } assert.Equal(t, 100, book.Size()) - book.saveToFile(fname) + book.Save() - book = NewAddrBook(fname, true).(*addrBook) + book = NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) - book.loadFromFile(fname) + book.Start() assert.Equal(t, 100, book.Size()) } @@ -93,19 +93,15 @@ func TestAddrBookLookup(t *testing.T) { randAddrs := randNetAddressPairs(t, 100) - book := NewAddrBook(fname, true).(*addrBook) + book := NewAddrBook(fname, true) book.SetLogger(log.TestingLogger()) for _, addrSrc := range randAddrs { addr := addrSrc.addr src := addrSrc.src book.AddAddress(addr, src) - ka := book.addrLookup[addr.ID] - assert.NotNil(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) - - if !(ka.Addr.Equals(addr) && ka.Src.Equals(src)) { - t.Fatalf("KnownAddress doesn't match addr & src") - } + ka := book.HasAddress(addr) + assert.True(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) } } From 3a695d4b6f92b61a1fa612ceba971414edc2c108 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2020 10:48:29 +0000 Subject: [PATCH 64/74] docs: fix links (#4531) * docs: fix links - fix broken links closes #4522 Signed-off-by: Marko Baricevic * fix more links * add enable and disable * remvoe deadlink --- docs/DOCS_README.md | 9 ++- docs/README.md | 2 +- docs/app-dev/abci-cli.md | 2 +- docs/app-dev/app-development.md | 2 +- docs/app-dev/indexing-transactions.md | 2 +- .../subscribing-to-events-via-websocket.md | 2 +- docs/architecture/README.md | 4 +- .../adr-002-event-subscription.md | 1 - docs/architecture/adr-011-monitoring.md | 2 +- docs/architecture/adr-018-ABCI-Validators.md | 2 +- .../adr-043-blockchain-riri-org.md | 66 ++++++++++--------- docs/tendermint-core/how-to-read-logs.md | 5 +- docs/tendermint-core/running-in-production.md | 2 +- docs/tendermint-core/using-tendermint.md | 4 ++ 14 files changed, 57 insertions(+), 48 deletions(-) diff --git a/docs/DOCS_README.md b/docs/DOCS_README.md index 0da81aa21..ae16c18c9 100644 --- a/docs/DOCS_README.md +++ b/docs/DOCS_README.md @@ -2,8 +2,7 @@ The documentation for Tendermint Core is hosted at: -- https://docs.tendermint.com/master/ and -- https://tendermint-staging.interblock.io/docs/ +- https://docs.tendermint.com/master/ built from the files in this (`/docs`) directory for [master](https://github.com/tendermint/tendermint/tree/master/docs) respectively. @@ -78,9 +77,13 @@ Install the theme and all dependencies. npm run serve ``` + + Run `pre` and `post` hooks and start a hot-reloading web-server. See output of this command for the URL (it is often https://localhost:8080). -To build documentation as a static website run `npm run build`. You will find the website in `.vuepress/dist` directory. + + +To build documentation as a static website run `npm run build`. You will find the website in `.vuepress/dist` directory. ## Search diff --git a/docs/README.md b/docs/README.md index b1b876045..1b7d691e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ Tendermint?](introduction/what-is-tendermint.md). To get started quickly with an example application, see the [quick start guide](introduction/quick-start.md). -To learn about application development on Tendermint, see the [Application Blockchain Interface](spec/abci/). +To learn about application development on Tendermint, see the [Application Blockchain Interface](https://github.com/tendermint/spec/tree/master/spec/abci). For more details on using Tendermint, see the respective documentation for [Tendermint Core](tendermint-core/), [benchmarking and monitoring](tools/), and [network deployments](networks/). diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index e747e6d48..ec8b0abf3 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -142,7 +142,7 @@ response. The server may be generic for a particular language, and we provide a [reference implementation in Golang](https://github.com/tendermint/tendermint/tree/master/abci/server). See the -[list of other ABCI implementations](./ecosystem.md) for servers in +[list of other ABCI implementations](https://github.com/tendermint/awesome#ecosystem) for servers in other languages. The handler is specific to the application, and may be arbitrary, so diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index 44a2ea623..9c1acc289 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -7,7 +7,7 @@ order: 4 ## XXX This page is undergoing deprecation. All content is being moved to the new [home -of the ABCI specification](../spec/abci/README.md). +of the ABCI specification](https://github.com/tendermint/spec/tree/master/spec/abci). ## ABCI Design diff --git a/docs/app-dev/indexing-transactions.md b/docs/app-dev/indexing-transactions.md index 4afca5775..3e3fcd551 100644 --- a/docs/app-dev/indexing-transactions.md +++ b/docs/app-dev/indexing-transactions.md @@ -14,7 +14,7 @@ type, only the key-value pairs defined in `EndBlock` are used. Each event contains a type and a list of attributes, which are key-value pairs denoting something about what happened during the method's execution. For more -details on `Events`, see the [ABCI](../spec/abci/abci.md) documentation. +details on `Events`, see the [ABCI]https://github.com/tendermint/spec/blob/master/spec/abci/abci.md#events) documentation. An Event has a composite key associated with it. A `compositeKey` is constructed by its type and key separated by a dot. For example: diff --git a/docs/app-dev/subscribing-to-events-via-websocket.md b/docs/app-dev/subscribing-to-events-via-websocket.md index 5f5cc8921..6e4f0d207 100644 --- a/docs/app-dev/subscribing-to-events-via-websocket.md +++ b/docs/app-dev/subscribing-to-events-via-websocket.md @@ -36,7 +36,7 @@ transactions](./indexing-transactions.md) for details. When validator set changes, ValidatorSetUpdates event is published. The event carries a list of pubkey/power pairs. The list is the same Tendermint receives from ABCI application (see [EndBlock -section](../spec/abci/abci.md#endblock) in +section](https://github.com/tendermint/spec/blob/master/spec/abci/abci.md#endblock) in the ABCI spec). Response: diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 6eb6ebc4f..b571564d5 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -37,7 +37,7 @@ Note the context/background should be written in the present tense. - [ADR-006-Trust-Metric](./adr-006-trust-metric.md) - [ADR-007-Trust-Metric-Usage](./adr-007-trust-metric-usage.md) - [ADR-008-Priv-Validator](./adr-008-priv-validator.md) -- [ADR-009-ABCI-Design](./adr-009-abci-design.md) +- [ADR-009-ABCI-Design](./adr-009-ABCI-design.md) - [ADR-010-Crypto-Changes](./adr-010-crypto-changes.md) - [ADR-011-Monitoring](./adr-011-monitoring.md) - [ADR-012-Peer-Transport](./adr-012-peer-transport.md) @@ -46,7 +46,7 @@ Note the context/background should be written in the present tense. - [ADR-015-Crypto-Encoding](./adr-015-crypto-encoding.md) - [ADR-016-Protocol-Versions](./adr-016-protocol-versions.md) - [ADR-017-Chain-Versions](./adr-017-chain-versions.md) -- [ADR-018-ABCI-Validators](./adr-018-abci-validators.md) +- [ADR-018-ABCI-Validators](./adr-018-ABCI-Validators.md) - [ADR-019-Multisigs](./adr-019-multisigs.md) - [ADR-020-Block-Size](./adr-020-block-size.md) - [ADR-021-ABCI-Events](./adr-021-abci-events.md) diff --git a/docs/architecture/adr-002-event-subscription.md b/docs/architecture/adr-002-event-subscription.md index a73d584ab..e5c98637f 100644 --- a/docs/architecture/adr-002-event-subscription.md +++ b/docs/architecture/adr-002-event-subscription.md @@ -65,7 +65,6 @@ For historic queries we will need a indexing storage (Postgres, SQLite, ...). ### Issues -- https://github.com/tendermint/basecoin/issues/91 - https://github.com/tendermint/tendermint/issues/376 - https://github.com/tendermint/tendermint/issues/287 - https://github.com/tendermint/tendermint/issues/525 (related) diff --git a/docs/architecture/adr-011-monitoring.md b/docs/architecture/adr-011-monitoring.md index 8f2d009dd..4c79507a1 100644 --- a/docs/architecture/adr-011-monitoring.md +++ b/docs/architecture/adr-011-monitoring.md @@ -19,7 +19,7 @@ A few solutions were considered: b) [go-kit metrics package](https://github.com/go-kit/kit/tree/master/metrics) as an interface plus Prometheus c) [telegraf](https://github.com/influxdata/telegraf) d) new service, which will listen to events emitted by pubsub and report metrics -2. [OpenCensus](https://opencensus.io/go/index.html) +2. [OpenCensus](https://opencensus.io/introduction/) ### 1. Prometheus diff --git a/docs/architecture/adr-018-ABCI-Validators.md b/docs/architecture/adr-018-ABCI-Validators.md index f40efca15..f5ffdccaa 100644 --- a/docs/architecture/adr-018-ABCI-Validators.md +++ b/docs/architecture/adr-018-ABCI-Validators.md @@ -32,7 +32,7 @@ message ValidatorUpdate { } ``` -As noted in ADR-009[https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-009-ABCI-design.md], +As noted in [ADR-009](adr-009-ABCI-design.md), the `Validator` does not contain a pubkey because quantum public keys are quite large and it would be wasteful to send them all over ABCI with every block. Thus, applications that want to take advantage of the information in BeginBlock diff --git a/docs/architecture/adr-043-blockchain-riri-org.md b/docs/architecture/adr-043-blockchain-riri-org.md index 6bb018f51..303def716 100644 --- a/docs/architecture/adr-043-blockchain-riri-org.md +++ b/docs/architecture/adr-043-blockchain-riri-org.md @@ -1,25 +1,26 @@ # ADR 043: Blockhchain Reactor Riri-Org ## Changelog -* 18-06-2019: Initial draft -* 08-07-2019: Reviewed -* 29-11-2019: Implemented -* 14-02-2020: Updated with the implementation details + +- 18-06-2019: Initial draft +- 08-07-2019: Reviewed +- 29-11-2019: Implemented +- 14-02-2020: Updated with the implementation details ## Context -The blockchain reactor is responsible for two high level processes:sending/receiving blocks from peers and FastSync-ing blocks to catch upnode who is far behind. The goal of [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) was to refactor these two processes by separating business logic currently wrapped up in go-channels into pure `handle*` functions. While the ADR specified what the final form of the reactor might look like it lacked guidance on intermediary steps to get there. -The following diagram illustrates the state of the [blockchain-reorg](https://github.com/tendermint/tendermint/pull/35610) reactor which will be referred to as `v1`. +The blockchain reactor is responsible for two high level processes:sending/receiving blocks from peers and FastSync-ing blocks to catch upnode who is far behind. The goal of [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) was to refactor these two processes by separating business logic currently wrapped up in go-channels into pure `handle*` functions. While the ADR specified what the final form of the reactor might look like it lacked guidance on intermediary steps to get there. +The following diagram illustrates the state of the [blockchain-reorg](https://github.com/tendermint/tendermint/pull/3561) reactor which will be referred to as `v1`. ![v1 Blockchain Reactor Architecture Diagram](https://github.com/tendermint/tendermint/blob/f9e556481654a24aeb689bdadaf5eab3ccd66829/docs/architecture/img/blockchain-reactor-v1.png) While `v1` of the blockchain reactor has shown significant improvements in terms of simplifying the concurrency model, the current PR has run into few roadblocks. -* The current PR large and difficult to review. -* Block gossiping and fast sync processes are highly coupled to the shared `Pool` data structure. -* Peer communication is spread over multiple components creating complex dependency graph which must be mocked out during testing. -* Timeouts modeled as stateful tickers introduce non-determinism in tests +- The current PR large and difficult to review. +- Block gossiping and fast sync processes are highly coupled to the shared `Pool` data structure. +- Peer communication is spread over multiple components creating complex dependency graph which must be mocked out during testing. +- Timeouts modeled as stateful tickers introduce non-determinism in tests This ADR is meant to specify the missing components and control necessary to achieve [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md). @@ -32,7 +33,7 @@ Diagram](https://github.com/tendermint/tendermint/blob/584e67ac3fac220c5c3e0652e ### Fast Sync Related Communication Channels -The diagram below shows the fast sync routines and the types of channels and queues used to communicate with each other. +The diagram below shows the fast sync routines and the types of channels and queues used to communicate with each other. In addition the per reactor channels used by the sendRoutine to send messages over the Peer MConnection are shown. ![v2 Blockchain Channels and Queues @@ -42,7 +43,6 @@ Diagram](https://github.com/tendermint/tendermint/blob/5cf570690f989646fb3b615b7 The reactor will include a demultiplexing routine which will send each message to each sub routine for independent processing. Each sub routine will then select the messages it's interested in and call the handle specific function specified in [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md). The demuxRoutine acts as "pacemaker" setting the time in which events are expected to be handled. - ```go func demuxRoutine(msgs, scheduleMsgs, processorMsgs, ioMsgs) { timer := time.NewTicker(interval) @@ -143,6 +143,7 @@ func (r *BlockchainReactor) AddPeer(peer p2p.Peer) { ``` ## IO handling + An io handling routine within the reactor will isolate peer communication. Message going through the ioRoutine will usually be one way, using `p2p` APIs. In the case in which the `p2p` API such as `trySend` return errors, the ioRoutine can funnel those message back to the demuxRoutine for distribution to the other routines. For instance errors from the ioRoutine can be consumed by the scheduler to inform better peer selection implementations. ```go @@ -169,6 +170,7 @@ func (r *BlockchainReacor) ioRoutine(ioMesgs chan Message, outMsgs chan Message) } ``` + ### Processor Internals The processor is responsible for ordering, verifying and executing blocks. The Processor will maintain an internal cursor `height` refering to the last processed block. As a set of blocks arrive unordered, the Processor will check if it has `height+1` necessary to process the next block. The processor also maintains the map `blockPeers` of peers to height, to keep track of which peer provided the block at `height`. `blockPeers` can be used in`handleRemovePeer(...)` to reschedule all unprocessed blocks provided by a peer who has errored. @@ -232,10 +234,10 @@ func handleTimeCheckEv(time) { The Schedule maintains the internal state used for scheduling blockRequestMessages based on some scheduling algorithm. The schedule needs to maintain state on: -* The state `blockState` of every block seem up to height of maxHeight -* The set of peers and their peer state `peerState` -* which peers have which blocks -* which blocks have been requested from which peers +- The state `blockState` of every block seem up to height of maxHeight +- The set of peers and their peer state `peerState` +- which peers have which blocks +- which blocks have been requested from which peers ```go type blockState int @@ -256,7 +258,7 @@ type schedule { // a map of peerID to schedule specific peer struct `scPeer` peers map[p2p.ID]scPeer - + // a map of heights to the peer we are waiting for a response from pending map[height]scPeer @@ -309,6 +311,7 @@ type scPeer struct { ``` # Scheduler + The scheduler is configured to maintain a target `n` of in flight messages and will use feedback from `_blockResponseMessage`, `_statusResponseMessage` and `_peerError` produce an optimal assignment @@ -342,7 +345,7 @@ func handleTimeCheckEv(time) { events = [] for peerID := range schedule.peersNotTouchedSince(time) { - pending = schedule.pendingFrom(peerID) + pending = schedule.pendingFrom(peerID) schedule.setPeerState(peerID, timedout) schedule.resetBlocks(pending) events = append(events, peerTimeout{peerID}) @@ -355,6 +358,7 @@ func handleTimeCheckEv(time) { ``` ## Peer + The Peer Stores per peer state based on messages received by the scheduler. ```go @@ -376,19 +380,19 @@ type Peer struct { This design is under active development. The Implementation has been staged in the following PRs: -* [Routine](https://github.com/tendermint/tendermint/pull/3878) -* [Processor](https://github.com/tendermint/tendermint/pull/4012) -* [Scheduler](https://github.com/tendermint/tendermint/pull/4043) -* [Reactor](https://github.com/tendermint/tendermint/pull/4067) +- [Routine](https://github.com/tendermint/tendermint/pull/3878) +- [Processor](https://github.com/tendermint/tendermint/pull/4012) +- [Scheduler](https://github.com/tendermint/tendermint/pull/4043) +- [Reactor](https://github.com/tendermint/tendermint/pull/4067) ## Consequences ### Positive -* Test become deterministic -* Simulation becomes a-termporal: no need wait for a wall-time timeout -* Peer Selection can be independently tested/simulated -* Develop a general approach to refactoring reactors +- Test become deterministic +- Simulation becomes a-termporal: no need wait for a wall-time timeout +- Peer Selection can be independently tested/simulated +- Develop a general approach to refactoring reactors ### Negative @@ -396,11 +400,11 @@ staged in the following PRs: ### Implementation Path -* Implement the scheduler, test the scheduler, review the rescheduler -* Implement the processor, test the processor, review the processor -* Implement the demuxer, write integration test, review integration tests +- Implement the scheduler, test the scheduler, review the rescheduler +- Implement the processor, test the processor, review the processor +- Implement the demuxer, write integration test, review integration tests ## References -* [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md): The original blockchain reactor re-org proposal -* [Blockchain re-org](https://github.com/tendermint/tendermint/pull/3561): The current blockchain reactor re-org implementation (v1) +- [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md): The original blockchain reactor re-org proposal +- [Blockchain re-org](https://github.com/tendermint/tendermint/pull/3561): The current blockchain reactor re-org implementation (v1) diff --git a/docs/tendermint-core/how-to-read-logs.md b/docs/tendermint-core/how-to-read-logs.md index 4031a178a..195a515a5 100644 --- a/docs/tendermint-core/how-to-read-logs.md +++ b/docs/tendermint-core/how-to-read-logs.md @@ -66,9 +66,8 @@ I[10-04|13:54:30.392] Started node module=main n Next follows a standard block creation cycle, where we enter a new round, propose a block, receive more than 2/3 of prevotes, then precommits and finally have a chance to commit a block. For details, -please refer to [Consensus -Overview](../introduction/introduction.md#consensus-overview) or [Byzantine Consensus -Algorithm](../spec/consensus/consensus.md). +please refer to [Byzantine Consensus +Algorithm](https://github.com/tendermint/spec/blob/master/spec/consensus/consensus.md). ``` I[10-04|13:54:30.393] enterNewRound(91/0). Current: 91/0/RoundStepNewHeight module=consensus diff --git a/docs/tendermint-core/running-in-production.md b/docs/tendermint-core/running-in-production.md index 02a0eae58..d386308de 100644 --- a/docs/tendermint-core/running-in-production.md +++ b/docs/tendermint-core/running-in-production.md @@ -106,7 +106,7 @@ Rate-limiting and authentication are another key aspects to help protect against DOS attacks. While in the future we may implement these features, for now, validators are supposed to use external tools like [NGINX](https://www.nginx.com/blog/rate-limiting-nginx/) or -[traefik](https://docs.traefik.io/configuration/commons/#rate-limiting) +[traefik](https://docs.traefik.io/middlewares/ratelimit/) to achieve the same things. ## Debugging Tendermint diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index b1098f54f..b33e770d7 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -156,10 +156,14 @@ and the `latest_app_hash` in particular: curl http://localhost:26657/status | json_pp | grep latest_app_hash ``` + + Visit http://localhost:26657 in your browser to see the list of other endpoints. Some take no arguments (like `/status`), while others specify the argument name and use `_` as a placeholder. + + ::: tip Find the RPC Documentation [here](https://docs.tendermint.com/master/rpc/) ::: From 7d001177e3b27d96f732a3627fe847c248b3b172 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2020 13:25:18 +0000 Subject: [PATCH 65/74] readme: add discord to readme (#4533) * readme: add discord to readme Signed-off-by: Marko Baricevic * add our id --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61391f51e..f3346a75c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Or [Blockchain](), for shor [![version](https://img.shields.io/github/tag/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/releases/latest) [![API Reference](https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667)](https://godoc.org/github.com/tendermint/tendermint) [![Go version](https://img.shields.io/badge/go-1.13-blue.svg)](https://github.com/moovweb/gvm) -[![riot.im](https://img.shields.io/badge/riot.im-JOIN%20CHAT-green.svg)](https://riot.im/app/#/room/#tendermint:matrix.org) +[![Discord](https://img.shields.io/discord/669268347736686612.svg)](https://discord.gg/AzefAFd) [![license](https://img.shields.io/github/license/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/blob/master/LICENSE) [![](https://tokei.rs/b1/github/tendermint/tendermint?category=lines)](https://github.com/tendermint/tendermint) From 431618cef6e09c24b81fba535e7cb2b7342c3af0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 6 Mar 2020 13:33:07 +0400 Subject: [PATCH 66/74] lite2: remove auto update (#4535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We first introduced auto-update as a separate struct AutoClient, which was wrapping Client and calling Update periodically. // AutoClient can auto update itself by fetching headers every N seconds. type AutoClient struct { base *Client updatePeriod time.Duration quit chan struct{} trustedHeaders chan *types.SignedHeader errs chan error } // NewAutoClient creates a new client and starts a polling goroutine. func NewAutoClient(base *Client, updatePeriod time.Duration) *AutoClient { c := &AutoClient{ base: base, updatePeriod: updatePeriod, quit: make(chan struct{}), trustedHeaders: make(chan *types.SignedHeader), errs: make(chan error), } go c.autoUpdate() return c } // TrustedHeaders returns a channel onto which new trusted headers are posted. func (c *AutoClient) TrustedHeaders() <-chan *types.SignedHeader { return c.trustedHeaders } // Err returns a channel onto which errors are posted. func (c *AutoClient) Errs() <-chan error { return c.errs } // Stop stops the client. func (c *AutoClient) Stop() { close(c.quit) } func (c *AutoClient) autoUpdate() { ticker := time.NewTicker(c.updatePeriod) defer ticker.Stop() for { select { case <-ticker.C: lastTrustedHeight, err := c.base.LastTrustedHeight() if err != nil { c.errs <- err continue } if lastTrustedHeight == -1 { // no headers yet => wait continue } newTrustedHeader, err := c.base.Update(time.Now()) if err != nil { c.errs <- err continue } if newTrustedHeader != nil { c.trustedHeaders <- newTrustedHeader } case <-c.quit: return } } } Later we merged it into the Client itself with the assumption that most clients will want it. But now I am not sure. Neither IBC nor cosmos/relayer are using it. It increases complexity (Start/Stop methods). That said, I think it makes sense to remove it until we see a need for it (until we better understand usage behavior). We can always introduce it later 😅. Maybe in the form of AutoClient. --- cmd/tendermint/commands/lite.go | 11 +--- .../adr-046-light-client-implementation.md | 17 ++---- lite2/client.go | 59 ------------------- lite2/client_test.go | 34 ----------- lite2/doc.go | 28 ++++----- lite2/example_test.go | 8 +-- lite2/rpc/client.go | 16 +---- 7 files changed, 21 insertions(+), 152 deletions(-) diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 447c6d92b..dae72266d 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -102,7 +102,7 @@ func runProxy(cmd *cobra.Command, args []string) error { db, err := dbm.NewGoLevelDB("lite-client-db", home) if err != nil { - return err + return errors.Wrap(err, "new goleveldb") } var c *lite.Client @@ -129,16 +129,9 @@ func runProxy(cmd *cobra.Command, args []string) error { lite.Logger(logger), ) } - if err != nil { - return errors.Wrap(err, "failed to create") + return err } - logger.Info("Starting client...") - err = c.Start() - if err != nil { - return errors.Wrap(err, "failed to start") - } - defer c.Stop() rpcClient, err := rpcclient.NewHTTP(primaryAddr, "/websocket") if err != nil { diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index 6058da8f8..aa7df3450 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -8,23 +8,13 @@ ## Context A `Client` struct represents a light client, connected to a single blockchain. -As soon as it's started (via `Start`), it tries to update to the latest header -(using bisection algorithm by default). -Cleaning routine is also started to remove headers outside of trusting period. -NOTE: since it's periodic, we still need to check header is not expired in -`TrustedHeader`, `TrustedValidatorSet` methods (and others which are using the -latest trusted header). - -The user has an option to manually verify headers using `VerifyHeader` and -`VerifyHeaderAtHeight` methods. To avoid races, `UpdatePeriod(0)` needs to be -passed when initializing the light client (it turns off the auto update). +The user has an option to verify headers using `VerifyHeader` or +`VerifyHeaderAtHeight` or `Update` methods. The latter method downloads the +latest header from primary and compares it with the currently trusted one. ```go type Client interface { - // start and stop updating & cleaning goroutines - Start() error - Stop() Cleanup() error // get trusted headers & validators @@ -41,6 +31,7 @@ type Client interface { // verify new headers VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error + Update(now time.Time) error } ``` diff --git a/lite2/client.go b/lite2/client.go index d65ed8ddb..b353f70d8 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -22,7 +22,6 @@ const ( sequential mode = iota + 1 skipping - defaultUpdatePeriod = 5 * time.Second defaultPruningSize = 1000 defaultMaxRetryAttempts = 10 ) @@ -55,13 +54,6 @@ func SkippingVerification(trustLevel tmmath.Fraction) Option { } } -// UpdatePeriod option can be used to change default polling period (5s). -func UpdatePeriod(d time.Duration) Option { - return func(c *Client) { - c.updatePeriod = d - } -} - // PruningSize option sets the maximum amount of headers & validator set pairs // that the light client stores. When Prune() is run, all headers (along with // the associated validator sets) that are earlier than the h amount of headers @@ -101,9 +93,6 @@ func MaxRetryAttempts(max uint16) Option { // headers from a primary provider, verifies them either sequentially or by // skipping some and stores them in a trusted store (usually, a local FS). // -// By default, the client will poll the primary provider for new headers every -// 5s (UpdatePeriod). If there are any, it will try to advance the state. -// // Default verification: SkippingVerification(DefaultTrustLevel) type Client struct { chainID string @@ -126,8 +115,6 @@ type Client struct { // Highest validator set from the store (height=H). latestTrustedVals *types.ValidatorSet - // See UpdatePeriod option - updatePeriod time.Duration // See RemoveNoLongerTrustedHeadersPeriod option pruningSize uint16 // See ConfirmationFunction option @@ -202,7 +189,6 @@ func NewClientFromTrustedStore( primary: primary, witnesses: witnesses, trustedStore: trustedStore, - updatePeriod: defaultUpdatePeriod, pruningSize: defaultPruningSize, confirmationFn: func(action string) bool { return true }, quit: make(chan struct{}), @@ -390,27 +376,6 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { return c.updateTrustedHeaderAndVals(h, vals) } -// Start starts two processes: 1) auto updating 2) removing outdated headers. -func (c *Client) Start() error { - c.logger.Info("Starting light client") - - if c.updatePeriod > 0 { - c.routinesWaitGroup.Add(1) - go c.autoUpdateRoutine() - } - - return nil -} - -// Stop stops two processes: 1) auto updating 2) removing outdated headers. -// Stop only returns after both of them are finished running. If you wish to -// remove all the data, call Cleanup. -func (c *Client) Stop() { - c.logger.Info("Stopping light client") - close(c.quit) - c.routinesWaitGroup.Wait() -} - // TrustedHeader returns a trusted header at the given height (0 - the latest). // // Headers along with validator sets, which can't be trusted anymore, are @@ -929,30 +894,6 @@ func (c *Client) removeWitness(idx int) { } } -func (c *Client) autoUpdateRoutine() { - defer c.routinesWaitGroup.Done() - - err := c.Update(time.Now()) - if err != nil { - c.logger.Error("Error during auto update", "err", err) - } - - ticker := time.NewTicker(c.updatePeriod) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - err := c.Update(time.Now()) - if err != nil { - c.logger.Error("Error during auto update", "err", err) - } - case <-c.quit: - return - } - } -} - // Update attempts to advance the state by downloading the latest header and // comparing it with the existing one. func (c *Client) Update(now time.Time) error { diff --git a/lite2/client_test.go b/lite2/client_test.go index 383bd4c60..e9023c3ad 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -154,7 +154,6 @@ func TestClient_SequentialVerification(t *testing.T) { )}, dbs.New(dbm.NewMemDB(), chainID), SequentialVerification(), - UpdatePeriod(0), ) if tc.initErr { @@ -163,9 +162,6 @@ func TestClient_SequentialVerification(t *testing.T) { } require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) if tc.verifyErr { @@ -281,7 +277,6 @@ func TestClient_SkippingVerification(t *testing.T) { )}, dbs.New(dbm.NewMemDB(), chainID), SkippingVerification(DefaultTrustLevel), - UpdatePeriod(0), ) if tc.initErr { require.Error(t, err) @@ -289,9 +284,6 @@ func TestClient_SkippingVerification(t *testing.T) { } require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() _, err = c.VerifyHeaderAtHeight(3, bTime.Add(3*time.Hour)) if tc.verifyErr { @@ -429,9 +421,6 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // Check we still have the 1st header (+header+). h, err := c.TrustedHeader(1) @@ -483,9 +472,6 @@ func TestClientRestoresTrustedHeaderAfterStartup2(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // Check we no longer have the invalid 1st header (+header+). h, err := c.TrustedHeader(1) @@ -520,9 +506,6 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // Check we still have the 1st header (+header+). h, err := c.TrustedHeader(1) @@ -584,9 +567,6 @@ func TestClientRestoresTrustedHeaderAfterStartup3(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // Check we have swapped invalid 1st header (+header+) with correct one (+header1+). h, err := c.TrustedHeader(1) @@ -622,9 +602,6 @@ func TestClient_Update(t *testing.T) { Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() // should result in downloading & verifying header #3 err = c.Update(bTime.Add(2 * time.Hour)) @@ -649,13 +626,9 @@ func TestClient_Concurrency(t *testing.T) { fullNode, []provider.Provider{fullNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), ) require.NoError(t, err) - err = c.Start() - require.NoError(t, err) - defer c.Stop() _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour)) require.NoError(t, err) @@ -697,7 +670,6 @@ func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { deadNode, []provider.Provider{fullNode, fullNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), MaxRetryAttempts(1), ) @@ -722,7 +694,6 @@ func TestClient_BackwardsVerification(t *testing.T) { fullNode, []provider.Provider{fullNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), ) require.NoError(t, err) @@ -755,7 +726,6 @@ func TestClient_BackwardsVerification(t *testing.T) { fullNode, []provider.Provider{fullNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), ) require.NoError(t, err) @@ -807,7 +777,6 @@ func TestClient_BackwardsVerification(t *testing.T) { tc.provider, []provider.Provider{tc.provider}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), ) require.NoError(t, err) @@ -854,7 +823,6 @@ func TestNewClientErrorsIfAllWitnessesUnavailable(t *testing.T) { fullNode, []provider.Provider{deadNode, deadNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), MaxRetryAttempts(1), ) @@ -898,7 +866,6 @@ func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) { fullNode, []provider.Provider{badProvider1, badProvider2}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), MaxRetryAttempts(1), ) @@ -926,7 +893,6 @@ func TestClientTrustedValidatorSet(t *testing.T) { fullNode, []provider.Provider{fullNode}, dbs.New(dbm.NewMemDB(), chainID), - UpdatePeriod(0), Logger(log.TestingLogger()), ) diff --git a/lite2/doc.go b/lite2/doc.go index aa280e64f..b61f5453f 100644 --- a/lite2/doc.go +++ b/lite2/doc.go @@ -65,33 +65,29 @@ Example usage: db, err := dbm.NewGoLevelDB("lite-client-db", dbDir) if err != nil { - // return err - t.Fatal(err) + // handle error } - c, err := NewClient( + + c, err := NewHTTPClient( chainID, TrustOptions{ Period: 504 * time.Hour, // 21 days Height: 100, Hash: header.Hash(), }, - httpp.New(chainID, "tcp://localhost:26657"), - []provider.Provider{httpp.New(chainID, "tcp://witness1:26657")}, - dbs.New(db, chainID), + "http://localhost:26657", + []string{"http://witness1:26657"}, + dbs.New(db, ""), ) - - err = c.Start() - if err != nil { - // return err - t.Fatal(err) - } - defer c.Stop() - - h, err := c.TrustedHeader(101) if err != nil { // handle error } - fmt.Println("got header", h) + + h, err := c.TrustedHeader(100) + if err != nil { + // handle error + } + fmt.Println("header", h) Check out other examples in example_test.go diff --git a/lite2/example_test.go b/lite2/example_test.go index e6839386f..34889ef98 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -58,18 +58,12 @@ func ExampleClient_Update() { primary, []provider.Provider{primary}, // NOTE: primary should not be used here dbs.New(db, chainID), - UpdatePeriod(0), // NOTE: value should be greater than zero // Logger(log.TestingLogger()), ) if err != nil { stdlog.Fatal(err) } - err = c.Start() - if err != nil { - stdlog.Fatal(err) - } defer func() { - c.Stop() c.Cleanup() }() @@ -78,6 +72,7 @@ func ExampleClient_Update() { // XXX: 30 * time.Minute clock drift is needed because a) Tendermint strips // monotonic component (see types/time/time.go) b) single instance is being // run. + // https://github.com/tendermint/tendermint/issues/4489 err = c.Update(time.Now().Add(30 * time.Minute)) if err != nil { stdlog.Fatal(err) @@ -137,7 +132,6 @@ func ExampleClient_VerifyHeaderAtHeight() { primary, []provider.Provider{primary}, // NOTE: primary should not be used here dbs.New(db, chainID), - UpdatePeriod(0), // Logger(log.TestingLogger()), ) if err != nil { diff --git a/lite2/rpc/client.go b/lite2/rpc/client.go index b59d459bc..abd15adc2 100644 --- a/lite2/rpc/client.go +++ b/lite2/rpc/client.go @@ -322,20 +322,8 @@ func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { } func (c *Client) updateLiteClientIfNeededTo(height int64) (*types.SignedHeader, error) { - lastTrustedHeight, err := c.lc.LastTrustedHeight() - if err != nil { - return nil, errors.Wrap(err, "LastTrustedHeight") - } - - if lastTrustedHeight < height { - return c.lc.VerifyHeaderAtHeight(height, time.Now()) - } - - h, err := c.lc.TrustedHeader(height) - if err != nil { - return nil, errors.Wrapf(err, "TrustedHeader(#%d)", height) - } - return h, nil + h, err := c.lc.VerifyHeaderAtHeight(height, time.Now()) + return h, errors.Wrapf(err, "failed to update light client to %d", height) } func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) { From d3f965ba68fa39af56793bc78a9d4bcdd6c57551 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 6 Mar 2020 17:10:28 +0400 Subject: [PATCH 67/74] lite2: indicate success/failure of Update (#4536) error itself is not enough since it only signals if there were any errors. Either (types.SignedHeader) or (success bool) is needed to indicate the status of the operation. Returning a header is optimal since most of the clients will want to get a newly verified header anyway. --- docs/architecture/adr-042-state-sync.md | 9 ++++----- .../adr-046-light-client-implementation.md | 10 +++++----- lite2/client.go | 17 +++++++++-------- lite2/client_test.go | 12 +++++------- lite2/example_test.go | 9 ++------- 5 files changed, 25 insertions(+), 32 deletions(-) diff --git a/docs/architecture/adr-042-state-sync.md b/docs/architecture/adr-042-state-sync.md index d525a4974..89d95f2e4 100644 --- a/docs/architecture/adr-042-state-sync.md +++ b/docs/architecture/adr-042-state-sync.md @@ -34,7 +34,7 @@ across different criteria: ### Implementation Question * What is the format of a snapshot - * Complete snapshot + * Complete snapshot * Ordered IAVL key ranges * Compressed individually chunks which can be validated * How is data validated @@ -58,7 +58,7 @@ request time. This solution would create an auxiliary data structure optimized for batch read/writes. Additionally the propsosals tend to vary on how they provide safety -properties. +properties. **LightClient** Where a client can aquire the merkle root from the block headers synchronized from a trusted validator set. Subsets of the application state, @@ -70,7 +70,7 @@ downloaded and compared against versions provided by a majority of peers. #### Lazy StateSync -An [initial specification](https://docs.google.com/document/d/15MFsQtNA0MGBv7F096FFWRDzQ1vR6_dics5Y49vF8JU/edit?ts=5a0f3629) was published by Alexis Sellier. +An initial specification was published by Alexis Sellier. In this design, the state has a given `size` of primitive elements (like keys or nodes), each element is assigned a number from 0 to `size-1`, and chunks consists of a range of such elements. Ackratos raised @@ -104,7 +104,7 @@ chunks and snappy compressed. Hashes of snappy compressed chunks are stored in a manifest file which co-ordinates the state-sync. Obtaining a correct manifest file seems to require an honest majority of peers. This means you may not find out the state is incorrect until you download the whole thing and compare it -with a verified block header. +with a verified block header. A similar solution was implemented by Binance in [#3594](https://github.com/tendermint/tendermint/pull/3594) @@ -229,7 +229,6 @@ Proposed ## References [sync: Sync current state without full replay for Applications](https://github.com/tendermint/tendermint/issues/828) - original issue -[tendermint state sync proposal](https://docs.google.com/document/d/15MFsQtNA0MGBv7F096FFWRDzQ1vR6_dics5Y49vF8JU/edit?ts=5a0f3629) - Cloudhead proposal [tendermint state sync proposal 2](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit) - ackratos proposal [proposal 2 implementation](https://github.com/tendermint/tendermint/pull/3243) - ackratos implementation [WIP General/Lazy State-Sync pseudo-spec](https://github.com/tendermint/tendermint/issues/3639) - Jae Proposal diff --git a/docs/architecture/adr-046-light-client-implementation.md b/docs/architecture/adr-046-light-client-implementation.md index aa7df3450..37a7c83c5 100644 --- a/docs/architecture/adr-046-light-client-implementation.md +++ b/docs/architecture/adr-046-light-client-implementation.md @@ -15,7 +15,10 @@ latest header from primary and compares it with the currently trusted one. ```go type Client interface { - Cleanup() error + // verify new headers + VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) + VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error + Update(now time.Time) (*types.SignedHeader, error) // get trusted headers & validators TrustedHeader(height int64) (*types.SignedHeader, error) @@ -28,10 +31,7 @@ type Client interface { Primary() provider.Provider Witnesses() []provider.Provider - // verify new headers - VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) - VerifyHeader(newHeader *types.SignedHeader, newVals *types.ValidatorSet, now time.Time) error - Update(now time.Time) error + Cleanup() error } ``` diff --git a/lite2/client.go b/lite2/client.go index b353f70d8..2eefd472d 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -895,33 +895,34 @@ func (c *Client) removeWitness(idx int) { } // Update attempts to advance the state by downloading the latest header and -// comparing it with the existing one. -func (c *Client) Update(now time.Time) error { +// comparing it with the existing one. It returns a new header on a successful +// update. Otherwise, it returns nil (plus an error, if any). +func (c *Client) Update(now time.Time) (*types.SignedHeader, error) { lastTrustedHeight, err := c.LastTrustedHeight() if err != nil { - return errors.Wrap(err, "can't get last trusted height") + return nil, errors.Wrap(err, "can't get last trusted height") } if lastTrustedHeight == -1 { // no headers yet => wait - return nil + return nil, nil } latestHeader, latestVals, err := c.fetchHeaderAndValsAtHeight(0) if err != nil { - return errors.Wrapf(err, "can't get latest header and vals") + return nil, errors.Wrapf(err, "can't get latest header and vals") } if latestHeader.Height > lastTrustedHeight { err = c.VerifyHeader(latestHeader, latestVals, now) if err != nil { - return err + return nil, err } - c.logger.Info("Advanced to new state", "height", latestHeader.Height, "hash", hash2str(latestHeader.Hash())) + return latestHeader, nil } - return nil + return nil, nil } // replaceProvider takes the first alternative provider and promotes it as the diff --git a/lite2/client_test.go b/lite2/client_test.go index e9023c3ad..65ea55122 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -604,13 +604,11 @@ func TestClient_Update(t *testing.T) { require.NoError(t, err) // should result in downloading & verifying header #3 - err = c.Update(bTime.Add(2 * time.Hour)) - require.NoError(t, err) - - h, err := c.TrustedHeader(3) + h, err := c.Update(bTime.Add(2 * time.Hour)) assert.NoError(t, err) - require.NotNil(t, h) - assert.EqualValues(t, 3, h.Height) + if assert.NotNil(t, h) { + assert.EqualValues(t, 3, h.Height) + } valSet, _, err := c.TrustedValidatorSet(3) assert.NoError(t, err) @@ -675,7 +673,7 @@ func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) { ) require.NoError(t, err) - err = c.Update(bTime.Add(2 * time.Hour)) + _, err = c.Update(bTime.Add(2 * time.Hour)) require.NoError(t, err) assert.NotEqual(t, c.Primary(), deadNode) diff --git a/lite2/example_test.go b/lite2/example_test.go index 34889ef98..e8c3b8bb3 100644 --- a/lite2/example_test.go +++ b/lite2/example_test.go @@ -73,17 +73,12 @@ func ExampleClient_Update() { // monotonic component (see types/time/time.go) b) single instance is being // run. // https://github.com/tendermint/tendermint/issues/4489 - err = c.Update(time.Now().Add(30 * time.Minute)) + h, err := c.Update(time.Now().Add(30 * time.Minute)) if err != nil { stdlog.Fatal(err) } - h, err := c.TrustedHeader(0) - if err != nil { - stdlog.Fatal(err) - } - - if h.Height > 2 { + if h != nil && h.Height > 2 { fmt.Println("successful update") } else { fmt.Println("update failed") From bc89aad16280a4b2429f3dd51b3a836042dbed66 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 6 Mar 2020 17:23:44 +0400 Subject: [PATCH 68/74] rpc: create buffered subscriptions on /subscribe (#4521) Closes #3935 --- CHANGELOG_PENDING.md | 1 + rpc/core/README.md | 11 +++++++++++ rpc/core/events.go | 7 ++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 47ff5dfb1..31fa4dfe7 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -24,6 +24,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi ### BUG FIXES: +- [rpc] \#3935 Create buffered subscriptions on `/subscribe` (@melekes) - [rpc] [\#4493](https://github.com/tendermint/tendermint/pull/4493) Keep the original subscription "id" field when new RPCs come in (@michaelfig) - [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) - [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) diff --git a/rpc/core/README.md b/rpc/core/README.md index d767c5f71..f62d2dbf4 100644 --- a/rpc/core/README.md +++ b/rpc/core/README.md @@ -5,3 +5,14 @@ Requests that return multiple items will be paginated to 30 items by default. You can specify further pages with the ?page parameter. You can also set a custom page size up to 100 with the ?per_page parameter. + +## Subscribing to events + +The user can subscribe to events emitted by Tendermint, using `/subscribe`. If +the maximum number of clients is reached or the client has too many +subscriptions, an error will be returned. The subscription timeout is 5 sec. +Each subscription has a buffer to accommodate short bursts of events or some +slowness in clients. If the buffer gets full, the subscription will be canceled +("client is not pulling messages fast enough"). If Tendermint exits, all +subscriptions are canceled ("Tendermint exited"). The user can unsubscribe +using either `/unsubscribe` or `/unsubscribe_all`. diff --git a/rpc/core/events.go b/rpc/core/events.go index 165aa6e54..7802f160e 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -12,6 +12,11 @@ import ( rpctypes "github.com/tendermint/tendermint/rpc/lib/types" ) +const ( + // Buffer on the Tendermint (server) side to allow some slowness in clients. + subBufferSize = 100 +) + // Subscribe for events via WebSocket. // More: https://docs.tendermint.com/master/rpc/#/Websocket/subscribe func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) { @@ -33,7 +38,7 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout) defer cancel() - sub, err := eventBus.Subscribe(subCtx, addr, q) + sub, err := eventBus.Subscribe(subCtx, addr, q, subBufferSize) if err != nil { return nil, err } From b6f0aa3a8874877771415d40f49798b16e86c3e5 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Fri, 6 Mar 2020 20:05:20 +0100 Subject: [PATCH 69/74] lite2: replace primary when providing invalid header (#4523) Closes: #4420 Created a new error ErrInvalidHeaderwhich can be formed during the verification process verifier.go and will result in the replacement of the primary provider with a witness by executing: replacePrimaryProvider() Co-authored-by: Anton Kaliaev --- go.sum | 10 ------- lite2/client.go | 67 ++++++++++++++++++++++++++++++------------ lite2/errors.go | 10 +++++++ lite2/verifier.go | 61 ++++++++++++++++++++++++++++---------- lite2/verifier_test.go | 4 +-- 5 files changed, 106 insertions(+), 46 deletions(-) diff --git a/go.sum b/go.sum index 886eadee3..21cbdfc57 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/Workiva/go-datastructures v1.0.50 h1:slDmfW6KCHcC7U+LP3DDBbm4fqTwZGn1beOFPfGaLvo= -github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/Workiva/go-datastructures v1.0.51 h1:LJHjjfcv+1gH+1D1SgrjcrF8iSZkgsAdCjclvHvVecQ= github.com/Workiva/go-datastructures v1.0.51/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -132,8 +130,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -386,22 +382,16 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= -github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= -github.com/tecbot/gorocksdb v0.0.0-20191017175515-d217d93fd4c5 h1:gVwAW5OwaZlDB5/CfqcGFM9p9C+KxvQKyNOltQ8orj0= -github.com/tecbot/gorocksdb v0.0.0-20191017175515-d217d93fd4c5/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= -github.com/tendermint/tm-db v0.4.0 h1:iPbCcLbf4nwDFhS39Zo1lpdS1X/cT9CkTlUx17FHQgA= -github.com/tendermint/tm-db v0.4.0/go.mod h1:+Cwhgowrf7NBGXmsqFMbwEtbo80XmyrlY5Jsk95JubQ= github.com/tendermint/tm-db v0.4.1 h1:TvX7JWjJOVZ+N3y+I86wddrGttOdMmmBxXcu0/Y7ZJ0= github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/lite2/client.go b/lite2/client.go index 2eefd472d..22c080949 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -476,9 +476,10 @@ func (c *Client) ChainID() string { // and calls VerifyHeader. It returns header immediately if such exists in // trustedStore (no verification is needed). // +// height must be > 0. +// // It returns provider.ErrSignedHeaderNotFound if header is not found by // primary. -// It returns ErrOldHeaderExpired if header expired. func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.SignedHeader, error) { if height <= 0 { return nil, errors.New("negative or zero height") @@ -515,7 +516,10 @@ func (c *Client) VerifyHeaderAtHeight(height int64, now time.Time) (*types.Signe // intermediate headers will be requested. See the specification for details. // https://github.com/tendermint/spec/blob/master/spec/consensus/light-client.md // -// It returns ErrOldHeaderExpired if newHeader expired. +// It returns ErrOldHeaderExpired if the latest trusted header expired. +// +// If the primary provides an invalid header (ErrInvalidHeader), it is rejected +// and replaced by another provider until all are exhausted. // // If, at any moment, SignedHeader or ValidatorSet are not found by the primary // provider, provider.ErrSignedHeaderNotFound / @@ -676,7 +680,23 @@ func (c *Client) sequence( err = VerifyAdjacent(c.chainID, trustedHeader, interimHeader, interimVals, c.trustingPeriod, now) if err != nil { - return errors.Wrapf(err, "failed to verify the header #%d", height) + err = errors.Wrapf(err, "verify adjacent from #%d to #%d failed", + trustedHeader.Height, interimHeader.Height) + + switch errors.Cause(err).(type) { + case ErrInvalidHeader: + c.logger.Error("primary sent invalid header -> replacing", "err", err) + replaceErr := c.replacePrimaryProvider() + if replaceErr != nil { + c.logger.Error("Can't replace primary", "err", replaceErr) + return err // return original error + } + // attempt to verify header again + height-- + continue + default: + return err + } } // 3) Update trustedHeader @@ -729,8 +749,21 @@ func (c *Client) bisection( return err } + case ErrInvalidHeader: + c.logger.Error("primary sent invalid header -> replacing", "err", err) + replaceErr := c.replacePrimaryProvider() + if replaceErr != nil { + c.logger.Error("Can't replace primary", "err", replaceErr) + // return original error + return errors.Wrapf(err, "verify from #%d to #%d failed", + trustedHeader.Height, interimHeader.Height) + } + // attempt to verify the header again + continue + default: - return errors.Wrapf(err, "failed to verify the header #%d", newHeader.Height) + return errors.Wrapf(err, "verify from #%d to #%d failed", + trustedHeader.Height, interimHeader.Height) } } } @@ -772,7 +805,9 @@ func (c *Client) fetchHeaderAndValsAtHeight(height int64) (*types.SignedHeader, return h, vals, nil } -// Backwards verification (see VerifyHeaderBackwards func in the spec) +// backwards verification (see VerifyHeaderBackwards func in the spec) verifies +// headers before a trusted header. If a sent header is invalid the primary is +// replaced with another provider and the operation is repeated. func (c *Client) backwards( initiallyTrustedHeader *types.SignedHeader, newHeader *types.SignedHeader, @@ -794,20 +829,14 @@ func (c *Client) backwards( return errors.Wrapf(err, "failed to obtain the header at height #%d", trustedHeader.Height-1) } - if err := interimHeader.ValidateBasic(c.chainID); err != nil { - return errors.Wrap(err, "untrustedHeader.ValidateBasic failed") - } - - if !interimHeader.Time.Before(trustedHeader.Time) { - return errors.Errorf("expected older header time %v to be before newer header time %v", - interimHeader.Time, - trustedHeader.Time) - } - - if !bytes.Equal(interimHeader.Hash(), trustedHeader.LastBlockID.Hash) { - return errors.Errorf("older header hash %X does not match trusted header's last block %X", - interimHeader.Hash(), - trustedHeader.LastBlockID.Hash) + if err := VerifyBackwards(c.chainID, interimHeader, trustedHeader); err != nil { + c.logger.Error("primary sent invalid header -> replacing", "err", err) + if replaceErr := c.replacePrimaryProvider(); replaceErr != nil { + c.logger.Error("Can't replace primary", "err", replaceErr) + // return original error + return errors.Wrapf(err, "verify backwards from %d to %d failed", + trustedHeader.Height, interimHeader.Height) + } } trustedHeader = interimHeader diff --git a/lite2/errors.go b/lite2/errors.go index d0b5d2d31..13a6cf29d 100644 --- a/lite2/errors.go +++ b/lite2/errors.go @@ -28,3 +28,13 @@ type ErrNewValSetCantBeTrusted struct { func (e ErrNewValSetCantBeTrusted) Error() string { return fmt.Sprintf("cant trust new val set: %v", e.Reason) } + +// ErrInvalidHeader means the header either failed the basic validation or +// commit is not signed by 2/3+. +type ErrInvalidHeader struct { + Reason error +} + +func (e ErrInvalidHeader) Error() string { + return fmt.Sprintf("invalid header: %v", e.Reason) +} diff --git a/lite2/verifier.go b/lite2/verifier.go index 250c8b204..6d8459ab6 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -23,12 +23,12 @@ var ( // VerifyNonAdjacent verifies non-adjacent untrustedHeader against // trustedHeader. It ensures that: // -// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); -// b) untrustedHeader is valid; +// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned) +// b) untrustedHeader is valid (if not, ErrInvalidHeader is returned) // c) trustLevel ([1/3, 1]) of trustedHeaderVals (or trustedHeaderNextVals) -// signed correctly (if not, ErrNewValSetCantBeTrusted is returned); -// d) more than 2/3 of untrustedVals have signed h2 (if not, -// ErrNotEnoughVotingPowerSigned is returned); +// signed correctly (if not, ErrNewValSetCantBeTrusted is returned) +// d) more than 2/3 of untrustedVals have signed h2 +// (otherwise, ErrInvalidHeader is returned) // e) headers are non-adjacent. func VerifyNonAdjacent( chainID string, @@ -49,7 +49,7 @@ func VerifyNonAdjacent( } if err := verifyNewHeaderAndVals(chainID, untrustedHeader, untrustedVals, trustedHeader, now); err != nil { - return err + return ErrInvalidHeader{err} } // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. @@ -67,11 +67,11 @@ func VerifyNonAdjacent( // Ensure that +2/3 of new validators signed correctly. // // NOTE: this should always be the last check because untrustedVals can be - // intentionaly made very large to DOS the light client. not the case for + // intentionally made very large to DOS the light client. not the case for // VerifyAdjacent, where validator set is known in advance. if err := untrustedVals.VerifyCommit(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, untrustedHeader.Commit); err != nil { - return err + return ErrInvalidHeader{err} } return nil @@ -80,11 +80,11 @@ func VerifyNonAdjacent( // VerifyAdjacent verifies directly adjacent untrustedHeader against // trustedHeader. It ensures that: // -// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned); -// b) untrustedHeader is valid; -// c) untrustedHeader.ValidatorsHash equals trustedHeader.NextValidatorsHash; -// d) more than 2/3 of new validators (untrustedVals) have signed h2 (if not, -// ErrNotEnoughVotingPowerSigned is returned); +// a) trustedHeader can still be trusted (if not, ErrOldHeaderExpired is returned) +// b) untrustedHeader is valid (if not, ErrInvalidHeader is returned) +// c) untrustedHeader.ValidatorsHash equals trustedHeader.NextValidatorsHash +// d) more than 2/3 of new validators (untrustedVals) have signed h2 +// (otherwise, ErrInvalidHeader is returned) // e) headers are adjacent. func VerifyAdjacent( chainID string, @@ -103,7 +103,7 @@ func VerifyAdjacent( } if err := verifyNewHeaderAndVals(chainID, untrustedHeader, untrustedVals, trustedHeader, now); err != nil { - return err + return ErrInvalidHeader{err} } // Check the validator hashes are the same @@ -118,7 +118,7 @@ func VerifyAdjacent( // Ensure that +2/3 of new validators signed correctly. if err := untrustedVals.VerifyCommit(chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, untrustedHeader.Commit); err != nil { - return err + return ErrInvalidHeader{err} } return nil @@ -200,3 +200,34 @@ func HeaderExpired(h *types.SignedHeader, trustingPeriod time.Duration, now time expirationTime := h.Time.Add(trustingPeriod) return !expirationTime.After(now) } + +// VerifyBackwards verifies an untrusted header with a height one less than +// that of an adjacent trusted header. It ensures that: +// +// a) untrusted header is valid +// b) untrusted header has a time before the trusted header +// c) that the LastBlockID hash of the trusted header is the same as the hash +// of the trusted header +// +// For any of these cases ErrInvalidHeader is returned. +func VerifyBackwards(chainID string, untrustedHeader, trustedHeader *types.SignedHeader) error { + if err := untrustedHeader.ValidateBasic(chainID); err != nil { + return ErrInvalidHeader{err} + } + + if !untrustedHeader.Time.Before(trustedHeader.Time) { + return ErrInvalidHeader{ + errors.Errorf("expected older header time %v to be before new header time %v", + untrustedHeader.Time, + trustedHeader.Time)} + } + + if !bytes.Equal(untrustedHeader.Hash(), trustedHeader.LastBlockID.Hash) { + return ErrInvalidHeader{ + errors.Errorf("older header hash %X does not match trusted header's last block %X", + untrustedHeader.Hash(), + trustedHeader.LastBlockID.Hash)} + } + + return nil +} diff --git a/lite2/verifier_test.go b/lite2/verifier_test.go index 241d33b05..adc671516 100644 --- a/lite2/verifier_test.go +++ b/lite2/verifier_test.go @@ -113,7 +113,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { vals, 3 * time.Hour, bTime.Add(2 * time.Hour), - types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}, + ErrInvalidHeader{Reason: types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}}, "", }, // vals does not match with what we have -> error @@ -227,7 +227,7 @@ func TestVerifyNonAdjacentHeaders(t *testing.T) { vals, 3 * time.Hour, bTime.Add(2 * time.Hour), - types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}, + ErrInvalidHeader{types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}}, "", }, // 3/3 new vals signed, 2/3 old vals present -> no error From f17717f3a3044d70ac36059a230e7e1eb22e9b9e Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 9 Mar 2020 09:44:37 +0100 Subject: [PATCH 70/74] adr: crypto encoding for proto (#4481) * adr: crypto encoding for proto work - this adr is meant to help with deciding on how to move forward with keys in tendermint. * minor change * fix gomod * add a third option * fix spelling * add first part of descision * breakdown keys and where they are used * add some wording * minor wording fix * question * change proto messages * minor update * undo go.mod changes * add a few things based on comemnts * push, push it real good * minor explanation on interface type * touch up --- docs/architecture/README.md | 2 + .../architecture/adr-054-crypto-encoding-2.md | 83 +++++++++++++++++++ docs/architecture/adr-template.md | 16 ++-- go.sum | 3 + 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 docs/architecture/adr-054-crypto-encoding-2.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index b571564d5..9f754fd37 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -70,3 +70,5 @@ Note the context/background should be written in the present tense. - [ADR-047-Handling-Evidence-From-Light-Client](./adr-047-handling-evidence-from-light-client.md) - [ADR-051-Double-Signing-Risk-Reduction](./adr-051-double-signing-risk-reduction.md) - [ADR-052-Tendermint-Mode](./adr-052-tendermint-mode.md) +- [ADR-053-State-Sync-Prototype](./adr-053-state-sync-prototype.md) +- [ADR-054-crypto-encoding-2](./adr-054-crypto-encoding-2.md) diff --git a/docs/architecture/adr-054-crypto-encoding-2.md b/docs/architecture/adr-054-crypto-encoding-2.md new file mode 100644 index 000000000..9ec05f229 --- /dev/null +++ b/docs/architecture/adr-054-crypto-encoding-2.md @@ -0,0 +1,83 @@ +# ADR 054: Crypto encoding (part 2) + +## Changelog + +\*2020-2-27: Created + +## Context + +Amino has been a pain point of many users in the ecosystem. While Tendermint does not suffer greatly from the performance degradation introduced by amino, we are making an effort in moving the encoding format to a widely adopted format, [Protocol Buffers](https://developers.google.com/protocol-buffers). With this migration a new standard is needed for the encoding of keys. This will cause ecosystem wide breaking changes. + +Currently amino encodes keys as ` `. + +## Decision + +When using the `oneof` protobuf type there are many times where one will have to manually switch over the possible messages and then pass them to the interface which is needed. By transitioning from a fixed size byte array (`[size]byte`) to byte slice's (`[]byte`) then this would enable the usage of the [cosmos-proto's](hhttps://github.com/regen-network/cosmos-proto#interface_type) interface type, which will generate these switch statements. + +The approach that will be taken to minimize headaches for users is one where all encoding of keys will shift to protobuf and where amino encoding is relied on, there will be custom marshal and unmarshal functions. + +Protobuf messages: + +```proto +message PubKey { + option (cosmos_proto.interface_type) = "*github.com/tendermint/tendermint/crypto.PubKey"; + oneof key { + bytes ed25519 = 1 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/ed25519.PubKey"]; + bytes secp256k1 = 2 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/secp256k1.PubKey"]; + bytes sr25519 = 3 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/sr25519.PubKey"]; + PubKeyMultiSigThreshold multisig = 4 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/multisig.PubKeyMultisigThreshold"];; + } + +message PrivKey { + option (cosmos_proto.interface_type) = "github.com/tendermint/tendermint/crypto.PrivKey"; + oneof sum { + bytes ed25519 = 1 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/ed25519.PrivKey"]; + bytes secp256k1 = 2 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/secp256k1.PrivKey"]; + bytes sr25519 = 3 + [(gogoproto.casttype) = "github.com/tendermint/tendermint/crypto/sr25519.PrivKey"];; + } +} +``` + +> Note: The places where backwards compatibility is needed is still unclear. + +All modules currently do not rely on amino encoded bytes and keys are not amino encoded for genesis, therefore a hardfork upgrade is what will be needed to adopt these changes. + +This work will be broken out into a few PRs, this work will be merged into a proto-breakage branch, all PRs will be reviewed prior to being merged: + +1. Encoding of keys to protobuf and protobuf messages +2. Move Tendermint types to protobuf, mainly the ones that are being encoded. +3. Go one by one through the reactors and transition amino encoded messages to protobuf. +4. Test with cosmos-sdk and/or testnets repo. + +## Status + +Proposed + +## Consequences + +- Move keys to protobuf encoding, where backwards compatibility is needed, amino marshal and unmarshal functions will be used. + +### Positive + +- Protocol Buffer encoding will not change going forward. +- Removing amino overhead from keys will help with the KSM. +- Have a large ecosystem of supported languages. + +### Negative + +- Hardfork is required to integrate this into running chains. + +### Neutral + +## References + +> Are there any relevant PR comments, issues that led up to this, or articles referenced for why we made the given design choice? If so link them here! + +- {reference link} diff --git a/docs/architecture/adr-template.md b/docs/architecture/adr-template.md index 28a5ecfbb..759fc6d72 100644 --- a/docs/architecture/adr-template.md +++ b/docs/architecture/adr-template.md @@ -1,17 +1,19 @@ # ADR {ADR-NUMBER}: {TITLE} ## Changelog -* {date}: {changelog} + +- {date}: {changelog} ## Context -> This section contains all the context one needs to understand the current state, and why there is a problem. It should be as succinct as possible and introduce the high level idea behind the solution. +> This section contains all the context one needs to understand the current state, and why there is a problem. It should be as succinct as possible and introduce the high level idea behind the solution. + ## Decision > This section explains all of the details of the proposed solution, including implementation details. -It should also describe affects / corollary items that may need to be changed as a part of this. -If the proposed change will be large, please also indicate a way to do the change to maximize ease of review. -(e.g. the optimal split of things to do between separate PR's) +> It should also describe affects / corollary items that may need to be changed as a part of this. +> If the proposed change will be large, please also indicate a way to do the change to maximize ease of review. +> (e.g. the optimal split of things to do between separate PR's) ## Status @@ -31,6 +33,6 @@ If the proposed change will be large, please also indicate a way to do the chang ## References -> Are there any relevant PR comments, issues that led up to this, or articles referrenced for why we made the given design choice? If so link them here! +> Are there any relevant PR comments, issues that led up to this, or articles referenced for why we made the given design choice? If so link them here! -* {reference link} +- {reference link} diff --git a/go.sum b/go.sum index 21cbdfc57..1fa4c0054 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,7 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= @@ -152,6 +153,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -238,6 +240,7 @@ github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= From 73c19bd6ac367e1a4a473f56fea4cfdbe0a37039 Mon Sep 17 00:00:00 2001 From: Tess Rinearson Date: Mon, 9 Mar 2020 18:52:08 +0100 Subject: [PATCH 71/74] CONTRIBUTING: include instructions for installing protobuf --- CONTRIBUTING.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 623cfb53a..6e6897ffa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,25 @@ specify exactly the dependency you want to update, eg. ## Protobuf -When working with [protobuf](https://developers.google.com/protocol-buffers) there are a few things you should know. We use [buf](https://buf.build/) for our linting and breaking changes checking. If you would like to run linting and check if the changes you have made are breaking then you will have to install the needed dependencies with `make buf`. Then the linting cmd will be `make proto-lint` and the breaking changes check will be `make proto-check-breaking`. To generate new stubs based off of your changes you can run `make proto-gen` (you can do this outside of GOPATH). +We use [Protocol Buffers](https://developers.google.com/protocol-buffers) along with [gogoproto](https://github.com/gogo/protobuf) to generate code for use across Tendermint Core. + +For linting and checking breaking changes, we use [buf](https://buf.build/). If you would like to run linting and check if the changes you have made are breaking then you will have to install the needed dependencies with `make buf`. Then the linting cmd will be `make proto-lint` and the breaking changes check will be `make proto-check-breaking`. + +To generate new stubs based off of your changes you can run `make proto-gen` after installing `protoc` and gogoproto. + +### Installation Instructions + +To install `protoc`, download an appropriate release (https://github.com/protocolbuffers/protobuf) and then move the provided binaries into your PATH (follow instructions in README included with the download). + +To install `gogoproto`, do the following: + +```sh +$ go get github.com/gogo/protobuf/gogoproto +$ cd $GOPATH/pkg/mod/github.com/gogo/protobuf@v1.3.1 # or wherever go get installs things +$ make install +``` + +You should now be able to run `make proto-gen` from inside the root Tendermint directory to generate new files from proto files. ## Vagrant From cb8a2cc67f5d7f34d3d83634b8c1760225256e42 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2020 15:29:31 +0000 Subject: [PATCH 72/74] build(deps): bump github.com/Workiva/go-datastructures (#4545) Bumps [github.com/Workiva/go-datastructures](https://github.com/Workiva/go-datastructures) from 1.0.51 to 1.0.52. - [Release notes](https://github.com/Workiva/go-datastructures/releases) - [Commits](https://github.com/Workiva/go-datastructures/compare/v1.0.51...v1.0.52) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5af1190cf..9e916e7e8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/ChainSafe/go-schnorrkel v0.0.0-20200102211924-4bcbc698314f - github.com/Workiva/go-datastructures v1.0.51 + github.com/Workiva/go-datastructures v1.0.52 github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a github.com/fortytw2/leaktest v1.3.0 diff --git a/go.sum b/go.sum index 1fa4c0054..6b5a1cf49 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.51 h1:LJHjjfcv+1gH+1D1SgrjcrF8iSZkgsAdCjclvHvVecQ= github.com/Workiva/go-datastructures v1.0.51/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= +github.com/Workiva/go-datastructures v1.0.52 h1:PLSK6pwn8mYdaoaCZEMsXBpBotr4HHn9abU0yMQt0NI= +github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= From 31bea92d127a40a14174611c27bb7e6dd66ca3c9 Mon Sep 17 00:00:00 2001 From: Tess Rinearson Date: Tue, 10 Mar 2020 21:34:44 +0100 Subject: [PATCH 73/74] libs/kv: remove unused type KI64Pair (#4542) --- CHANGELOG_PENDING.md | 1 + libs/kv/kvpair.go | 29 ---- libs/kv/result.go | 11 -- libs/kv/types.pb.go | 296 ++-------------------------------------- libs/kv/types.proto | 7 - libs/kv/typespb_test.go | 124 ----------------- 6 files changed, 13 insertions(+), 455 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 31fa4dfe7..aa0c0c41f 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -21,6 +21,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) VerifyCommitX() functions should return as soon as +2/3 threashold is reached. - [examples/kvstore] [\#4509](https://github.com/tendermint/tendermint/pull/4509) ABCI query now returns the proper height (@erikgrinaker) - [cmd] \#4515 Change `tendermint debug dump` sub-command archives filename's format (@melekes) +- [libs/kv]: [\#4542](https://github.com/tendermint/tendermint/pull/4542) remove unused type KI64Pair (@tessr) ### BUG FIXES: diff --git a/libs/kv/kvpair.go b/libs/kv/kvpair.go index 3007c0272..8eebae606 100644 --- a/libs/kv/kvpair.go +++ b/libs/kv/kvpair.go @@ -36,32 +36,3 @@ func (kvs Pairs) Less(i, j int) bool { func (kvs Pairs) Swap(i, j int) { kvs[i], kvs[j] = kvs[j], kvs[i] } func (kvs Pairs) Sort() { sort.Sort(kvs) } -//---------------------------------------- -// KI64Pair - -/* -Defined in types.proto -type KI64Pair struct { - Key []byte - Value int64 -} -*/ - -type KI64Pairs []KI64Pair - -// Sorting -func (kvs KI64Pairs) Len() int { return len(kvs) } -func (kvs KI64Pairs) Less(i, j int) bool { - switch bytes.Compare(kvs[i].Key, kvs[j].Key) { - case -1: - return true - case 0: - return kvs[i].Value < kvs[j].Value - case 1: - return false - default: - panic("invalid comparison result") - } -} -func (kvs KI64Pairs) Swap(i, j int) { kvs[i], kvs[j] = kvs[j], kvs[i] } -func (kvs KI64Pairs) Sort() { sort.Sort(kvs) } diff --git a/libs/kv/result.go b/libs/kv/result.go index b2d855cbc..fd40450b1 100644 --- a/libs/kv/result.go +++ b/libs/kv/result.go @@ -30,16 +30,6 @@ func (r *Pair) UnmarshalJSON(b []byte) error { return jsonpbUnmarshaller.Unmarshal(reader, r) } -func (r *KI64Pair) MarshalJSON() ([]byte, error) { - s, err := jsonpbMarshaller.MarshalToString(r) - return []byte(s), err -} - -func (r *KI64Pair) UnmarshalJSON(b []byte) error { - reader := bytes.NewBuffer(b) - return jsonpbUnmarshaller.Unmarshal(reader, r) -} - // Some compile time assertions to ensure we don't // have accidental runtime surprises later on. // jsonEncodingRoundTripper ensures that asserted @@ -51,4 +41,3 @@ type jsonRoundTripper interface { } var _ jsonRoundTripper = (*Pair)(nil) -var _ jsonRoundTripper = (*KI64Pair)(nil) diff --git a/libs/kv/types.pb.go b/libs/kv/types.pb.go index c197d5791..7a6e6e12e 100644 --- a/libs/kv/types.pb.go +++ b/libs/kv/types.pb.go @@ -26,7 +26,6 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// Define these here for compatibility but use tmlibs/kv.Pair. type Pair struct { Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` @@ -82,74 +81,16 @@ func (m *Pair) GetValue() []byte { return nil } -// Define these here for compatibility but use tmlibs/kv.KI64Pair. -type KI64Pair struct { - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *KI64Pair) Reset() { *m = KI64Pair{} } -func (m *KI64Pair) String() string { return proto.CompactTextString(m) } -func (*KI64Pair) ProtoMessage() {} -func (*KI64Pair) Descriptor() ([]byte, []int) { - return fileDescriptor_31432671d164f444, []int{1} -} -func (m *KI64Pair) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *KI64Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_KI64Pair.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *KI64Pair) XXX_Merge(src proto.Message) { - xxx_messageInfo_KI64Pair.Merge(m, src) -} -func (m *KI64Pair) XXX_Size() int { - return m.Size() -} -func (m *KI64Pair) XXX_DiscardUnknown() { - xxx_messageInfo_KI64Pair.DiscardUnknown(m) -} - -var xxx_messageInfo_KI64Pair proto.InternalMessageInfo - -func (m *KI64Pair) GetKey() []byte { - if m != nil { - return m.Key - } - return nil -} - -func (m *KI64Pair) GetValue() int64 { - if m != nil { - return m.Value - } - return 0 -} - func init() { proto.RegisterType((*Pair)(nil), "tendermint.libs.kv.Pair") golang_proto.RegisterType((*Pair)(nil), "tendermint.libs.kv.Pair") - proto.RegisterType((*KI64Pair)(nil), "tendermint.libs.kv.KI64Pair") - golang_proto.RegisterType((*KI64Pair)(nil), "tendermint.libs.kv.KI64Pair") } func init() { proto.RegisterFile("libs/kv/types.proto", fileDescriptor_31432671d164f444) } func init() { golang_proto.RegisterFile("libs/kv/types.proto", fileDescriptor_31432671d164f444) } var fileDescriptor_31432671d164f444 = []byte{ - // 196 bytes of a gzipped FileDescriptorProto + // 182 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xce, 0xc9, 0x4c, 0x2a, 0xd6, 0xcf, 0x2e, 0xd3, 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x2a, 0x49, 0xcd, 0x4b, 0x49, 0x2d, 0xca, 0xcd, 0xcc, 0x2b, 0xd1, 0x03, 0xc9, 0xeb, 0x65, @@ -157,12 +98,11 @@ var fileDescriptor_31432671d164f444 = []byte{ 0x95, 0xe9, 0xa7, 0xe7, 0xa7, 0xe7, 0x23, 0x58, 0x10, 0xbd, 0x4a, 0x7a, 0x5c, 0x2c, 0x01, 0x89, 0x99, 0x45, 0x42, 0x02, 0x5c, 0xcc, 0xd9, 0xa9, 0x95, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x3c, 0x41, 0x20, 0xa6, 0x90, 0x08, 0x17, 0x6b, 0x59, 0x62, 0x4e, 0x69, 0xaa, 0x04, 0x13, 0x58, 0x0c, 0xc2, - 0x51, 0x32, 0xe2, 0xe2, 0xf0, 0xf6, 0x34, 0x33, 0x21, 0x46, 0x0f, 0x33, 0x54, 0x8f, 0x93, 0xdb, - 0x8f, 0x87, 0x72, 0x8c, 0x2b, 0x1e, 0xc9, 0x31, 0xee, 0x78, 0x24, 0xc7, 0x78, 0xe2, 0x91, 0x1c, - 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x1e, 0x78, 0x2c, 0xc7, 0x18, 0xa5, 0x91, - 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x8f, 0xf0, 0x08, 0x32, 0x13, 0xea, - 0xe7, 0x24, 0x36, 0xb0, 0x93, 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x5f, 0x67, 0xcb, - 0x05, 0x01, 0x00, 0x00, + 0x71, 0x72, 0xfb, 0xf1, 0x50, 0x8e, 0x71, 0xc5, 0x23, 0x39, 0xc6, 0x1d, 0x8f, 0xe4, 0x18, 0x4f, + 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x03, 0x8f, 0xe5, 0x18, + 0xa3, 0x34, 0xd2, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x11, 0x8e, 0x42, + 0x66, 0x42, 0xdd, 0x9f, 0xc4, 0x06, 0xb6, 0xde, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x5d, 0x3a, + 0xdc, 0xba, 0xd1, 0x00, 0x00, 0x00, } func (this *Pair) Equal(that interface{}) bool { @@ -195,36 +135,6 @@ func (this *Pair) Equal(that interface{}) bool { } return true } -func (this *KI64Pair) Equal(that interface{}) bool { - if that == nil { - return this == nil - } - - that1, ok := that.(*KI64Pair) - if !ok { - that2, ok := that.(KI64Pair) - if ok { - that1 = &that2 - } else { - return false - } - } - if that1 == nil { - return this == nil - } else if this == nil { - return false - } - if !bytes.Equal(this.Key, that1.Key) { - return false - } - if this.Value != that1.Value { - return false - } - if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { - return false - } - return true -} func (m *Pair) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -266,45 +176,6 @@ func (m *Pair) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *KI64Pair) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *KI64Pair) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *KI64Pair) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if m.XXX_unrecognized != nil { - i -= len(m.XXX_unrecognized) - copy(dAtA[i:], m.XXX_unrecognized) - } - if m.Value != 0 { - i = encodeVarintTypes(dAtA, i, uint64(m.Value)) - i-- - dAtA[i] = 0x10 - } - if len(m.Key) > 0 { - i -= len(m.Key) - copy(dAtA[i:], m.Key) - i = encodeVarintTypes(dAtA, i, uint64(len(m.Key))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - func encodeVarintTypes(dAtA []byte, offset int, v uint64) int { offset -= sovTypes(v) base := offset @@ -334,23 +205,6 @@ func NewPopulatedPair(r randyTypes, easy bool) *Pair { return this } -func NewPopulatedKI64Pair(r randyTypes, easy bool) *KI64Pair { - this := &KI64Pair{} - v3 := r.Intn(100) - this.Key = make([]byte, v3) - for i := 0; i < v3; i++ { - this.Key[i] = byte(r.Intn(256)) - } - this.Value = int64(r.Int63()) - if r.Intn(2) == 0 { - this.Value *= -1 - } - if !easy && r.Intn(10) != 0 { - this.XXX_unrecognized = randUnrecognizedTypes(r, 3) - } - return this -} - type randyTypes interface { Float32() float32 Float64() float64 @@ -370,9 +224,9 @@ func randUTF8RuneTypes(r randyTypes) rune { return rune(ru + 61) } func randStringTypes(r randyTypes) string { - v4 := r.Intn(100) - tmps := make([]rune, v4) - for i := 0; i < v4; i++ { + v3 := r.Intn(100) + tmps := make([]rune, v3) + for i := 0; i < v3; i++ { tmps[i] = randUTF8RuneTypes(r) } return string(tmps) @@ -394,11 +248,11 @@ func randFieldTypes(dAtA []byte, r randyTypes, fieldNumber int, wire int) []byte switch wire { case 0: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) - v5 := r.Int63() + v4 := r.Int63() if r.Intn(2) == 0 { - v5 *= -1 + v4 *= -1 } - dAtA = encodeVarintPopulateTypes(dAtA, uint64(v5)) + dAtA = encodeVarintPopulateTypes(dAtA, uint64(v4)) case 1: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256))) @@ -443,25 +297,6 @@ func (m *Pair) Size() (n int) { return n } -func (m *KI64Pair) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Key) - if l > 0 { - n += 1 + l + sovTypes(uint64(l)) - } - if m.Value != 0 { - n += 1 + sovTypes(uint64(m.Value)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - func sovTypes(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -590,113 +425,6 @@ func (m *Pair) Unmarshal(dAtA []byte) error { } return nil } -func (m *KI64Pair) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: KI64Pair: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: KI64Pair: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthTypes - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthTypes - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...) - if m.Key == nil { - m.Key = []byte{} - } - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - m.Value = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Value |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipTypes(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthTypes - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthTypes - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func skipTypes(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/libs/kv/types.proto b/libs/kv/types.proto index 7e1375c21..1b6a7a58d 100644 --- a/libs/kv/types.proto +++ b/libs/kv/types.proto @@ -16,14 +16,7 @@ option (gogoproto.testgen_all) = true; //---------------------------------------- // Abstract types -// Define these here for compatibility but use tmlibs/kv.Pair. message Pair { bytes key = 1; bytes value = 2; } - -// Define these here for compatibility but use tmlibs/kv.KI64Pair. -message KI64Pair { - bytes key = 1; - int64 value = 2; -} diff --git a/libs/kv/typespb_test.go b/libs/kv/typespb_test.go index 38656ecf9..dc45bf7f2 100644 --- a/libs/kv/typespb_test.go +++ b/libs/kv/typespb_test.go @@ -78,62 +78,6 @@ func TestPairMarshalTo(t *testing.T) { } } -func TestKI64PairProto(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, false) - dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) - if err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - msg := &KI64Pair{} - if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - littlefuzz := make([]byte, len(dAtA)) - copy(littlefuzz, dAtA) - for i := range dAtA { - dAtA[i] = byte(popr.Intn(256)) - } - if !p.Equal(msg) { - t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) - } - if len(littlefuzz) > 0 { - fuzzamount := 100 - for i := 0; i < fuzzamount; i++ { - littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) - littlefuzz = append(littlefuzz, byte(popr.Intn(256))) - } - // shouldn't panic - _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) - } -} - -func TestKI64PairMarshalTo(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, false) - size := p.Size() - dAtA := make([]byte, size) - for i := range dAtA { - dAtA[i] = byte(popr.Intn(256)) - } - _, err := p.MarshalTo(dAtA) - if err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - msg := &KI64Pair{} - if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - for i := range dAtA { - dAtA[i] = byte(popr.Intn(256)) - } - if !p.Equal(msg) { - t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) - } -} - func TestPairJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -152,24 +96,6 @@ func TestPairJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } -func TestKI64PairJSON(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, true) - marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} - jsondata, err := marshaler.MarshalToString(p) - if err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - msg := &KI64Pair{} - err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) - if err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - if !p.Equal(msg) { - t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) - } -} func TestPairProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -198,34 +124,6 @@ func TestPairProtoCompactText(t *testing.T) { } } -func TestKI64PairProtoText(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, true) - dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) - msg := &KI64Pair{} - if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - if !p.Equal(msg) { - t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) - } -} - -func TestKI64PairProtoCompactText(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, true) - dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) - msg := &KI64Pair{} - if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - if !p.Equal(msg) { - t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) - } -} - func TestPairSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -248,26 +146,4 @@ func TestPairSize(t *testing.T) { } } -func TestKI64PairSize(t *testing.T) { - seed := time.Now().UnixNano() - popr := math_rand.New(math_rand.NewSource(seed)) - p := NewPopulatedKI64Pair(popr, true) - size2 := github_com_gogo_protobuf_proto.Size(p) - dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) - if err != nil { - t.Fatalf("seed = %d, err = %v", seed, err) - } - size := p.Size() - if len(dAtA) != size { - t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) - } - if size2 != size { - t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) - } - size3 := github_com_gogo_protobuf_proto.Size(p) - if size3 != size { - t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) - } -} - //These tests are generated by github.com/gogo/protobuf/plugin/testgen From c534291a5652ddb8d9c2b89f2fdb18485ef2ccd1 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 11 Mar 2020 11:57:42 +0100 Subject: [PATCH 74/74] rc1/v0.33.2(#4541) * rc1/v0.33.2 - release candiadte for 0.33.2 Signed-off-by: Marko Baricevic Co-authored-by: Anton Kaliaev --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 13 +---------- version/version.go | 2 +- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6040849c1..89315a1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## v0.33.2 + +*March 11, 2020* + +Special thanks to external contributors on this release: +@antho1404, @michaelfig, @gterzian, @tau3, @Shivani912 + +Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +- CLI/RPC/Config + - [cli] [\#4505](https://github.com/tendermint/tendermint/pull/4505) `tendermint lite` sub-command new syntax (@melekes): + `lite cosmoshub-3 -p 52.57.29.196:26657 -w public-seed-node.cosmoshub.certus.one:26657 + --height 962118 --hash 28B97BE9F6DE51AC69F70E0B7BFD7E5C9CD1A595B7DC31AFF27C50D4948` + +- Go API + - [lite2] [\#4535](https://github.com/tendermint/tendermint/pull/4535) Remove `Start/Stop` (@melekes) + - [lite2] [\#4469](https://github.com/tendermint/tendermint/issues/4469) Remove `RemoveNoLongerTrustedHeaders` and `RemoveNoLongerTrustedHeadersPeriod` option (@cmwaters) + - [lite2] [\#4473](https://github.com/tendermint/tendermint/issues/4473) Return height as a 2nd param in `TrustedValidatorSet` (@melekes) + - [lite2] [\#4536](https://github.com/tendermint/tendermint/pull/4536) `Update` returns a signed header (1st param) (@melekes) + + +### IMPROVEMENTS: + +- [blockchain/v2] [\#4361](https://github.com/tendermint/tendermint/pull/4361) Add reactor (@brapse) +- [cmd] [\#4515](https://github.com/tendermint/tendermint/issues/4515) Change `tendermint debug dump` sub-command archives filename's format (@melekes) +- [consensus] [\#3583](https://github.com/tendermint/tendermint/issues/3583) Reduce `non-deterministic signature` log noise (@tau3) +- [examples/kvstore] [\#4507](https://github.com/tendermint/tendermint/issues/4507) ABCI query now returns the proper height (@erikgrinaker) +- [lite2] [\#4462](https://github.com/tendermint/tendermint/issues/4462) Add `NewHTTPClient` and `NewHTTPClientFromTrustedStore` (@cmwaters) +- [lite2] [\#4329](https://github.com/tendermint/tendermint/issues/4329) modified bisection to loop (@cmwaters) +- [lite2] [\#4385](https://github.com/tendermint/tendermint/issues/4385) Disconnect from bad nodes (@melekes) +- [lite2] [\#4398](https://github.com/tendermint/tendermint/issues/4398) Add `VerifyAdjacent` and `VerifyNonAdjacent` funcs (@cmwaters) +- [lite2] [\#4426](https://github.com/tendermint/tendermint/issues/4426) Don't save intermediate headers (@cmwaters) +- [lite2] [\#4464](https://github.com/tendermint/tendermint/issues/4464) Cross-check first header (@cmwaters) +- [lite2] [\#4470](https://github.com/tendermint/tendermint/issues/4470) Fix inconsistent header-validatorset pairing (@melekes) +- [lite2] [\#4488](https://github.com/tendermint/tendermint/issues/4488) Allow local clock drift -10 sec. (@melekes) +- [p2p] [\#4449](https://github.com/tendermint/tendermint/pull/4449) Use `curve25519.X25519()` instead of `ScalarMult` (@erikgrinaker) +- [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) **VerifyCommitX() functions should return as soon as +2/3 threshold is reached** (@alessio). +- [libs/kv] [\#4542](https://github.com/tendermint/tendermint/pull/4542) remove unused type KI64Pair (@tessr) + +### BUG FIXES: + +- [cmd] [\#4303](https://github.com/tendermint/tendermint/issues/4303) Show useful error when Tendermint is not initialized (@melekes) +- [cmd] [\#4515](https://github.com/tendermint/tendermint/issues/4515) **Fix `tendermint debug kill` sub-command** (@melekes) +- [rpc] [\#3935](https://github.com/tendermint/tendermint/issues/3935) **Create buffered subscriptions on `/subscribe`** (@melekes) +- [rpc] [\#4375](https://github.com/tendermint/tendermint/issues/4375) Stop searching for txs in `/tx_search` upon client timeout (@gterzian) +- [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) +- [rpc] [\#4432](https://github.com/tendermint/tendermint/issues/4432) Fix `/tx_search` pagination with ordered results (@erikgrinaker) +- [rpc] [\#4492](https://github.com/tendermint/tendermint/issues/4492) Keep the original subscription "id" field when new RPCs come in (@michaelfig) + + ## v0.33.1 *Feburary 13, 2020* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index aa0c0c41f..42609c19b 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,4 @@ -## v0.33.2 +## v0.33.3 \*\* @@ -18,15 +18,4 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi ### IMPROVEMENTS: -- [types] [\#4417](https://github.com/tendermint/tendermint/issues/4417) VerifyCommitX() functions should return as soon as +2/3 threashold is reached. -- [examples/kvstore] [\#4509](https://github.com/tendermint/tendermint/pull/4509) ABCI query now returns the proper height (@erikgrinaker) -- [cmd] \#4515 Change `tendermint debug dump` sub-command archives filename's format (@melekes) -- [libs/kv]: [\#4542](https://github.com/tendermint/tendermint/pull/4542) remove unused type KI64Pair (@tessr) - ### BUG FIXES: - -- [rpc] \#3935 Create buffered subscriptions on `/subscribe` (@melekes) -- [rpc] [\#4493](https://github.com/tendermint/tendermint/pull/4493) Keep the original subscription "id" field when new RPCs come in (@michaelfig) -- [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker) -- [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404) -- [cmd] \#4515 Fix `tendermint debug kill` sub-command (@melekes) diff --git a/version/version.go b/version/version.go index 57ef21f1f..9a16e5d58 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.33.1" + TMCoreSemVer = "0.33.2" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.1"