diff --git a/.circleci/config.yml b/.circleci/config.yml index 5836b4546..539dd7ee4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -331,6 +331,34 @@ jobs: docker push "tendermint/tendermint" docker logout + reproducible_builds: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - checkout + - setup_remote_docker: + docker_layer_caching: true + - run: + name: Build tendermint + no_output_timeout: 20m + command: | + sudo apt-get install -y ruby + bash -x ./scripts/gitian-build.sh all + for os in darwin linux windows; do + cp gitian-build-${os}/result/tendermint-${os}-res.yml . + cp gitian-build-${os}/build/out/tendermint-*.tar.gz . + rm -rf gitian-build-${os}/ + done + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-darwin-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-linux-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-windows-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-*.tar.gz + workflows: version: 2 test-suite: @@ -340,7 +368,6 @@ workflows: branches: only: - master - - develop - setup_dependencies - test_abci_apps: requires: @@ -364,6 +391,12 @@ workflows: - upload_coverage: requires: - test_cover + - reproducible_builds: + filters: + branches: + only: + - master + - /v[0-9]+\.[0-9]+/ release: jobs: - prepare_build diff --git a/.gitignore b/.gitignore index 10ee3099c..9e2e5a9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ terraform.tfstate.backup terraform.tfstate.d .vscode + +profile\.out diff --git a/.golangci.yml b/.golangci.yml index 6adbbd9da..17d575316 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,6 @@ linters: - golint - maligned - errcheck - - staticcheck - interfacer - unconvert - goconst @@ -16,7 +15,6 @@ linters: - nakedret - lll - gochecknoglobals - - gocritic - gochecknoinits - scopelint - stylecheck diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f547a05..398250d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,87 @@ # Changelog +## v0.32.2 + +*July 31, 2019* + +Special thanks to external contributors on this release: +@ruseinov, @bluele, @guagualvcha + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +- Go API + - [libs] [\#3811](https://github.com/tendermint/tendermint/issues/3811) Remove `db` from libs in favor of `https://github.com/tendermint/tm-db` + +### FEATURES: + +- [node] [\#3846](https://github.com/tendermint/tendermint/pull/3846) Allow replacing existing p2p.Reactor(s) using [`CustomReactors` + option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). + Warning: beware of accidental name clashes. Here is the list of existing + reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. +- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file +- [rpc] [\#3818](https://github.com/tendermint/tendermint/issues/3818) Make `max_body_bytes` and `max_header_bytes` configurable(@bluele) +- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) +- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) + +### IMPROVEMENTS: + +- [abci] [\#3809](https://github.com/tendermint/tendermint/issues/3809) Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) +- [rpc] [\#2252](https://github.com/tendermint/tendermint/issues/2252) Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence +- [p2p] [\#3664](https://github.com/tendermint/tendermint/issues/3664) p2p/conn: reuse buffer when write/read from secret connection(@guagualvcha) +- [rpc] [\#3076](https://github.com/tendermint/tendermint/issues/3076) Improve transaction search performance + +### BUG FIXES: + +- [p2p] [\#3644](https://github.com/tendermint/tendermint/issues/3644) Fix error logging for connection stop (@defunctzombie) +- [rpc] [\#3813](https://github.com/tendermint/tendermint/issues/3813) Return err if page is incorrect (less than 0 or greater than total pages) + +## v0.32.1 + +*July 15, 2019* + +Special thanks to external contributors on this release: +@ParthDesai, @climber73, @jim380, @ashleyvega + +This release contains a minor enhancement to the ABCI and some breaking changes to our libs folder, namely: +- CheckTx requests include a `CheckTxType` enum that can be set to `Recheck` to indicate to the application that this transaction was already checked/validated and certain expensive operations (like checking signatures) can be skipped +- Removed various functions from `libs` pkgs + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +- Go API + + - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) The CheckTx and DeliverTx methods in the ABCI `Application` interface now take structs as arguments (RequestCheckTx and RequestDeliverTx, respectively), instead of just the raw tx bytes. This allows more information to be passed to these methods, for instance, indicating whether a tx has already been checked. + - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) + - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) + - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) + - [libs] Remove unused `FailRand()` func and minor clean up to `fail.go`(@marbar3778) + +### FEATURES: + +- [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. +- [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass + custom reactors to run inside Tendermint node (@ParthDesai) +- [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127)RequestCheckTx has a new field, `CheckTxType`, which can take values of `CheckTxType_New` and `CheckTxType_Recheck`, indicating whether this is a new tx being checked for the first time or whether this tx is being rechecked after a block commit. This allows applications to skip certain expensive operations, like signature checking, if they've already been done once. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) + +### IMPROVEMENTS: + +- [rpc] [\#3700](https://github.com/tendermint/tendermint/issues/3700) Make possible to set absolute paths for TLS cert and key (@climber73) +- [abci] [\#3513](https://github.com/tendermint/tendermint/issues/3513) Call the reqRes callback after the resCb so they always happen in the same order + +### BUG FIXES: + +- [p2p] [\#3338](https://github.com/tendermint/tendermint/issues/3338) Prevent "sent next PEX request too soon" errors by not calling + ensurePeers outside of ensurePeersRoutine +- [behaviour] [\3772](https://github.com/tendermint/tendermint/pull/3772) Return correct reason in MessageOutOfOrder (@jim380) +- [config] [\#3723](https://github.com/tendermint/tendermint/issues/3723) Add consensus_params to testnet config generation; document time_iota_ms (@ashleyvega) + + ## v0.32.0 *June 25, 2019* @@ -21,29 +103,29 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: * CLI/RPC/Config - - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: + - [cli] [\#3613](https://github.com/tendermint/tendermint/issues/3613) Switch from golang/dep to Go Modules to resolve dependencies: It is recommended to switch to Go Modules if your project has tendermint as a dependency. Read more on Modules here: https://github.com/golang/go/wiki/Modules - [config] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed `leveldb` as generic option for `db_backend`. Must be `goleveldb` or `cleveldb`. - - [rpc] \#3616 Fix field names for `/block_results` response (eg. `results.DeliverTx` + - [rpc] [\#3616](https://github.com/tendermint/tendermint/issues/3616) Fix field names for `/block_results` response (eg. `results.DeliverTx` -> `results.deliver_tx`). See docs for details. - - [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` + - [rpc] [\#3724](https://github.com/tendermint/tendermint/issues/3724) RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` * Apps - - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + - [abci] [\#1859](https://github.com/tendermint/tendermint/issues/1859) `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` contains a `type` and a list of `attributes` (list of key-value pairs) allowing for inclusion of multiple distinct events in each response. * Go API - - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI + - [abci] [\#3193](https://github.com/tendermint/tendermint/issues/3193) Use RequestDeliverTx and RequestCheckTx in the ABCI Application interface - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. - - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + - [p2p] [\#3521](https://github.com/tendermint/tendermint/issues/3521) Remove NewNetAddressStringWithOptionalID * Blockchain Protocol @@ -52,16 +134,16 @@ program](https://hackerone.com/tendermint). ### FEATURES: ### IMPROVEMENTS: -- [abci/examples] \#3659 Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) -- [consensus] \#3656 Exit if SwitchToConsensus fails -- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [abci/examples] [\#3659](https://github.com/tendermint/tendermint/issues/3659) Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) +- [consensus] [\#3656](https://github.com/tendermint/tendermint/issues/3656) Exit if SwitchToConsensus fails +- [p2p] [\#3666](https://github.com/tendermint/tendermint/issues/3666) Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) ### BUG FIXES: -- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) -- [node] \#3716 Fix a bug where `nil` is recorded as node's address -- [node] \#3741 Fix profiler blocking the entire node +- [libs/db] [\#3717](https://github.com/tendermint/tendermint/issues/3717) Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] [\#3718](https://github.com/tendermint/tendermint/issues/3718) Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] [\#3716](https://github.com/tendermint/tendermint/issues/3716) Fix a bug where `nil` is recorded as node's address +- [node] [\#3741](https://github.com/tendermint/tendermint/issues/3741) Fix profiler blocking the entire node ## v0.31.7 @@ -72,11 +154,11 @@ The regression caused the invalid committed txs to be proposed in blocks over an over again. ### BUG FIXES: -- [mempool] \#3699 Remove all committed txs from the mempool. +- [mempool] [\#3699](https://github.com/tendermint/tendermint/issues/3699) Remove all committed txs from the mempool. This reverts the change from v0.31.6 where we only remove valid txs from the mempool. Note this means malicious proposals can cause txs to be dropped from the mempools of other nodes by including them in blocks before they are valid. - See \#3322. + See [\#3322](https://github.com/tendermint/tendermint/issues/3322). ## v0.31.6 diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9c4c8b684..efc518e80 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,6 +1,6 @@ -## v0.32.1 +## v0.32.3 -** +\*\* Special thanks to external contributors on this release: @@ -9,33 +9,21 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: -* CLI/RPC/Config +- CLI/RPC/Config -* Apps +- Apps -* Go API - - [abci] \#2127 ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI - client interface (`abcicli.Client`) to allow for supplying the ABCI - `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the - mempool indicate to the ABCI app whether a CheckTx request is a recheck or - not. - - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - -* Blockchain Protocol - -* P2P Protocol +- Go API ### FEATURES: -- [node] Refactor `NewNode` to use functional options to make it more flexible - and extensible in the future. -- [node] [\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass - custom reactors to run inside Tendermint node (@ParthDesai) ### IMPROVEMENTS: - - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) + +- [privval] \#3370 Refactors and simplifies validator/kms connection handling. Please refer to thttps://github.com/tendermint/tendermint/pull/3370#issue-257360971 +- [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) +- [mempool] \#3877 Make `max_tx_bytes` configurable instead of `max_msg_bytes` ### BUG FIXES: -- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling - ensurePeers outside of ensurePeersRoutine -- [behaviour] Return correct reason in MessageOutOfOrder (@jim380) - + +- [config] \#3868 move misplaced `max_msg_bytes` into mempool section +- [store] \#3893 register block amino, not just crypto diff --git a/DOCKER/Dockerfile.develop b/DOCKER/Dockerfile.develop deleted file mode 100644 index 943b21291..000000000 --- a/DOCKER/Dockerfile.develop +++ /dev/null @@ -1,34 +0,0 @@ -FROM alpine:3.7 - -ENV DATA_ROOT /tendermint -ENV TMHOME $DATA_ROOT - -RUN addgroup tmuser && \ - adduser -S -G tmuser tmuser - -RUN mkdir -p $DATA_ROOT && \ - chown -R tmuser:tmuser $DATA_ROOT - -RUN apk add --no-cache bash curl jq - -ENV GOPATH /go -ENV PATH "$PATH:/go/bin" -RUN mkdir -p /go/src/github.com/tendermint/tendermint && \ - apk add --no-cache go build-base git && \ - cd /go/src/github.com/tendermint/tendermint && \ - git clone https://github.com/tendermint/tendermint . && \ - git checkout develop && \ - make get_tools && \ - make install && \ - cd - && \ - rm -rf /go/src/github.com/tendermint/tendermint && \ - apk del go build-base git - -VOLUME $DATA_ROOT - -EXPOSE 26656 -EXPOSE 26657 - -ENTRYPOINT ["tendermint"] - -CMD ["node", "--moniker=`hostname`", "--proxy_app=kvstore"] diff --git a/DOCKER/README.md b/DOCKER/README.md index 43edce0fc..57e631aaa 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -12,28 +12,25 @@ - `0.9.1`, `0.9`, [(Dockerfile)](https://github.com/tendermint/tendermint/blob/809e0e8c5933604ba8b2d096803ada7c5ec4dfd3/DOCKER/Dockerfile) - `0.9.0` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/d474baeeea6c22b289e7402449572f7c89ee21da/DOCKER/Dockerfile) - `0.8.0`, `0.8` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/bf64dd21fdb193e54d8addaaaa2ecf7ac371de8c/DOCKER/Dockerfile) -- `develop` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/master/DOCKER/Dockerfile.develop) - -`develop` tag points to the [develop](https://github.com/tendermint/tendermint/tree/develop) branch. ## Quick reference -* **Where to get help:** - https://cosmos.network/community +- **Where to get help:** + [cosmos.network/ecosystem](https://cosmos.network/ecosystem) -* **Where to file issues:** - https://github.com/tendermint/tendermint/issues +- **Where to file issues:** + [Tendermint Issues](https://github.com/tendermint/tendermint/issues) -* **Supported Docker versions:** +- **Supported Docker versions:** [the latest release](https://github.com/moby/moby/releases) (down to 1.6 on a best-effort basis) ## Tendermint Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine, written in any programming language, and securely replicates it on many machines. -For more background, see the [introduction](https://tendermint.readthedocs.io/en/master/introduction.html). +For more background, see the [the docs](https://tendermint.com/docs/introduction/#quick-start). -To get started developing applications, see the [application developers guide](https://tendermint.readthedocs.io/en/master/getting-started.html). +To get started developing applications, see the [application developers guide](https://tendermint.com/docs/introduction/quick-start.html). ## How to use this image @@ -48,7 +45,7 @@ docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy_app ## Local cluster -To run a 4-node network, see the `Makefile` in the root of [the repo](https://github.com/tendermint/tendermint/master/Makefile) and run: +To run a 4-node network, see the `Makefile` in the root of [the repo](https://github.com/tendermint/tendermint/blob/master/Makefile) and run: ``` make build-linux @@ -60,7 +57,7 @@ Note that this will build and use a different image than the ones provided here. ## License -- Tendermint's license is [Apache 2.0](https://github.com/tendermint/tendermint/master/LICENSE). +- Tendermint's license is [Apache 2.0](https://github.com/tendermint/tendermint/blob/master/LICENSE). ## Contributing diff --git a/README.md b/README.md index 994ca63b2..3ea9d5de4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@ # Tendermint + ![banner](docs/tendermint-core-image.jpg) [Byzantine-Fault Tolerant](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance) [State Machines](https://en.wikipedia.org/wiki/State_machine_replication). -Or [Blockchain](https://en.wikipedia.org/wiki/Blockchain_(database)), for short. +Or [Blockchain](), for short. [![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) +[![API Reference](https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667)](https://godoc.org/github.com/tendermint/tendermint) [![Go version](https://img.shields.io/badge/go-1.12.0-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) [![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) - -Branch | Tests | Coverage -----------|-------|---------- -master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) +| Branch | Tests | Coverage | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) | Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. @@ -49,9 +47,9 @@ For examples of the kinds of bugs we're looking for, see [SECURITY.md](SECURITY. ## Minimum requirements -Requirement|Notes ----|--- -Go version | Go1.11.4 or higher +| Requirement | Notes | +| ----------- | ------------------ | +| Go version | Go1.11.4 or higher | ## Documentation @@ -145,20 +143,20 @@ Additional documentation is found [here](/docs/tools). ### Sub-projects -* [Amino](http://github.com/tendermint/go-amino), reflection-based proto3, with +- [Amino](http://github.com/tendermint/go-amino), reflection-based proto3, with interfaces -* [IAVL](http://github.com/tendermint/iavl), Merkleized IAVL+ Tree implementation +- [IAVL](http://github.com/tendermint/iavl), Merkleized IAVL+ Tree implementation +- [Tm-cmn](http://github.com/tendermint/tm-cmn), Commonly used libs across Tendermint & Cosmos repos ### Applications -* [Cosmos SDK](http://github.com/cosmos/cosmos-sdk); a cryptocurrency application framework -* [Ethermint](http://github.com/cosmos/ethermint); Ethereum on Tendermint -* [Many more](https://tendermint.com/ecosystem) +- [Cosmos SDK](http://github.com/cosmos/cosmos-sdk); a cryptocurrency application framework +- [Ethermint](http://github.com/cosmos/ethermint); Ethereum on Tendermint +- [Many more](https://tendermint.com/ecosystem) ### Research -* [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938) -* [Master's Thesis on Tendermint](https://atrium.lib.uoguelph.ca/xmlui/handle/10214/9769) -* [Original Whitepaper](https://tendermint.com/static/docs/tendermint.pdf) -* [Blog](https://blog.cosmos.network/tendermint/home) - +- [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938) +- [Master's Thesis on Tendermint](https://atrium.lib.uoguelph.ca/xmlui/handle/10214/9769) +- [Original Whitepaper](https://tendermint.com/static/docs/tendermint.pdf) +- [Blog](https://blog.cosmos.network/tendermint/home) diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index 23d790550..e326055fb 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -6,8 +6,8 @@ import ( "sync" "time" - context "golang.org/x/net/context" - grpc "google.golang.org/grpc" + "golang.org/x/net/context" + "google.golang.org/grpc" "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" @@ -39,7 +39,7 @@ func NewGRPCClient(addr string, mustConnect bool) *grpcClient { return cli } -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } @@ -49,7 +49,7 @@ func (cli *grpcClient) OnStart() error { } RETRY_LOOP: for { - conn, err := grpc.Dial(cli.addr, grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial(cli.addr, grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { if cli.mustConnect { return err @@ -65,7 +65,7 @@ RETRY_LOOP: ENSURE_CONNECTED: for { - _, err := client.Echo(context.Background(), &types.RequestEcho{Message: "hello"}, grpc.FailFast(true)) + _, err := client.Echo(context.Background(), &types.RequestEcho{Message: "hello"}, grpc.WaitForReady(true)) if err == nil { break ENSURE_CONNECTED } @@ -125,7 +125,7 @@ func (cli *grpcClient) SetResponseCallback(resCb Callback) { func (cli *grpcClient) EchoAsync(msg string) *ReqRes { req := types.ToRequestEcho(msg) - res, err := cli.client.Echo(context.Background(), req.GetEcho(), grpc.FailFast(true)) + res, err := cli.client.Echo(context.Background(), req.GetEcho(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -134,7 +134,7 @@ func (cli *grpcClient) EchoAsync(msg string) *ReqRes { func (cli *grpcClient) FlushAsync() *ReqRes { req := types.ToRequestFlush() - res, err := cli.client.Flush(context.Background(), req.GetFlush(), grpc.FailFast(true)) + res, err := cli.client.Flush(context.Background(), req.GetFlush(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -143,7 +143,7 @@ func (cli *grpcClient) FlushAsync() *ReqRes { func (cli *grpcClient) InfoAsync(params types.RequestInfo) *ReqRes { req := types.ToRequestInfo(params) - res, err := cli.client.Info(context.Background(), req.GetInfo(), grpc.FailFast(true)) + res, err := cli.client.Info(context.Background(), req.GetInfo(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -152,7 +152,7 @@ func (cli *grpcClient) InfoAsync(params types.RequestInfo) *ReqRes { func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { req := types.ToRequestSetOption(params) - res, err := cli.client.SetOption(context.Background(), req.GetSetOption(), grpc.FailFast(true)) + res, err := cli.client.SetOption(context.Background(), req.GetSetOption(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -161,7 +161,7 @@ func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { func (cli *grpcClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { req := types.ToRequestDeliverTx(params) - res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.FailFast(true)) + res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -170,7 +170,7 @@ func (cli *grpcClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { req := types.ToRequestCheckTx(params) - res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.FailFast(true)) + res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -179,7 +179,7 @@ func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { func (cli *grpcClient) QueryAsync(params types.RequestQuery) *ReqRes { req := types.ToRequestQuery(params) - res, err := cli.client.Query(context.Background(), req.GetQuery(), grpc.FailFast(true)) + res, err := cli.client.Query(context.Background(), req.GetQuery(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -188,7 +188,7 @@ func (cli *grpcClient) QueryAsync(params types.RequestQuery) *ReqRes { func (cli *grpcClient) CommitAsync() *ReqRes { req := types.ToRequestCommit() - res, err := cli.client.Commit(context.Background(), req.GetCommit(), grpc.FailFast(true)) + res, err := cli.client.Commit(context.Background(), req.GetCommit(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -197,7 +197,7 @@ func (cli *grpcClient) CommitAsync() *ReqRes { func (cli *grpcClient) InitChainAsync(params types.RequestInitChain) *ReqRes { req := types.ToRequestInitChain(params) - res, err := cli.client.InitChain(context.Background(), req.GetInitChain(), grpc.FailFast(true)) + res, err := cli.client.InitChain(context.Background(), req.GetInitChain(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -206,7 +206,7 @@ func (cli *grpcClient) InitChainAsync(params types.RequestInitChain) *ReqRes { func (cli *grpcClient) BeginBlockAsync(params types.RequestBeginBlock) *ReqRes { req := types.ToRequestBeginBlock(params) - res, err := cli.client.BeginBlock(context.Background(), req.GetBeginBlock(), grpc.FailFast(true)) + res, err := cli.client.BeginBlock(context.Background(), req.GetBeginBlock(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -215,7 +215,7 @@ func (cli *grpcClient) BeginBlockAsync(params types.RequestBeginBlock) *ReqRes { func (cli *grpcClient) EndBlockAsync(params types.RequestEndBlock) *ReqRes { req := types.ToRequestEndBlock(params) - res, err := cli.client.EndBlock(context.Background(), req.GetEndBlock(), grpc.FailFast(true)) + res, err := cli.client.EndBlock(context.Background(), req.GetEndBlock(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -228,18 +228,22 @@ func (cli *grpcClient) finishAsyncCall(req *types.Request, res *types.Response) reqres.Done() // Release waiters reqres.SetDone() // so reqRes.SetCallback will run the callback - // go routine for callbacks + // goroutine for callbacks go func() { - // Notify reqRes listener if set - if cb := reqres.GetCallback(); cb != nil { - cb(res) - } + cli.mtx.Lock() + defer cli.mtx.Unlock() // Notify client listener if set if cli.resCb != nil { cli.resCb(reqres.Request, res) } + + // Notify reqRes listener if set + if cb := reqres.GetCallback(); cb != nil { + cb(res) + } }() + return reqres } diff --git a/abci/cmd/abci-cli/abci-cli.go b/abci/cmd/abci-cli/abci-cli.go index cd0a6fd1f..31721b21b 100644 --- a/abci/cmd/abci-cli/abci-cli.go +++ b/abci/cmd/abci-cli/abci-cli.go @@ -174,9 +174,7 @@ where example.file looks something like: info `, Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdBatch(cmd, args) - }, + RunE: cmdBatch, } var consoleCmd = &cobra.Command{ @@ -189,9 +187,7 @@ without opening a new connection each time `, Args: cobra.ExactArgs(0), ValidArgs: []string{"echo", "info", "set_option", "deliver_tx", "check_tx", "commit", "query"}, - RunE: func(cmd *cobra.Command, args []string) error { - return cmdConsole(cmd, args) - }, + RunE: cmdConsole, } var echoCmd = &cobra.Command{ @@ -199,27 +195,21 @@ var echoCmd = &cobra.Command{ Short: "have the application echo a message", Long: "have the application echo a message", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdEcho(cmd, args) - }, + RunE: cmdEcho, } var infoCmd = &cobra.Command{ Use: "info", Short: "get some info about the application", Long: "get some info about the application", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdInfo(cmd, args) - }, + RunE: cmdInfo, } var setOptionCmd = &cobra.Command{ Use: "set_option", Short: "set an option on the application", Long: "set an option on the application", Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdSetOption(cmd, args) - }, + RunE: cmdSetOption, } var deliverTxCmd = &cobra.Command{ @@ -227,9 +217,7 @@ var deliverTxCmd = &cobra.Command{ Short: "deliver a new transaction to the application", Long: "deliver a new transaction to the application", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdDeliverTx(cmd, args) - }, + RunE: cmdDeliverTx, } var checkTxCmd = &cobra.Command{ @@ -237,9 +225,7 @@ var checkTxCmd = &cobra.Command{ Short: "validate a transaction", Long: "validate a transaction", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCheckTx(cmd, args) - }, + RunE: cmdCheckTx, } var commitCmd = &cobra.Command{ @@ -247,9 +233,7 @@ var commitCmd = &cobra.Command{ Short: "commit the application state and return the Merkle root hash", Long: "commit the application state and return the Merkle root hash", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCommit(cmd, args) - }, + RunE: cmdCommit, } var versionCmd = &cobra.Command{ @@ -268,9 +252,7 @@ var queryCmd = &cobra.Command{ Short: "query the application state", Long: "query the application state", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdQuery(cmd, args) - }, + RunE: cmdQuery, } var counterCmd = &cobra.Command{ @@ -278,9 +260,7 @@ var counterCmd = &cobra.Command{ Short: "ABCI demo example", Long: "ABCI demo example", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCounter(cmd, args) - }, + RunE: cmdCounter, } var kvstoreCmd = &cobra.Command{ @@ -288,9 +268,7 @@ var kvstoreCmd = &cobra.Command{ Short: "ABCI demo example", Long: "ABCI demo example", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdKVStore(cmd, args) - }, + RunE: cmdKVStore, } var testCmd = &cobra.Command{ @@ -298,9 +276,7 @@ var testCmd = &cobra.Command{ Short: "run integration tests", Long: "run integration tests", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdTest(cmd, args) - }, + RunE: cmdTest, } // Generates new Args array based off of previous call args to maintain flag persistence @@ -356,16 +332,18 @@ func cmdTest(cmd *cobra.Command, args []string) error { func cmdBatch(cmd *cobra.Command, args []string) error { bufReader := bufio.NewReader(os.Stdin) +LOOP: for { line, more, err := bufReader.ReadLine() - if more { + switch { + case more: return errors.New("Input line is too long") - } else if err == io.EOF { - break - } else if len(line) == 0 { + case err == io.EOF: + break LOOP + case len(line) == 0: continue - } else if err != nil { + case err != nil: return err } @@ -419,7 +397,7 @@ func muxOnCommands(cmd *cobra.Command, pArgs []string) error { } // otherwise, we need to skip the next one too - i += 1 + i++ continue } diff --git a/abci/example/example_test.go b/abci/example/example_test.go index 6282f3a44..74510700b 100644 --- a/abci/example/example_test.go +++ b/abci/example/example_test.go @@ -107,7 +107,7 @@ func testStream(t *testing.T, app types.Application) { //------------------------- // test grpc -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } @@ -123,7 +123,7 @@ func testGRPCSync(t *testing.T, app *types.GRPCApplication) { defer server.Stop() // Connect to the socket - conn, err := grpc.Dial("unix://test.sock", grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial("unix://test.sock", grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { t.Fatalf("Error dialing GRPC server: %v", err.Error()) } diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 82d404c76..6eb4a1247 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -9,8 +9,8 @@ import ( "github.com/tendermint/tendermint/abci/example/code" "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/version" + dbm "github.com/tendermint/tm-db" ) var ( @@ -115,6 +115,7 @@ func (app *KVStoreApplication) Commit() types.ResponseCommit { return types.ResponseCommit{Data: appHash} } +// Returns an associated value or nil if missing. func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { if reqQuery.Prove { value := app.state.db.Get(prefixKey(reqQuery.Data)) diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index 1649d3e84..80b07ff5a 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -148,7 +148,7 @@ func TestValUpdates(t *testing.T) { makeApplyBlock(t, kvstore, 2, diff, tx1, tx2, tx3) - vals1 = append(vals[:nInit-2], vals[nInit+1]) + vals1 = append(vals[:nInit-2], vals[nInit+1]) // nolint: gocritic vals2 = kvstore.Validators() valsEqual(t, vals1, vals2) diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index ba0b53896..eb2514a69 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -9,8 +9,10 @@ import ( "github.com/tendermint/tendermint/abci/example/code" "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) const ( @@ -27,6 +29,8 @@ type PersistentKVStoreApplication struct { // validator set ValUpdates []types.ValidatorUpdate + valAddrToPubKeyMap map[string]types.PubKey + logger log.Logger } @@ -40,8 +44,9 @@ func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication state := loadState(db) return &PersistentKVStoreApplication{ - app: &KVStoreApplication{state: state}, - logger: log.NewNopLogger(), + app: &KVStoreApplication{state: state}, + valAddrToPubKeyMap: make(map[string]types.PubKey), + logger: log.NewNopLogger(), } } @@ -83,8 +88,20 @@ func (app *PersistentKVStoreApplication) Commit() types.ResponseCommit { return app.app.Commit() } -func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { - return app.app.Query(reqQuery) +// When path=/val and data={validator address}, returns the validator update (types.ValidatorUpdate) varint encoded. +// For any other path, returns an associated value or nil if missing. +func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + switch reqQuery.Path { + case "/val": + key := []byte("val:" + string(reqQuery.Data)) + value := app.app.state.db.Get(key) + + resQuery.Key = reqQuery.Data + resQuery.Value = value + return + default: + return app.app.Query(reqQuery) + } } // Save the validators in the merkle tree @@ -102,6 +119,19 @@ func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) t func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { // reset valset changes app.ValUpdates = make([]types.ValidatorUpdate, 0) + + for _, ev := range req.ByzantineValidators { + if ev.Type == tmtypes.ABCIEvidenceTypeDuplicateVote { + // decrease voting power by 1 + if ev.TotalVotingPower == 0 { + continue + } + app.updateValidator(types.ValidatorUpdate{ + PubKey: app.valAddrToPubKeyMap[string(ev.Validator.Address)], + Power: ev.TotalVotingPower - 1, + }) + } + } return types.ResponseBeginBlock{} } @@ -174,6 +204,10 @@ func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.Respon // add, update, or remove a validator func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate) types.ResponseDeliverTx { key := []byte("val:" + string(v.PubKey.Data)) + + pubkey := ed25519.PubKeyEd25519{} + copy(pubkey[:], v.PubKey.Data) + if v.Power == 0 { // remove validator if !app.app.state.db.Has(key) { @@ -183,6 +217,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)} } app.app.state.db.Delete(key) + delete(app.valAddrToPubKeyMap, string(pubkey.Address())) } else { // add or update validator value := bytes.NewBuffer(make([]byte, 0)) @@ -192,6 +227,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Error encoding validator: %v", err)} } app.app.state.db.Set(key, value.Bytes()) + app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey } // we only update the changes array if we successfully updated the tree diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 96cb844b7..3e1d775d7 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -127,11 +127,12 @@ func (s *SocketServer) acceptConnectionsRoutine() { func (s *SocketServer) waitForClose(closeConn chan error, connID int) { err := <-closeConn - if err == io.EOF { + switch { + case err == io.EOF: s.Logger.Error("Connection was closed by client") - } else if err != nil { + case err != nil: s.Logger.Error("Connection error", "error", err) - } else { + default: // never happens s.Logger.Error("Connection was closed.") } @@ -146,6 +147,16 @@ func (s *SocketServer) waitForClose(closeConn chan error, connID int) { func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, responses chan<- *types.Response) { var count int var bufReader = bufio.NewReader(conn) + + defer func() { + // make sure to recover from any app-related panics to allow proper socket cleanup + r := recover() + if r != nil { + closeConn <- fmt.Errorf("recovered from panic: %v", r) + s.appMtx.Unlock() + } + }() + for { var req = &types.Request{} @@ -154,7 +165,7 @@ func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, respo if err == io.EOF { closeConn <- err } else { - closeConn <- fmt.Errorf("Error reading message: %v", err.Error()) + closeConn <- fmt.Errorf("error reading message: %v", err) } return } diff --git a/blockchain/wire.go b/blockchain/v0/codec.go similarity index 91% rename from blockchain/wire.go rename to blockchain/v0/codec.go index 487fbe2bc..4494f41aa 100644 --- a/blockchain/wire.go +++ b/blockchain/v0/codec.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( amino "github.com/tendermint/go-amino" diff --git a/blockchain/pool.go b/blockchain/v0/pool.go similarity index 95% rename from blockchain/pool.go rename to blockchain/v0/pool.go index c842c0d13..7733dc5ff 100644 --- a/blockchain/pool.go +++ b/blockchain/v0/pool.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "errors" @@ -59,6 +59,7 @@ var peerTimeout = 15 * time.Second // not const so we can override with tests are not at peer limits, we can probably switch to consensus reactor */ +// BlockPool keeps track of the fast sync peers, block requests and block responses. type BlockPool struct { cmn.BaseService startTime time.Time @@ -111,17 +112,18 @@ func (pool *BlockPool) makeRequestersRoutine() { } _, numPending, lenRequesters := pool.GetStatus() - if numPending >= maxPendingRequests { + switch { + case numPending >= maxPendingRequests: // sleep for a bit. time.Sleep(requestIntervalMS * time.Millisecond) // check for timed out peers pool.removeTimedoutPeers() - } else if lenRequesters >= maxTotalRequesters { + case lenRequesters >= maxTotalRequesters: // sleep for a bit. time.Sleep(requestIntervalMS * time.Millisecond) // check for timed out peers pool.removeTimedoutPeers() - } else { + default: // request for more blocks. pool.makeNextRequester() } @@ -184,6 +186,7 @@ func (pool *BlockPool) IsCaughtUp() bool { return isCaughtUp } +// PeekTwoBlocks returns blocks at pool.height and pool.height+1. // We need to see the second block's Commit to validate the first block. // So we peek two blocks at a time. // The caller will verify the commit. @@ -200,7 +203,7 @@ func (pool *BlockPool) PeekTwoBlocks() (first *types.Block, second *types.Block) return } -// Pop the first block at pool.height +// PopRequest pops the first block at pool.height. // It must have been validated by 'second'.Commit from PeekTwoBlocks(). func (pool *BlockPool) PopRequest() { pool.mtx.Lock() @@ -220,7 +223,7 @@ func (pool *BlockPool) PopRequest() { } } -// Invalidates the block at pool.height, +// RedoRequest invalidates the block at pool.height, // Remove the peer and redo request from others. // Returns the ID of the removed peer. func (pool *BlockPool) RedoRequest(height int64) p2p.ID { @@ -236,6 +239,7 @@ func (pool *BlockPool) RedoRequest(height int64) p2p.ID { return peerID } +// AddBlock validates that the block comes from the peer it was expected from and calls the requester to store it. // TODO: ensure that blocks come in order for each peer. func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) { pool.mtx.Lock() @@ -565,9 +569,9 @@ func (bpr *bpRequester) reset() { // Tells bpRequester to pick another peer and try again. // NOTE: Nonblocking, and does nothing if another redo // was already requested. -func (bpr *bpRequester) redo(peerId p2p.ID) { +func (bpr *bpRequester) redo(peerID p2p.ID) { select { - case bpr.redoCh <- peerId: + case bpr.redoCh <- peerID: default: } } @@ -622,8 +626,8 @@ OUTER_LOOP: } } -//------------------------------------- - +// BlockRequest stores a block request identified by the block Height and the PeerID responsible for +// delivering the block type BlockRequest struct { Height int64 PeerID p2p.ID diff --git a/blockchain/pool_test.go b/blockchain/v0/pool_test.go similarity index 99% rename from blockchain/pool_test.go rename to blockchain/v0/pool_test.go index 01d7dba20..d741d59df 100644 --- a/blockchain/pool_test.go +++ b/blockchain/v0/pool_test.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "fmt" diff --git a/blockchain/reactor.go b/blockchain/v0/reactor.go similarity index 96% rename from blockchain/reactor.go rename to blockchain/v0/reactor.go index 139393778..574ef3f29 100644 --- a/blockchain/reactor.go +++ b/blockchain/v0/reactor.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "errors" @@ -11,6 +11,7 @@ import ( "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -60,7 +61,7 @@ type BlockchainReactor struct { initialState sm.State blockExec *sm.BlockExecutor - store *BlockStore + store *store.BlockStore pool *BlockPool fastSync bool @@ -69,7 +70,7 @@ type BlockchainReactor struct { } // NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, +func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, fastSync bool) *BlockchainReactor { if state.LastBlockHeight != store.Height() { @@ -140,9 +141,9 @@ func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor by sending our state to peer. func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) - if !peer.Send(BlockchainChannel, msgBytes) { - // doing nothing, will try later in `poolRoutine` - } + peer.Send(BlockchainChannel, msgBytes) + // it's OK if send fails. will try later in poolRoutine + // peer is added to the pool once we receive the first // bcStatusResponseMessage from the peer and call pool.SetPeerHeight } @@ -190,18 +191,13 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) switch msg := msg.(type) { case *bcBlockRequestMessage: - if queued := bcR.respondToPeer(msg, src); !queued { - // Unfortunately not queued since the queue is full. - } + bcR.respondToPeer(msg, src) case *bcBlockResponseMessage: bcR.pool.AddBlock(src.ID(), msg.Block, len(msgBytes)) case *bcStatusRequestMessage: // Send peer our state. msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) - queued := src.TrySend(BlockchainChannel, msgBytes) - if !queued { - // sorry - } + src.TrySend(BlockchainChannel, msgBytes) case *bcStatusResponseMessage: // Got a peer status. Unverified. bcR.pool.SetPeerHeight(src.ID(), msg.Height) @@ -273,9 +269,10 @@ FOR_LOOP: conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) if ok { conR.SwitchToConsensus(state, blocksSynced) - } else { - // should only happen during testing } + // else { + // should only happen during testing + // } break FOR_LOOP } @@ -378,6 +375,7 @@ 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) @@ -425,8 +423,8 @@ func (m *bcNoBlockResponseMessage) ValidateBasic() error { return nil } -func (brm *bcNoBlockResponseMessage) String() string { - return fmt.Sprintf("[bcNoBlockResponseMessage %d]", brm.Height) +func (m *bcNoBlockResponseMessage) String() string { + return fmt.Sprintf("[bcNoBlockResponseMessage %d]", m.Height) } //------------------------------------- diff --git a/blockchain/reactor_test.go b/blockchain/v0/reactor_test.go similarity index 74% rename from blockchain/reactor_test.go rename to blockchain/v0/reactor_test.go index b5137bb2a..1ba43400e 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "os" @@ -6,12 +6,13 @@ import ( "testing" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/store" + "github.com/stretchr/testify/assert" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/p2p" @@ -19,6 +20,7 @@ import ( sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-db" ) var config *cfg.Config @@ -43,24 +45,6 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G }, privValidators } -func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { - addr := privVal.GetPubKey().Address() - idx, _ := valset.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - - privVal.SignVote(header.ChainID, vote) - - return vote -} - type BlockchainReactorPair struct { reactor *BlockchainReactor app proxy.AppConns @@ -76,16 +60,16 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals proxyApp := proxy.NewAppConns(cc) err := proxyApp.Start() if err != nil { - panic(cmn.ErrorWrap(err, "error start app")) + panic(errors.Wrap(err, "error start app")) } blockDB := dbm.NewMemDB() stateDB := dbm.NewMemDB() - blockStore := NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + panic(errors.Wrap(err, "error constructing state from genesis file")) } // Make the BlockchainReactor itself. @@ -104,8 +88,12 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) lastBlock := blockStore.LoadBlock(blockHeight - 1) - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + vote, err := types.MakeVote(lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVals[0], lastBlock.Header.ChainID) + if err != nil { + panic(err) + } + voteCommitSig := vote.CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{voteCommitSig}) } thisBlock := makeBlock(blockHeight, state, lastCommit) @@ -115,7 +103,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals state, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { - panic(cmn.ErrorWrap(err, "error apply block")) + panic(errors.Wrap(err, "error apply block")) } blockStore.SaveBlock(thisBlock, thisParts, lastCommit) @@ -258,6 +246,82 @@ func TestBadBlockStopsPeer(t *testing.T) { assert.True(t, lastReactorPair.reactor.Switch.Peers().Size() < len(reactorPairs)-1) } +func TestBcBlockRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcBlockRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + nonResponseHeight int64 + expectErr bool + }{ + {"Valid Non-Response Message", 0, false}, + {"Valid Non-Response Message", 1, false}, + {"Invalid Non-Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} + assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcStatusRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + responseHeight int64 + expectErr bool + }{ + {"Valid Response Message", 0, false}, + {"Valid Response Message", 1, false}, + {"Invalid Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + response := bcStatusResponseMessage{Height: tc.responseHeight} + assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + //---------------------------------------------- // utility funcs diff --git a/blockchain/v1/codec.go b/blockchain/v1/codec.go new file mode 100644 index 000000000..786584435 --- /dev/null +++ b/blockchain/v1/codec.go @@ -0,0 +1,13 @@ +package v1 + +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/v1/peer.go b/blockchain/v1/peer.go new file mode 100644 index 000000000..02b1b4fc1 --- /dev/null +++ b/blockchain/v1/peer.go @@ -0,0 +1,209 @@ +package v1 + +import ( + "fmt" + "math" + "time" + + flow "github.com/tendermint/tendermint/libs/flowrate" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +//-------- +// Peer + +// BpPeerParams stores the peer parameters that are used when creating a peer. +type BpPeerParams struct { + timeout time.Duration + minRecvRate int64 + sampleRate time.Duration + windowSize time.Duration +} + +// BpPeer is the datastructure associated with a fast sync peer. +type BpPeer struct { + logger log.Logger + ID p2p.ID + + Height int64 // the peer reported height + NumPendingBlockRequests int // number of requests still waiting for block responses + blocks map[int64]*types.Block // blocks received or expected to be received from this peer + blockResponseTimer *time.Timer + recvMonitor *flow.Monitor + params *BpPeerParams // parameters for timer and monitor + + onErr func(err error, peerID p2p.ID) // function to call on error +} + +// NewBpPeer creates a new peer. +func NewBpPeer( + peerID p2p.ID, height int64, onErr func(err error, peerID p2p.ID), params *BpPeerParams) *BpPeer { + + if params == nil { + params = BpPeerDefaultParams() + } + return &BpPeer{ + ID: peerID, + Height: height, + blocks: make(map[int64]*types.Block, maxRequestsPerPeer), + logger: log.NewNopLogger(), + onErr: onErr, + params: params, + } +} + +// String returns a string representation of a peer. +func (peer *BpPeer) String() string { + return fmt.Sprintf("peer: %v height: %v pending: %v", peer.ID, peer.Height, peer.NumPendingBlockRequests) +} + +// SetLogger sets the logger of the peer. +func (peer *BpPeer) SetLogger(l log.Logger) { + peer.logger = l +} + +// Cleanup performs cleanup of the peer, removes blocks, requests, stops timer and monitor. +func (peer *BpPeer) Cleanup() { + if peer.blockResponseTimer != nil { + peer.blockResponseTimer.Stop() + } + if peer.NumPendingBlockRequests != 0 { + peer.logger.Info("peer with pending requests is being cleaned", "peer", peer.ID) + } + if len(peer.blocks)-peer.NumPendingBlockRequests != 0 { + peer.logger.Info("peer with pending blocks is being cleaned", "peer", peer.ID) + } + for h := range peer.blocks { + delete(peer.blocks, h) + } + peer.NumPendingBlockRequests = 0 + peer.recvMonitor = nil +} + +// BlockAtHeight returns the block at a given height if available and errMissingBlock otherwise. +func (peer *BpPeer) BlockAtHeight(height int64) (*types.Block, error) { + block, ok := peer.blocks[height] + if !ok { + return nil, errMissingBlock + } + if block == nil { + return nil, errMissingBlock + } + return peer.blocks[height], nil +} + +// AddBlock adds a block at peer level. Block must be non-nil and recvSize a positive integer +// The peer must have a pending request for this block. +func (peer *BpPeer) AddBlock(block *types.Block, recvSize int) error { + if block == nil || recvSize < 0 { + panic("bad parameters") + } + existingBlock, ok := peer.blocks[block.Height] + if !ok { + peer.logger.Error("unsolicited block", "blockHeight", block.Height, "peer", peer.ID) + return errMissingBlock + } + if existingBlock != nil { + peer.logger.Error("already have a block for height", "height", block.Height) + return errDuplicateBlock + } + if peer.NumPendingBlockRequests == 0 { + panic("peer does not have pending requests") + } + peer.blocks[block.Height] = block + peer.NumPendingBlockRequests-- + if peer.NumPendingBlockRequests == 0 { + peer.stopMonitor() + peer.stopBlockResponseTimer() + } else { + peer.recvMonitor.Update(recvSize) + peer.resetBlockResponseTimer() + } + return nil +} + +// RemoveBlock removes the block of given height +func (peer *BpPeer) RemoveBlock(height int64) { + delete(peer.blocks, height) +} + +// RequestSent records that a request was sent, and starts the peer timer and monitor if needed. +func (peer *BpPeer) RequestSent(height int64) { + peer.blocks[height] = nil + + if peer.NumPendingBlockRequests == 0 { + peer.startMonitor() + peer.resetBlockResponseTimer() + } + peer.NumPendingBlockRequests++ +} + +// CheckRate verifies that the response rate of the peer is acceptable (higher than the minimum allowed). +func (peer *BpPeer) CheckRate() error { + if peer.NumPendingBlockRequests == 0 { + return nil + } + curRate := peer.recvMonitor.Status().CurRate + // curRate can be 0 on start + if curRate != 0 && curRate < peer.params.minRecvRate { + err := errSlowPeer + peer.logger.Error("SendTimeout", "peer", peer, + "reason", err, + "curRate", fmt.Sprintf("%d KB/s", curRate/1024), + "minRate", fmt.Sprintf("%d KB/s", peer.params.minRecvRate/1024)) + return err + } + return nil +} + +func (peer *BpPeer) onTimeout() { + peer.onErr(errNoPeerResponse, peer.ID) +} + +func (peer *BpPeer) stopMonitor() { + peer.recvMonitor.Done() + peer.recvMonitor = nil +} + +func (peer *BpPeer) startMonitor() { + peer.recvMonitor = flow.New(peer.params.sampleRate, peer.params.windowSize) + initialValue := float64(peer.params.minRecvRate) * math.E + peer.recvMonitor.SetREMA(initialValue) +} + +func (peer *BpPeer) resetBlockResponseTimer() { + if peer.blockResponseTimer == nil { + peer.blockResponseTimer = time.AfterFunc(peer.params.timeout, peer.onTimeout) + } else { + peer.blockResponseTimer.Reset(peer.params.timeout) + } +} + +func (peer *BpPeer) stopBlockResponseTimer() bool { + if peer.blockResponseTimer == nil { + return false + } + return peer.blockResponseTimer.Stop() +} + +// BpPeerDefaultParams returns the default peer parameters. +func BpPeerDefaultParams() *BpPeerParams { + return &BpPeerParams{ + // Timeout for a peer to respond to a block request. + timeout: 15 * time.Second, + + // Minimum recv rate to ensure we're receiving blocks from a peer fast + // enough. If a peer is not sending data at at least that rate, we + // consider them to have timedout and we disconnect. + // + // Assuming a DSL connection (not a good choice) 128 Kbps (upload) ~ 15 KB/s, + // sending data across atlantic ~ 7.5 KB/s. + minRecvRate: int64(7680), + + // Monitor parameters + sampleRate: time.Second, + windowSize: 40 * time.Second, + } +} diff --git a/blockchain/v1/peer_test.go b/blockchain/v1/peer_test.go new file mode 100644 index 000000000..3c19e4efd --- /dev/null +++ b/blockchain/v1/peer_test.go @@ -0,0 +1,278 @@ +package v1 + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +func TestPeerMonitor(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + nil) + peer.SetLogger(log.TestingLogger()) + peer.startMonitor() + assert.NotNil(t, peer.recvMonitor) + peer.stopMonitor() + assert.Nil(t, peer.recvMonitor) +} + +func TestPeerResetBlockResponseTimer(t *testing.T) { + var ( + numErrFuncCalls int // number of calls to the errFunc + lastErr error // last generated error + peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine + ) + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) { + peerTestMtx.Lock() + defer peerTestMtx.Unlock() + lastErr = err + numErrFuncCalls++ + }, + params) + + peer.SetLogger(log.TestingLogger()) + checkByStoppingPeerTimer(t, peer, false) + + // initial reset call with peer having a nil timer + peer.resetBlockResponseTimer() + assert.NotNil(t, peer.blockResponseTimer) + // make sure timer is running and stop it + checkByStoppingPeerTimer(t, peer, true) + + // reset with running timer + peer.resetBlockResponseTimer() + time.Sleep(time.Millisecond) + peer.resetBlockResponseTimer() + assert.NotNil(t, peer.blockResponseTimer) + + // let the timer expire and ... + time.Sleep(3 * time.Millisecond) + // ... check timer is not running + checkByStoppingPeerTimer(t, peer, false) + + peerTestMtx.Lock() + // ... check errNoPeerResponse has been sent + assert.Equal(t, 1, numErrFuncCalls) + assert.Equal(t, lastErr, errNoPeerResponse) + peerTestMtx.Unlock() +} + +func TestPeerRequestSent(t *testing.T) { + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + + peer.SetLogger(log.TestingLogger()) + + peer.RequestSent(1) + assert.NotNil(t, peer.recvMonitor) + assert.NotNil(t, peer.blockResponseTimer) + assert.Equal(t, 1, peer.NumPendingBlockRequests) + + peer.RequestSent(1) + assert.NotNil(t, peer.recvMonitor) + assert.NotNil(t, peer.blockResponseTimer) + assert.Equal(t, 2, peer.NumPendingBlockRequests) +} + +func TestPeerGetAndRemoveBlock(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 100, + func(err error, _ p2p.ID) {}, + nil) + + // Change peer height + peer.Height = int64(10) + assert.Equal(t, int64(10), peer.Height) + + // request some blocks and receive few of them + for i := 1; i <= 10; i++ { + peer.RequestSent(int64(i)) + if i > 5 { + // only receive blocks 1..5 + continue + } + _ = peer.AddBlock(makeSmallBlock(i), 10) + } + + tests := []struct { + name string + height int64 + wantErr error + blockPresent bool + }{ + {"no request", 100, errMissingBlock, false}, + {"no block", 6, errMissingBlock, false}, + {"block 1 present", 1, nil, true}, + {"block max present", 5, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // try to get the block + b, err := peer.BlockAtHeight(tt.height) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.blockPresent, b != nil) + + // remove the block + peer.RemoveBlock(tt.height) + _, err = peer.BlockAtHeight(tt.height) + assert.Equal(t, errMissingBlock, err) + }) + } +} + +func TestPeerAddBlock(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 100, + func(err error, _ p2p.ID) {}, + nil) + + // request some blocks, receive one + for i := 1; i <= 10; i++ { + peer.RequestSent(int64(i)) + if i == 5 { + // receive block 5 + _ = peer.AddBlock(makeSmallBlock(i), 10) + } + } + + tests := []struct { + name string + height int64 + wantErr error + blockPresent bool + }{ + {"no request", 50, errMissingBlock, false}, + {"duplicate block", 5, errDuplicateBlock, true}, + {"block 1 successfully received", 1, nil, true}, + {"block max successfully received", 10, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // try to get the block + err := peer.AddBlock(makeSmallBlock(int(tt.height)), 10) + assert.Equal(t, tt.wantErr, err) + _, err = peer.BlockAtHeight(tt.height) + assert.Equal(t, tt.blockPresent, err == nil) + }) + } +} + +func TestPeerOnErrFuncCalledDueToExpiration(t *testing.T) { + + params := &BpPeerParams{timeout: 2 * time.Millisecond} + var ( + numErrFuncCalls int // number of calls to the onErr function + lastErr error // last generated error + peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine + ) + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) { + peerTestMtx.Lock() + defer peerTestMtx.Unlock() + lastErr = err + numErrFuncCalls++ + }, + params) + + peer.SetLogger(log.TestingLogger()) + + peer.RequestSent(1) + time.Sleep(4 * time.Millisecond) + // timer should have expired by now, check that the on error function was called + peerTestMtx.Lock() + assert.Equal(t, 1, numErrFuncCalls) + assert.Equal(t, errNoPeerResponse, lastErr) + peerTestMtx.Unlock() +} + +func TestPeerCheckRate(t *testing.T) { + params := &BpPeerParams{ + timeout: time.Second, + minRecvRate: int64(100), // 100 bytes/sec exponential moving average + } + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + peer.SetLogger(log.TestingLogger()) + + require.Nil(t, peer.CheckRate()) + + for i := 0; i < 40; i++ { + peer.RequestSent(int64(i)) + } + + // monitor starts with a higher rEMA (~ 2*minRecvRate), wait for it to go down + time.Sleep(900 * time.Millisecond) + + // normal peer - send a bit more than 100 bytes/sec, > 10 bytes/100msec, check peer is not considered slow + for i := 0; i < 10; i++ { + _ = peer.AddBlock(makeSmallBlock(i), 11) + time.Sleep(100 * time.Millisecond) + require.Nil(t, peer.CheckRate()) + } + + // slow peer - send a bit less than 10 bytes/100msec + for i := 10; i < 20; i++ { + _ = peer.AddBlock(makeSmallBlock(i), 9) + time.Sleep(100 * time.Millisecond) + } + // check peer is considered slow + assert.Equal(t, errSlowPeer, peer.CheckRate()) +} + +func TestPeerCleanup(t *testing.T) { + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + peer.SetLogger(log.TestingLogger()) + + assert.Nil(t, peer.blockResponseTimer) + peer.RequestSent(1) + assert.NotNil(t, peer.blockResponseTimer) + + peer.Cleanup() + checkByStoppingPeerTimer(t, peer, false) +} + +// Check if peer timer is running or not (a running timer can be successfully stopped). +// Note: stops the timer. +func checkByStoppingPeerTimer(t *testing.T, peer *BpPeer, running bool) { + assert.NotPanics(t, func() { + stopped := peer.stopBlockResponseTimer() + if running { + assert.True(t, stopped) + } else { + assert.False(t, stopped) + } + }) +} + +func makeSmallBlock(height int) *types.Block { + return types.MakeBlock(int64(height), []types.Tx{types.Tx("foo")}, nil, nil) +} diff --git a/blockchain/v1/pool.go b/blockchain/v1/pool.go new file mode 100644 index 000000000..5de741305 --- /dev/null +++ b/blockchain/v1/pool.go @@ -0,0 +1,369 @@ +package v1 + +import ( + "sort" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +// BlockPool keeps track of the fast sync peers, block requests and block responses. +type BlockPool struct { + logger log.Logger + // Set of peers that have sent status responses, with height bigger than pool.Height + peers map[p2p.ID]*BpPeer + // Set of block heights and the corresponding peers from where a block response is expected or has been received. + blocks map[int64]p2p.ID + + plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest + nextRequestHeight int64 // next height to be added to plannedRequests + + Height int64 // height of next block to execute + MaxPeerHeight int64 // maximum height of all peers + toBcR bcReactor +} + +// NewBlockPool creates a new BlockPool. +func NewBlockPool(height int64, toBcR bcReactor) *BlockPool { + return &BlockPool{ + Height: height, + MaxPeerHeight: 0, + peers: make(map[p2p.ID]*BpPeer), + blocks: make(map[int64]p2p.ID), + plannedRequests: make(map[int64]struct{}), + nextRequestHeight: height, + toBcR: toBcR, + } +} + +// SetLogger sets the logger of the pool. +func (pool *BlockPool) SetLogger(l log.Logger) { + pool.logger = l +} + +// ReachedMaxHeight check if the pool has reached the maximum peer height. +func (pool *BlockPool) ReachedMaxHeight() bool { + return pool.Height >= pool.MaxPeerHeight +} + +func (pool *BlockPool) rescheduleRequest(peerID p2p.ID, height int64) { + pool.logger.Info("reschedule requests made to peer for height ", "peerID", peerID, "height", height) + pool.plannedRequests[height] = struct{}{} + delete(pool.blocks, height) + pool.peers[peerID].RemoveBlock(height) +} + +// Updates the pool's max height. If no peers are left MaxPeerHeight is set to 0. +func (pool *BlockPool) updateMaxPeerHeight() { + var newMax int64 + for _, peer := range pool.peers { + peerHeight := peer.Height + if peerHeight > newMax { + newMax = peerHeight + } + } + pool.MaxPeerHeight = newMax +} + +// UpdatePeer adds a new peer or updates an existing peer with a new height. +// If a peer is short it is not added. +func (pool *BlockPool) UpdatePeer(peerID p2p.ID, height int64) error { + + peer := pool.peers[peerID] + + if peer == nil { + if height < pool.Height { + pool.logger.Info("Peer height too small", + "peer", peerID, "height", height, "fsm_height", pool.Height) + return errPeerTooShort + } + // Add new peer. + peer = NewBpPeer(peerID, height, pool.toBcR.sendPeerError, nil) + peer.SetLogger(pool.logger.With("peer", peerID)) + pool.peers[peerID] = peer + pool.logger.Info("added peer", "peerID", peerID, "height", height, "num_peers", len(pool.peers)) + } else { + // Check if peer is lowering its height. This is not allowed. + if height < peer.Height { + pool.RemovePeer(peerID, errPeerLowersItsHeight) + return errPeerLowersItsHeight + } + // Update existing peer. + peer.Height = height + } + + // Update the pool's MaxPeerHeight if needed. + pool.updateMaxPeerHeight() + + return nil +} + +// Cleans and deletes the peer. Recomputes the max peer height. +func (pool *BlockPool) deletePeer(peer *BpPeer) { + if peer == nil { + return + } + peer.Cleanup() + delete(pool.peers, peer.ID) + + if peer.Height == pool.MaxPeerHeight { + pool.updateMaxPeerHeight() + } +} + +// RemovePeer removes the blocks and requests from the peer, reschedules them and deletes the peer. +func (pool *BlockPool) RemovePeer(peerID p2p.ID, err error) { + peer := pool.peers[peerID] + if peer == nil { + return + } + pool.logger.Info("removing peer", "peerID", peerID, "error", err) + + // Reschedule the block requests made to the peer, or received and not processed yet. + // Note that some of the requests may be removed further down. + for h := range pool.peers[peerID].blocks { + pool.rescheduleRequest(peerID, h) + } + + oldMaxPeerHeight := pool.MaxPeerHeight + // Delete the peer. This operation may result in the pool's MaxPeerHeight being lowered. + pool.deletePeer(peer) + + // Check if the pool's MaxPeerHeight has been lowered. + // This may happen if the tallest peer has been removed. + if oldMaxPeerHeight > pool.MaxPeerHeight { + // Remove any planned requests for heights over the new MaxPeerHeight. + for h := range pool.plannedRequests { + if h > pool.MaxPeerHeight { + delete(pool.plannedRequests, h) + } + } + // Adjust the nextRequestHeight to the new max plus one. + if pool.nextRequestHeight > pool.MaxPeerHeight { + pool.nextRequestHeight = pool.MaxPeerHeight + 1 + } + } +} + +func (pool *BlockPool) removeShortPeers() { + for _, peer := range pool.peers { + if peer.Height < pool.Height { + pool.RemovePeer(peer.ID, nil) + } + } +} + +func (pool *BlockPool) removeBadPeers() { + pool.removeShortPeers() + for _, peer := range pool.peers { + if err := peer.CheckRate(); err != nil { + pool.RemovePeer(peer.ID, err) + pool.toBcR.sendPeerError(err, peer.ID) + } + } +} + +// MakeNextRequests creates more requests if the block pool is running low. +func (pool *BlockPool) MakeNextRequests(maxNumRequests int) { + heights := pool.makeRequestBatch(maxNumRequests) + if len(heights) != 0 { + pool.logger.Info("makeNextRequests will make following requests", + "number", len(heights), "heights", heights) + } + + for _, height := range heights { + h := int64(height) + if !pool.sendRequest(h) { + // If a good peer was not found for sending the request at height h then return, + // as it shouldn't be possible to find a peer for h+1. + return + } + delete(pool.plannedRequests, h) + } +} + +// Makes a batch of requests sorted by height such that the block pool has up to maxNumRequests entries. +func (pool *BlockPool) makeRequestBatch(maxNumRequests int) []int { + pool.removeBadPeers() + // At this point pool.requests may include heights for requests to be redone due to removal of peers: + // - peers timed out or were removed by switch + // - FSM timed out on waiting to advance the block execution due to missing blocks at h or h+1 + // Determine the number of requests needed by subtracting the number of requests already made from the maximum + // allowed + numNeeded := int(maxNumRequests) - len(pool.blocks) + for len(pool.plannedRequests) < numNeeded { + if pool.nextRequestHeight > pool.MaxPeerHeight { + break + } + pool.plannedRequests[pool.nextRequestHeight] = struct{}{} + pool.nextRequestHeight++ + } + + heights := make([]int, 0, len(pool.plannedRequests)) + for k := range pool.plannedRequests { + heights = append(heights, int(k)) + } + sort.Ints(heights) + return heights +} + +func (pool *BlockPool) sendRequest(height int64) bool { + for _, peer := range pool.peers { + if peer.NumPendingBlockRequests >= maxRequestsPerPeer { + continue + } + if peer.Height < height { + continue + } + + err := pool.toBcR.sendBlockRequest(peer.ID, height) + if err == errNilPeerForBlockRequest { + // Switch does not have this peer, remove it and continue to look for another peer. + pool.logger.Error("switch does not have peer..removing peer selected for height", "peer", + peer.ID, "height", height) + pool.RemovePeer(peer.ID, err) + continue + } + + if err == errSendQueueFull { + pool.logger.Error("peer queue is full", "peer", peer.ID, "height", height) + continue + } + + pool.logger.Info("assigned request to peer", "peer", peer.ID, "height", height) + + pool.blocks[height] = peer.ID + peer.RequestSent(height) + + return true + } + pool.logger.Error("could not find peer to send request for block at height", "height", height) + return false +} + +// AddBlock validates that the block comes from the peer it was expected from and stores it in the 'blocks' map. +func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) error { + peer, ok := pool.peers[peerID] + if !ok { + pool.logger.Error("block from unknown peer", "height", block.Height, "peer", peerID) + return errBadDataFromPeer + } + if wantPeerID, ok := pool.blocks[block.Height]; ok && wantPeerID != peerID { + pool.logger.Error("block received from wrong peer", "height", block.Height, + "peer", peerID, "expected_peer", wantPeerID) + return errBadDataFromPeer + } + + return peer.AddBlock(block, blockSize) +} + +// BlockData stores the peer responsible to deliver a block and the actual block if delivered. +type BlockData struct { + block *types.Block + peer *BpPeer +} + +// BlockAndPeerAtHeight retrieves the block and delivery peer at specified height. +// Returns errMissingBlock if a block was not found +func (pool *BlockPool) BlockAndPeerAtHeight(height int64) (bData *BlockData, err error) { + peerID := pool.blocks[height] + peer := pool.peers[peerID] + if peer == nil { + return nil, errMissingBlock + } + + block, err := peer.BlockAtHeight(height) + if err != nil { + return nil, err + } + + return &BlockData{peer: peer, block: block}, nil + +} + +// FirstTwoBlocksAndPeers returns the blocks and the delivery peers at pool's height H and H+1. +func (pool *BlockPool) FirstTwoBlocksAndPeers() (first, second *BlockData, err error) { + first, err = pool.BlockAndPeerAtHeight(pool.Height) + second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) + if err == nil { + err = err2 + } + return +} + +// InvalidateFirstTwoBlocks removes the peers that sent us the first two blocks, blocks are removed by RemovePeer(). +func (pool *BlockPool) InvalidateFirstTwoBlocks(err error) { + first, err1 := pool.BlockAndPeerAtHeight(pool.Height) + second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) + + if err1 == nil { + pool.RemovePeer(first.peer.ID, err) + } + if err2 == nil { + pool.RemovePeer(second.peer.ID, err) + } +} + +// ProcessedCurrentHeightBlock performs cleanup after a block is processed. It removes block at pool height and +// the peers that are now short. +func (pool *BlockPool) ProcessedCurrentHeightBlock() { + peerID, peerOk := pool.blocks[pool.Height] + if peerOk { + pool.peers[peerID].RemoveBlock(pool.Height) + } + delete(pool.blocks, pool.Height) + pool.logger.Debug("removed block at height", "height", pool.Height) + pool.Height++ + pool.removeShortPeers() +} + +// RemovePeerAtCurrentHeights checks if a block at pool's height H exists and if not, it removes the +// delivery peer and returns. If a block at height H exists then the check and peer removal is done for H+1. +// This function is called when the FSM is not able to make progress for some time. +// This happens if either the block H or H+1 have not been delivered. +func (pool *BlockPool) RemovePeerAtCurrentHeights(err error) { + peerID := pool.blocks[pool.Height] + peer, ok := pool.peers[peerID] + if ok { + if _, err := peer.BlockAtHeight(pool.Height); err != nil { + pool.logger.Info("remove peer that hasn't sent block at pool.Height", + "peer", peerID, "height", pool.Height) + pool.RemovePeer(peerID, err) + return + } + } + peerID = pool.blocks[pool.Height+1] + peer, ok = pool.peers[peerID] + if ok { + if _, err := peer.BlockAtHeight(pool.Height + 1); err != nil { + pool.logger.Info("remove peer that hasn't sent block at pool.Height+1", + "peer", peerID, "height", pool.Height+1) + pool.RemovePeer(peerID, err) + return + } + } +} + +// Cleanup performs pool and peer cleanup +func (pool *BlockPool) Cleanup() { + for id, peer := range pool.peers { + peer.Cleanup() + delete(pool.peers, id) + } + pool.plannedRequests = make(map[int64]struct{}) + pool.blocks = make(map[int64]p2p.ID) + pool.nextRequestHeight = 0 + pool.Height = 0 + pool.MaxPeerHeight = 0 +} + +// NumPeers returns the number of peers in the pool +func (pool *BlockPool) NumPeers() int { + return len(pool.peers) +} + +// NeedsBlocks returns true if more blocks are required. +func (pool *BlockPool) NeedsBlocks() bool { + return len(pool.blocks) < maxNumRequests +} diff --git a/blockchain/v1/pool_test.go b/blockchain/v1/pool_test.go new file mode 100644 index 000000000..72758d3b1 --- /dev/null +++ b/blockchain/v1/pool_test.go @@ -0,0 +1,650 @@ +package v1 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +type testPeer struct { + id p2p.ID + height int64 +} + +type testBcR struct { + logger log.Logger +} + +type testValues struct { + numRequestsSent int +} + +var testResults testValues + +func resetPoolTestResults() { + testResults.numRequestsSent = 0 +} + +func (testR *testBcR) sendPeerError(err error, peerID p2p.ID) { +} + +func (testR *testBcR) sendStatusRequest() { +} + +func (testR *testBcR) sendBlockRequest(peerID p2p.ID, height int64) error { + testResults.numRequestsSent++ + return nil +} + +func (testR *testBcR) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { +} + +func (testR *testBcR) switchToConsensus() { + +} + +func newTestBcR() *testBcR { + testBcR := &testBcR{logger: log.TestingLogger()} + return testBcR +} + +type tPBlocks struct { + id p2p.ID + create bool +} + +// Makes a block pool with specified current height, list of peers, block requests and block responses +func makeBlockPool(bcr *testBcR, height int64, peers []BpPeer, blocks map[int64]tPBlocks) *BlockPool { + bPool := NewBlockPool(height, bcr) + bPool.SetLogger(bcr.logger) + + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + var maxH int64 + for _, p := range peers { + if p.Height > maxH { + maxH = p.Height + } + bPool.peers[p.ID] = NewBpPeer(p.ID, p.Height, bcr.sendPeerError, nil) + bPool.peers[p.ID].SetLogger(bcr.logger) + + } + bPool.MaxPeerHeight = maxH + for h, p := range blocks { + bPool.blocks[h] = p.id + bPool.peers[p.id].RequestSent(int64(h)) + if p.create { + // simulate that a block at height h has been received + _ = bPool.peers[p.id].AddBlock(types.MakeBlock(int64(h), txs, nil, nil), 100) + } + } + return bPool +} + +func assertPeerSetsEquivalent(t *testing.T, set1 map[p2p.ID]*BpPeer, set2 map[p2p.ID]*BpPeer) { + assert.Equal(t, len(set1), len(set2)) + for peerID, peer1 := range set1 { + peer2 := set2[peerID] + assert.NotNil(t, peer2) + assert.Equal(t, peer1.NumPendingBlockRequests, peer2.NumPendingBlockRequests) + assert.Equal(t, peer1.Height, peer2.Height) + assert.Equal(t, len(peer1.blocks), len(peer2.blocks)) + for h, block1 := range peer1.blocks { + block2 := peer2.blocks[h] + // block1 and block2 could be nil if a request was made but no block was received + assert.Equal(t, block1, block2) + } + } +} + +func assertBlockPoolEquivalent(t *testing.T, poolWanted, pool *BlockPool) { + assert.Equal(t, poolWanted.blocks, pool.blocks) + assertPeerSetsEquivalent(t, poolWanted.peers, pool.peers) + assert.Equal(t, poolWanted.MaxPeerHeight, pool.MaxPeerHeight) + assert.Equal(t, poolWanted.Height, pool.Height) + +} + +func TestBlockPoolUpdatePeer(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + args testPeer + poolWanted *BlockPool + errWanted error + }{ + { + name: "add a first short peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + args: testPeer{"P1", 50}, + errWanted: errPeerTooShort, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "add a first good peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + args: testPeer{"P1", 101}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 101}}, map[int64]tPBlocks{}), + }, + { + name: "increase the height of P1 from 120 to 123", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: testPeer{"P1", 123}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 123}}, map[int64]tPBlocks{}), + }, + { + name: "decrease the height of P1 from 120 to 110", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: testPeer{"P1", 110}, + errWanted: errPeerLowersItsHeight, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "decrease the height of P1 from 105 to 102 with blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 105}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 101: {"P1", true}, 102: {"P1", true}}), + args: testPeer{"P1", 102}, + errWanted: errPeerLowersItsHeight, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, + map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + err := pool.UpdatePeer(tt.args.id, tt.args.height) + assert.Equal(t, tt.errWanted, err) + assert.Equal(t, tt.poolWanted.blocks, tt.pool.blocks) + assertPeerSetsEquivalent(t, tt.poolWanted.peers, tt.pool.peers) + assert.Equal(t, tt.poolWanted.MaxPeerHeight, tt.pool.MaxPeerHeight) + }) + } +} + +func TestBlockPoolRemovePeer(t *testing.T) { + testBcR := newTestBcR() + + type args struct { + peerID p2p.ID + err error + } + + tests := []struct { + name string + pool *BlockPool + args args + poolWanted *BlockPool + }{ + { + name: "attempt to delete non-existing peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P99", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + }, + { + name: "delete the only peer without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "delete the shortest of two peers without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + }, + { + name: "delete the tallest of two peers without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P2", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + }, + { + name: "delete the only peer with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "delete the shortest of two peers with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 200}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 200}}, map[int64]tPBlocks{}), + }, + { + name: "delete the tallest of two peers with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 110}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 110}}, map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.RemovePeer(tt.args.peerID, tt.args.err) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolRemoveShortPeers(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "no short peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + }, + + { + name: "one short peer", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 90}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + }, + + { + name: "all short peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 90}, {ID: "P2", Height: 91}, {ID: "P3", Height: 92}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + pool.removeShortPeers() + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolSendRequestBatch(t *testing.T) { + type testPeerResult struct { + id p2p.ID + numPendingBlockRequests int + } + + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + maxRequestsPerPeer int + expRequests map[int64]bool + expPeerResults []testPeerResult + expnumPendingBlockRequests int + }{ + { + name: "one peer - send up to maxRequestsPerPeer block requests", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + maxRequestsPerPeer: 2, + expRequests: map[int64]bool{10: true, 11: true}, + expPeerResults: []testPeerResult{{id: "P1", numPendingBlockRequests: 2}}, + expnumPendingBlockRequests: 2, + }, + { + name: "n peers - send n*maxRequestsPerPeer block requests", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, map[int64]tPBlocks{}), + maxRequestsPerPeer: 2, + expRequests: map[int64]bool{10: true, 11: true}, + expPeerResults: []testPeerResult{ + {id: "P1", numPendingBlockRequests: 2}, + {id: "P2", numPendingBlockRequests: 2}}, + expnumPendingBlockRequests: 4, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetPoolTestResults() + + var pool = tt.pool + maxRequestsPerPeer = tt.maxRequestsPerPeer + pool.MakeNextRequests(10) + assert.Equal(t, testResults.numRequestsSent, maxRequestsPerPeer*len(pool.peers)) + + for _, tPeer := range tt.expPeerResults { + var peer = pool.peers[tPeer.id] + assert.NotNil(t, peer) + assert.Equal(t, tPeer.numPendingBlockRequests, peer.NumPendingBlockRequests) + } + assert.Equal(t, testResults.numRequestsSent, maxRequestsPerPeer*len(pool.peers)) + + }) + } +} + +func TestBlockPoolAddBlock(t *testing.T) { + testBcR := newTestBcR() + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + type args struct { + peerID p2p.ID + block *types.Block + blockSize int + } + tests := []struct { + name string + pool *BlockPool + args args + poolWanted *BlockPool + errWanted error + }{ + {name: "block from unknown peer", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + args: args{ + peerID: "P2", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + errWanted: errBadDataFromPeer, + }, + {name: "unexpected block 11 from known peer - waiting for 10", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(11), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + errWanted: errMissingBlock, + }, + {name: "unexpected block 10 from known peer - already have 10", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), + errWanted: errDuplicateBlock, + }, + {name: "unexpected block 10 from known peer P2 - expected 10 to come from P1", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P2", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + errWanted: errBadDataFromPeer, + }, + {name: "expected block from known peer", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}}), + errWanted: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.pool.AddBlock(tt.args.peerID, tt.args.block, tt.args.blockSize) + assert.Equal(t, tt.errWanted, err) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolFirstTwoBlocksAndPeers(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + firstWanted int64 + secondWanted int64 + errWanted error + }{ + { + name: "both blocks missing", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + errWanted: errMissingBlock, + }, + { + name: "second block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), + firstWanted: 15, + errWanted: errMissingBlock, + }, + { + name: "first block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{16: {"P2", true}, 18: {"P2", true}}), + secondWanted: 16, + errWanted: errMissingBlock, + }, + { + name: "both blocks present", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), + firstWanted: 10, + secondWanted: 11, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + gotFirst, gotSecond, err := pool.FirstTwoBlocksAndPeers() + assert.Equal(t, tt.errWanted, err) + + if tt.firstWanted != 0 { + peer := pool.blocks[tt.firstWanted] + block := pool.peers[peer].blocks[tt.firstWanted] + assert.Equal(t, block, gotFirst.block, + "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", + tt.firstWanted, gotFirst.block.Height) + } + + if tt.secondWanted != 0 { + peer := pool.blocks[tt.secondWanted] + block := pool.peers[peer].blocks[tt.secondWanted] + assert.Equal(t, block, gotSecond.block, + "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", + tt.secondWanted, gotSecond.block.Height) + } + }) + } +} + +func TestBlockPoolInvalidateFirstTwoBlocks(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "both blocks missing", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + }, + { + name: "second block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P2", Height: 100}}, + map[int64]tPBlocks{18: {"P2", true}}), + }, + { + name: "first block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{18: {"P1", true}, 16: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{18: {"P1", true}}), + }, + { + name: "both blocks present", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{}, + map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.InvalidateFirstTwoBlocks(errNoPeerResponse) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestProcessedCurrentHeightBlock(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "one peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", true}}), + poolWanted: makeBlockPool(testBcR, 101, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{101: {"P1", true}}), + }, + { + name: "multiple peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 101, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.ProcessedCurrentHeightBlock() + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestRemovePeerAtCurrentHeight(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "one peer, remove peer for block at H", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", false}, 101: {"P1", true}}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "one peer, remove peer for block at H+1", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "multiple peers, remove peer for block at H", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", false}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + { + name: "multiple peers, remove peer for block at H+1", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", false}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.RemovePeerAtCurrentHeights(errNoPeerResponse) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go new file mode 100644 index 000000000..480b87f34 --- /dev/null +++ b/blockchain/v1/reactor.go @@ -0,0 +1,622 @@ +package v1 + +import ( + "errors" + "fmt" + "reflect" + "time" + + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/behaviour" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" +) + +const ( + // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) + BlockchainChannel = byte(0x40) + trySyncIntervalMS = 10 + trySendIntervalMS = 10 + + // ask for best height every 10s + statusUpdateIntervalSeconds = 10 + + // NOTE: keep up to date with bcBlockResponseMessage + bcBlockResponseMessagePrefixSize = 4 + bcBlockResponseMessageFieldKeySize = 1 + maxMsgSize = types.MaxBlockSizeBytes + + bcBlockResponseMessagePrefixSize + + bcBlockResponseMessageFieldKeySize +) + +var ( + // Maximum number of requests that can be pending per peer, i.e. for which requests have been sent but blocks + // have not been received. + maxRequestsPerPeer = 20 + // Maximum number of block requests for the reactor, pending or for which blocks have been received. + maxNumRequests = 64 +) + +type consensusReactor interface { + // for when we switch from blockchain reactor and fast sync to + // the consensus machine + SwitchToConsensus(sm.State, int) +} + +// BlockchainReactor handles long-term catchup syncing. +type BlockchainReactor struct { + p2p.BaseReactor + + initialState sm.State // immutable + state sm.State + + blockExec *sm.BlockExecutor + store *store.BlockStore + + fastSync bool + + fsm *BcReactorFSM + blocksSynced int + + // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. + messagesForFSMCh chan bcReactorMessage + + // Switch goroutine may send RemovePeer to the blockchain reactor. This is an error message that is relayed + // to this channel to be processed in the context of the poolRoutine. + errorsForFSMCh chan bcReactorMessage + + // This channel is used by the FSM and indirectly the block pool to report errors to the blockchain reactor and + // the switch. + eventsFromFSMCh chan bcFsmMessage + + swReporter *behaviour.SwitchReporter +} + +// NewBlockchainReactor returns new reactor instance. +func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, + fastSync bool) *BlockchainReactor { + + if state.LastBlockHeight != store.Height() { + panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, + store.Height())) + } + + const capacity = 1000 + eventsFromFSMCh := make(chan bcFsmMessage, capacity) + messagesForFSMCh := make(chan bcReactorMessage, capacity) + errorsForFSMCh := make(chan bcReactorMessage, capacity) + + startHeight := store.Height() + 1 + bcR := &BlockchainReactor{ + initialState: state, + state: state, + blockExec: blockExec, + fastSync: fastSync, + store: store, + messagesForFSMCh: messagesForFSMCh, + eventsFromFSMCh: eventsFromFSMCh, + errorsForFSMCh: errorsForFSMCh, + } + fsm := NewFSM(startHeight, bcR) + bcR.fsm = fsm + bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) + //bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + + return bcR +} + +// bcReactorMessage is used by the reactor to send messages to the FSM. +type bcReactorMessage struct { + event bReactorEvent + data bReactorEventData +} + +type bFsmEvent uint + +const ( + // message type events + peerErrorEv = iota + 1 + syncFinishedEv +) + +type bFsmEventData struct { + peerID p2p.ID + err error +} + +// bcFsmMessage is used by the FSM to send messages to the reactor +type bcFsmMessage struct { + event bFsmEvent + data bFsmEventData +} + +// SetLogger implements cmn.Service by setting the logger on reactor and pool. +func (bcR *BlockchainReactor) SetLogger(l log.Logger) { + bcR.BaseService.Logger = l + bcR.fsm.SetLogger(l) +} + +// OnStart implements cmn.Service. +func (bcR *BlockchainReactor) OnStart() error { + bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + if bcR.fastSync { + go bcR.poolRoutine() + } + return nil +} + +// OnStop implements cmn.Service. +func (bcR *BlockchainReactor) OnStop() { + _ = bcR.Stop() +} + +// GetChannels implements Reactor +func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { + return []*p2p.ChannelDescriptor{ + { + ID: BlockchainChannel, + Priority: 10, + SendQueueCapacity: 2000, + RecvBufferCapacity: 50 * 4096, + RecvMessageCapacity: maxMsgSize, + }, + } +} + +// AddPeer implements Reactor by sending our state to peer. +func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) + peer.Send(BlockchainChannel, msgBytes) + // it's OK if send fails. will try later in poolRoutine + + // peer is added to the pool once we receive the first + // bcStatusResponseMessage from the peer and call pool.updatePeer() +} + +// sendBlockToPeer loads a block and sends it to the requesting peer. +// If the block doesn't exist a bcNoBlockResponseMessage is sent. +// If all nodes are honest, no node should be requesting for a block that doesn't exist. +func (bcR *BlockchainReactor) sendBlockToPeer(msg *bcBlockRequestMessage, + src p2p.Peer) (queued bool) { + + block := bcR.store.LoadBlock(msg.Height) + if block != nil { + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockResponseMessage{Block: block}) + return src.TrySend(BlockchainChannel, msgBytes) + } + + bcR.Logger.Info("peer asking for a block we don't have", "src", src, "height", msg.Height) + + msgBytes := cdc.MustMarshalBinaryBare(&bcNoBlockResponseMessage{Height: msg.Height}) + return src.TrySend(BlockchainChannel, msgBytes) +} + +func (bcR *BlockchainReactor) sendStatusResponseToPeer(msg *bcStatusRequestMessage, src p2p.Peer) (queued bool) { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) + return src.TrySend(BlockchainChannel, msgBytes) +} + +// RemovePeer implements Reactor by removing peer from the pool. +func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { + msgData := bcReactorMessage{ + event: peerRemoveEv, + data: bReactorEventData{ + peerID: peer.ID(), + err: errSwitchRemovesPeer, + }, + } + bcR.errorsForFSMCh <- msgData +} + +// Receive implements Reactor by handling 4 types of messages (look below). +func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := decodeMsg(msgBytes) + if err != nil { + bcR.Logger.Error("error decoding message", + "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + _ = bcR.swReporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + if err = msg.ValidateBasic(); err != nil { + bcR.Logger.Error("peer sent us invalid msg", "peer", src, "msg", msg, "err", err) + _ = bcR.swReporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + bcR.Logger.Debug("Receive", "src", src, "chID", chID, "msg", msg) + + switch msg := msg.(type) { + case *bcBlockRequestMessage: + if queued := bcR.sendBlockToPeer(msg, src); !queued { + // Unfortunately not queued since the queue is full. + bcR.Logger.Error("Could not send block message to peer", "src", src, "height", msg.Height) + } + + case *bcStatusRequestMessage: + // Send peer our state. + if queued := bcR.sendStatusResponseToPeer(msg, src); !queued { + // Unfortunately not queued since the queue is full. + bcR.Logger.Error("Could not send status message to peer", "src", src) + } + + case *bcBlockResponseMessage: + msgForFSM := bcReactorMessage{ + event: blockResponseEv, + data: bReactorEventData{ + peerID: src.ID(), + height: msg.Block.Height, + block: msg.Block, + length: len(msgBytes), + }, + } + bcR.Logger.Info("Received", "src", src, "height", msg.Block.Height) + bcR.messagesForFSMCh <- msgForFSM + + case *bcStatusResponseMessage: + // Got a peer status. Unverified. + msgForFSM := bcReactorMessage{ + event: statusResponseEv, + data: bReactorEventData{ + peerID: src.ID(), + height: msg.Height, + length: len(msgBytes), + }, + } + bcR.messagesForFSMCh <- msgForFSM + + default: + bcR.Logger.Error(fmt.Sprintf("unknown message type %v", reflect.TypeOf(msg))) + } +} + +// processBlocksRoutine processes blocks until signlaed to stop over the stopProcessing channel +func (bcR *BlockchainReactor) processBlocksRoutine(stopProcessing chan struct{}) { + + processReceivedBlockTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) + doProcessBlockCh := make(chan struct{}, 1) + + lastHundred := time.Now() + lastRate := 0.0 + +ForLoop: + for { + select { + case <-stopProcessing: + bcR.Logger.Info("finishing block execution") + break ForLoop + case <-processReceivedBlockTicker.C: // try to execute blocks + select { + case doProcessBlockCh <- struct{}{}: + default: + } + case <-doProcessBlockCh: + for { + err := bcR.processBlock() + if err == errMissingBlock { + break + } + // Notify FSM of block processing result. + msgForFSM := bcReactorMessage{ + event: processedBlockEv, + data: bReactorEventData{ + err: err, + }, + } + _ = bcR.fsm.Handle(&msgForFSM) + + if err != nil { + break + } + + bcR.blocksSynced++ + if bcR.blocksSynced%100 == 0 { + lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) + height, maxPeerHeight := bcR.fsm.Status() + bcR.Logger.Info("Fast Sync Rate", "height", height, + "max_peer_height", maxPeerHeight, "blocks/s", lastRate) + lastHundred = time.Now() + } + } + } + } +} + +// poolRoutine receives and handles messages from the Receive() routine and from the FSM. +func (bcR *BlockchainReactor) poolRoutine() { + + bcR.fsm.Start() + + sendBlockRequestTicker := time.NewTicker(trySendIntervalMS * time.Millisecond) + statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) + + stopProcessing := make(chan struct{}, 1) + go bcR.processBlocksRoutine(stopProcessing) + +ForLoop: + for { + select { + + case <-sendBlockRequestTicker.C: + if !bcR.fsm.NeedsBlocks() { + continue + } + _ = bcR.fsm.Handle(&bcReactorMessage{ + event: makeRequestsEv, + data: bReactorEventData{ + maxNumRequests: maxNumRequests}}) + + case <-statusUpdateTicker.C: + // Ask for status updates. + go bcR.sendStatusRequest() + + case msg := <-bcR.messagesForFSMCh: + // Sent from the Receive() routine when status (statusResponseEv) and + // block (blockResponseEv) response events are received + _ = bcR.fsm.Handle(&msg) + + case msg := <-bcR.errorsForFSMCh: + // Sent from the switch.RemovePeer() routine (RemovePeerEv) and + // FSM state timer expiry routine (stateTimeoutEv). + _ = bcR.fsm.Handle(&msg) + + case msg := <-bcR.eventsFromFSMCh: + switch msg.event { + case syncFinishedEv: + stopProcessing <- struct{}{} + // Sent from the FSM when it enters finished state. + break ForLoop + case peerErrorEv: + // Sent from the FSM when it detects peer error + bcR.reportPeerErrorToSwitch(msg.data.err, msg.data.peerID) + if msg.data.err == errNoPeerResponse { + // Sent from the peer timeout handler routine + _ = bcR.fsm.Handle(&bcReactorMessage{ + event: peerRemoveEv, + data: bReactorEventData{ + peerID: msg.data.peerID, + err: msg.data.err, + }, + }) + } + // else { + // For slow peers, or errors due to blocks received from wrong peer + // the FSM had already removed the peers + // } + default: + bcR.Logger.Error("Event from FSM not supported", "type", msg.event) + } + + case <-bcR.Quit(): + break ForLoop + } + } +} + +func (bcR *BlockchainReactor) reportPeerErrorToSwitch(err error, peerID p2p.ID) { + peer := bcR.Switch.Peers().Get(peerID) + if peer != nil { + _ = bcR.swReporter.Report(behaviour.BadMessage(peerID, err.Error())) + } +} + +func (bcR *BlockchainReactor) processBlock() error { + + first, second, err := bcR.fsm.FirstTwoBlocks() + if err != nil { + // We need both to sync the first block. + return err + } + + chainID := bcR.initialState.ChainID + + firstParts := first.MakePartSet(types.BlockPartSizeBytes) + firstPartsHeader := firstParts.Header() + firstID := types.BlockID{Hash: first.Hash(), PartsHeader: firstPartsHeader} + // Finally, verify the first block using the second's commit + // NOTE: we can probably make this more efficient, but note that calling + // first.Hash() doesn't verify the tx contents, so MakePartSet() is + // currently necessary. + err = bcR.state.Validators.VerifyCommit(chainID, firstID, first.Height, second.LastCommit) + if err != nil { + bcR.Logger.Error("error during commit verification", "err", err, + "first", first.Height, "second", second.Height) + return errBlockVerificationFailure + } + + bcR.store.SaveBlock(first, firstParts, second.LastCommit) + + bcR.state, err = bcR.blockExec.ApplyBlock(bcR.state, firstID, first) + if err != nil { + panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) + } + + return nil +} + +// Implements bcRNotifier +// sendStatusRequest broadcasts `BlockStore` height. +func (bcR *BlockchainReactor) sendStatusRequest() { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusRequestMessage{bcR.store.Height()}) + bcR.Switch.Broadcast(BlockchainChannel, msgBytes) +} + +// Implements bcRNotifier +// BlockRequest sends `BlockRequest` height. +func (bcR *BlockchainReactor) sendBlockRequest(peerID p2p.ID, height int64) error { + peer := bcR.Switch.Peers().Get(peerID) + if peer == nil { + return errNilPeerForBlockRequest + } + + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockRequestMessage{height}) + queued := peer.TrySend(BlockchainChannel, msgBytes) + if !queued { + return errSendQueueFull + } + return nil +} + +// Implements bcRNotifier +func (bcR *BlockchainReactor) switchToConsensus() { + conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) + if ok { + conR.SwitchToConsensus(bcR.state, bcR.blocksSynced) + bcR.eventsFromFSMCh <- bcFsmMessage{event: syncFinishedEv} + } + // else { + // Should only happen during testing. + // } +} + +// Implements bcRNotifier +// Called by FSM and pool: +// - pool calls when it detects slow peer or when peer times out +// - FSM calls when: +// - adding a block (addBlock) fails +// - reactor processing of a block reports failure and FSM sends back the peers of first and second blocks +func (bcR *BlockchainReactor) sendPeerError(err error, peerID p2p.ID) { + bcR.Logger.Info("sendPeerError:", "peer", peerID, "error", err) + msgData := bcFsmMessage{ + event: peerErrorEv, + data: bFsmEventData{ + peerID: peerID, + err: err, + }, + } + bcR.eventsFromFSMCh <- msgData +} + +// Implements bcRNotifier +func (bcR *BlockchainReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { + if timer == nil { + panic("nil timer pointer parameter") + } + if *timer == nil { + *timer = time.AfterFunc(timeout, func() { + msg := bcReactorMessage{ + event: stateTimeoutEv, + data: bReactorEventData{ + stateName: name, + }, + } + bcR.errorsForFSMCh <- msg + }) + } else { + (*timer).Reset(timeout) + } +} + +//----------------------------------------------------------------------------- +// Messages + +// 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 +} + +//------------------------------------- + +type bcBlockRequestMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcBlockRequestMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +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 nil +} + +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 { + 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) +} diff --git a/blockchain/v1/reactor_fsm.go b/blockchain/v1/reactor_fsm.go new file mode 100644 index 000000000..4bfef64ea --- /dev/null +++ b/blockchain/v1/reactor_fsm.go @@ -0,0 +1,450 @@ +package v1 + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +// Blockchain Reactor State +type bcReactorFSMState struct { + name string + + // called when transitioning out of current state + handle func(*BcReactorFSM, bReactorEvent, bReactorEventData) (next *bcReactorFSMState, err error) + // called when entering the state + enter func(fsm *BcReactorFSM) + + // timeout to ensure FSM is not stuck in a state forever + // the timer is owned and run by the fsm instance + timeout time.Duration +} + +func (s *bcReactorFSMState) String() string { + return s.name +} + +// BcReactorFSM is the datastructure for the Blockchain Reactor State Machine +type BcReactorFSM struct { + logger log.Logger + mtx sync.Mutex + + startTime time.Time + + state *bcReactorFSMState + stateTimer *time.Timer + pool *BlockPool + + // interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc. + toBcR bcReactor +} + +// NewFSM creates a new reactor FSM. +func NewFSM(height int64, toBcR bcReactor) *BcReactorFSM { + return &BcReactorFSM{ + state: unknown, + startTime: time.Now(), + pool: NewBlockPool(height, toBcR), + toBcR: toBcR, + } +} + +// bReactorEventData is part of the message sent by the reactor to the FSM and used by the state handlers. +type bReactorEventData struct { + peerID p2p.ID + err error // for peer error: timeout, slow; for processed block event if error occurred + height int64 // for status response; for processed block event + block *types.Block // for block response + stateName string // for state timeout events + length int // for block response event, length of received block, used to detect slow peers + maxNumRequests int // for request needed event, maximum number of pending requests +} + +// Blockchain Reactor Events (the input to the state machine) +type bReactorEvent uint + +const ( + // message type events + startFSMEv = iota + 1 + statusResponseEv + blockResponseEv + processedBlockEv + makeRequestsEv + stopFSMEv + + // other events + peerRemoveEv = iota + 256 + stateTimeoutEv +) + +func (msg *bcReactorMessage) String() string { + var dataStr string + + switch msg.event { + case startFSMEv: + dataStr = "" + case statusResponseEv: + dataStr = fmt.Sprintf("peer=%v height=%v", msg.data.peerID, msg.data.height) + case blockResponseEv: + dataStr = fmt.Sprintf("peer=%v block.height=%v length=%v", + msg.data.peerID, msg.data.block.Height, msg.data.length) + case processedBlockEv: + dataStr = fmt.Sprintf("error=%v", msg.data.err) + case makeRequestsEv: + dataStr = "" + case stopFSMEv: + dataStr = "" + case peerRemoveEv: + dataStr = fmt.Sprintf("peer: %v is being removed by the switch", msg.data.peerID) + case stateTimeoutEv: + dataStr = fmt.Sprintf("state=%v", msg.data.stateName) + default: + dataStr = fmt.Sprintf("cannot interpret message data") + } + + return fmt.Sprintf("%v: %v", msg.event, dataStr) +} + +func (ev bReactorEvent) String() string { + switch ev { + case startFSMEv: + return "startFSMEv" + case statusResponseEv: + return "statusResponseEv" + case blockResponseEv: + return "blockResponseEv" + case processedBlockEv: + return "processedBlockEv" + case makeRequestsEv: + return "makeRequestsEv" + case stopFSMEv: + return "stopFSMEv" + case peerRemoveEv: + return "peerRemoveEv" + case stateTimeoutEv: + return "stateTimeoutEv" + default: + return "event unknown" + } + +} + +// states +var ( + unknown *bcReactorFSMState + waitForPeer *bcReactorFSMState + waitForBlock *bcReactorFSMState + finished *bcReactorFSMState +) + +// timeouts for state timers +const ( + waitForPeerTimeout = 3 * time.Second + waitForBlockAtCurrentHeightTimeout = 10 * time.Second +) + +// errors +var ( + // internal to the package + errNoErrorFinished = errors.New("fast sync is finished") + errInvalidEvent = errors.New("invalid event in current state") + errMissingBlock = errors.New("missing blocks") + errNilPeerForBlockRequest = errors.New("peer for block request does not exist in the switch") + errSendQueueFull = errors.New("block request not made, send-queue is full") + errPeerTooShort = errors.New("peer height too low, old peer removed/ new peer not added") + errSwitchRemovesPeer = errors.New("switch is removing peer") + errTimeoutEventWrongState = errors.New("timeout event for a state different than the current one") + errNoTallerPeer = errors.New("fast sync timed out on waiting for a peer taller than this node") + + // reported eventually to the switch + errPeerLowersItsHeight = errors.New("fast sync peer reports a height lower than previous") // handle return + errNoPeerResponseForCurrentHeights = errors.New("fast sync timed out on peer block response for current heights") // handle return + errNoPeerResponse = errors.New("fast sync timed out on peer block response") // xx + errBadDataFromPeer = errors.New("fast sync received block from wrong peer or block is bad") // xx + errDuplicateBlock = errors.New("fast sync received duplicate block from peer") + errBlockVerificationFailure = errors.New("fast sync block verification failure") // xx + errSlowPeer = errors.New("fast sync peer is not sending us data fast enough") // xx + +) + +func init() { + unknown = &bcReactorFSMState{ + name: "unknown", + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + case startFSMEv: + // Broadcast Status message. Currently doesn't return non-nil error. + fsm.toBcR.sendStatusRequest() + return waitForPeer, nil + + case stopFSMEv: + return finished, errNoErrorFinished + + default: + return unknown, errInvalidEvent + } + }, + } + + waitForPeer = &bcReactorFSMState{ + name: "waitForPeer", + timeout: waitForPeerTimeout, + enter: func(fsm *BcReactorFSM) { + // Stop when leaving the state. + fsm.resetStateTimer() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + case stateTimeoutEv: + if data.stateName != "waitForPeer" { + fsm.logger.Error("received a state timeout event for different state", + "state", data.stateName) + return waitForPeer, errTimeoutEventWrongState + } + // There was no statusResponse received from any peer. + // Should we send status request again? + return finished, errNoTallerPeer + + case statusResponseEv: + if err := fsm.pool.UpdatePeer(data.peerID, data.height); err != nil { + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + } + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return waitForBlock, nil + + case stopFSMEv: + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return finished, errNoErrorFinished + + default: + return waitForPeer, errInvalidEvent + } + }, + } + + waitForBlock = &bcReactorFSMState{ + name: "waitForBlock", + timeout: waitForBlockAtCurrentHeightTimeout, + enter: func(fsm *BcReactorFSM) { + // Stop when leaving the state. + fsm.resetStateTimer() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + + case statusResponseEv: + err := fsm.pool.UpdatePeer(data.peerID, data.height) + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + if fsm.pool.ReachedMaxHeight() { + return finished, err + } + return waitForBlock, err + + case blockResponseEv: + fsm.logger.Debug("blockResponseEv", "H", data.block.Height) + err := fsm.pool.AddBlock(data.peerID, data.block, data.length) + if err != nil { + // A block was received that was unsolicited, from unexpected peer, or that we already have it. + // Ignore block, remove peer and send error to switch. + fsm.pool.RemovePeer(data.peerID, err) + fsm.toBcR.sendPeerError(err, data.peerID) + } + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + return waitForBlock, err + + case processedBlockEv: + if data.err != nil { + first, second, _ := fsm.pool.FirstTwoBlocksAndPeers() + fsm.logger.Error("error processing block", "err", data.err, + "first", first.block.Height, "second", second.block.Height) + fsm.logger.Error("send peer error for", "peer", first.peer.ID) + fsm.toBcR.sendPeerError(data.err, first.peer.ID) + fsm.logger.Error("send peer error for", "peer", second.peer.ID) + fsm.toBcR.sendPeerError(data.err, second.peer.ID) + // Remove the first two blocks. This will also remove the peers + fsm.pool.InvalidateFirstTwoBlocks(data.err) + } else { + fsm.pool.ProcessedCurrentHeightBlock() + // Since we advanced one block reset the state timer + fsm.resetStateTimer() + } + + // Both cases above may result in achieving maximum height. + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + + return waitForBlock, data.err + + case peerRemoveEv: + // This event is sent by the switch to remove disconnected and errored peers. + fsm.pool.RemovePeer(data.peerID, data.err) + if fsm.pool.NumPeers() == 0 { + return waitForPeer, nil + } + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + return waitForBlock, nil + + case makeRequestsEv: + fsm.makeNextRequests(data.maxNumRequests) + return waitForBlock, nil + + case stateTimeoutEv: + if data.stateName != "waitForBlock" { + fsm.logger.Error("received a state timeout event for different state", + "state", data.stateName) + return waitForBlock, errTimeoutEventWrongState + } + // We haven't received the block at current height or height+1. Remove peer. + fsm.pool.RemovePeerAtCurrentHeights(errNoPeerResponseForCurrentHeights) + fsm.resetStateTimer() + if fsm.pool.NumPeers() == 0 { + return waitForPeer, errNoPeerResponseForCurrentHeights + } + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + return waitForBlock, errNoPeerResponseForCurrentHeights + + case stopFSMEv: + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return finished, errNoErrorFinished + + default: + return waitForBlock, errInvalidEvent + } + }, + } + + finished = &bcReactorFSMState{ + name: "finished", + enter: func(fsm *BcReactorFSM) { + fsm.logger.Info("Time to switch to consensus reactor!", "height", fsm.pool.Height) + fsm.toBcR.switchToConsensus() + fsm.cleanup() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + return finished, nil + }, + } +} + +// Interface used by FSM for sending Block and Status requests, +// informing of peer errors and state timeouts +// Implemented by BlockchainReactor and tests +type bcReactor interface { + sendStatusRequest() + sendBlockRequest(peerID p2p.ID, height int64) error + sendPeerError(err error, peerID p2p.ID) + resetStateTimer(name string, timer **time.Timer, timeout time.Duration) + switchToConsensus() +} + +// SetLogger sets the FSM logger. +func (fsm *BcReactorFSM) SetLogger(l log.Logger) { + fsm.logger = l + fsm.pool.SetLogger(l) +} + +// Start starts the FSM. +func (fsm *BcReactorFSM) Start() { + _ = fsm.Handle(&bcReactorMessage{event: startFSMEv}) +} + +// Handle processes messages and events sent to the FSM. +func (fsm *BcReactorFSM) Handle(msg *bcReactorMessage) error { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + fsm.logger.Debug("FSM received", "event", msg, "state", fsm.state) + + if fsm.state == nil { + fsm.state = unknown + } + next, err := fsm.state.handle(fsm, msg.event, msg.data) + if err != nil { + fsm.logger.Error("FSM event handler returned", "err", err, + "state", fsm.state, "event", msg.event) + } + + oldState := fsm.state.name + fsm.transition(next) + if oldState != fsm.state.name { + fsm.logger.Info("FSM changed state", "new_state", fsm.state) + } + return err +} + +func (fsm *BcReactorFSM) transition(next *bcReactorFSMState) { + if next == nil { + return + } + if fsm.state != next { + fsm.state = next + if next.enter != nil { + next.enter(fsm) + } + } +} + +// Called when entering an FSM state in order to detect lack of progress in the state machine. +// Note the use of the 'bcr' interface to facilitate testing without timer expiring. +func (fsm *BcReactorFSM) resetStateTimer() { + fsm.toBcR.resetStateTimer(fsm.state.name, &fsm.stateTimer, fsm.state.timeout) +} + +func (fsm *BcReactorFSM) isCaughtUp() bool { + return fsm.state == finished +} + +func (fsm *BcReactorFSM) makeNextRequests(maxNumRequests int) { + fsm.pool.MakeNextRequests(maxNumRequests) +} + +func (fsm *BcReactorFSM) cleanup() { + fsm.pool.Cleanup() +} + +// NeedsBlocks checks if more block requests are required. +func (fsm *BcReactorFSM) NeedsBlocks() bool { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + return fsm.state.name == "waitForBlock" && fsm.pool.NeedsBlocks() +} + +// FirstTwoBlocks returns the two blocks at pool height and height+1 +func (fsm *BcReactorFSM) FirstTwoBlocks() (first, second *types.Block, err error) { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + firstBP, secondBP, err := fsm.pool.FirstTwoBlocksAndPeers() + if err == nil { + first = firstBP.block + second = secondBP.block + } + return +} + +// Status returns the pool's height and the maximum peer height. +func (fsm *BcReactorFSM) Status() (height, maxPeerHeight int64) { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + return fsm.pool.Height, fsm.pool.MaxPeerHeight +} diff --git a/blockchain/v1/reactor_fsm_test.go b/blockchain/v1/reactor_fsm_test.go new file mode 100644 index 000000000..54e177f25 --- /dev/null +++ b/blockchain/v1/reactor_fsm_test.go @@ -0,0 +1,938 @@ +package v1 + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +type lastBlockRequestT struct { + peerID p2p.ID + height int64 +} + +type lastPeerErrorT struct { + peerID p2p.ID + err error +} + +// reactor for FSM testing +type testReactor struct { + logger log.Logger + fsm *BcReactorFSM + numStatusRequests int + numBlockRequests int + lastBlockRequest lastBlockRequestT + lastPeerError lastPeerErrorT + stateTimerStarts map[string]int +} + +func sendEventToFSM(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) error { + return fsm.Handle(&bcReactorMessage{event: ev, data: data}) +} + +type fsmStepTestValues struct { + currentState string + event bReactorEvent + data bReactorEventData + + wantErr error + wantState string + wantStatusReqSent bool + wantReqIncreased bool + wantNewBlocks []int64 + wantRemovedPeers []p2p.ID +} + +// --------------------------------------------------------------------------- +// helper test function for different FSM events, state and expected behavior +func sStopFSMEv(current, expected string) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: stopFSMEv, + wantState: expected, + wantErr: errNoErrorFinished} +} + +func sUnknownFSMEv(current string) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: 1234, + wantState: current, + wantErr: errInvalidEvent} +} + +func sStartFSMEv() fsmStepTestValues { + return fsmStepTestValues{ + currentState: "unknown", + event: startFSMEv, + wantState: "waitForPeer", + wantStatusReqSent: true} +} + +func sStateTimeoutEv(current, expected string, timedoutState string, wantErr error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: stateTimeoutEv, + data: bReactorEventData{ + stateName: timedoutState, + }, + wantState: expected, + wantErr: wantErr, + } +} + +func sProcessedBlockEv(current, expected string, reactorError error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: processedBlockEv, + data: bReactorEventData{ + err: reactorError, + }, + wantState: expected, + wantErr: reactorError, + } +} + +func sStatusEv(current, expected string, peerID p2p.ID, height int64, err error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: statusResponseEv, + data: bReactorEventData{peerID: peerID, height: height}, + wantState: expected, + wantErr: err} +} + +func sMakeRequestsEv(current, expected string, maxPendingRequests int) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: makeRequestsEv, + data: bReactorEventData{maxNumRequests: maxPendingRequests}, + wantState: expected, + wantReqIncreased: true, + } +} + +func sMakeRequestsEvErrored(current, expected string, + maxPendingRequests int, err error, peersRemoved []p2p.ID) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: makeRequestsEv, + data: bReactorEventData{maxNumRequests: maxPendingRequests}, + wantState: expected, + wantErr: err, + wantRemovedPeers: peersRemoved, + wantReqIncreased: true, + } +} + +func sBlockRespEv(current, expected string, peerID p2p.ID, height int64, prevBlocks []int64) fsmStepTestValues { + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + return fsmStepTestValues{ + currentState: current, + event: blockResponseEv, + data: bReactorEventData{ + peerID: peerID, + height: height, + block: types.MakeBlock(int64(height), txs, nil, nil), + length: 100}, + wantState: expected, + wantNewBlocks: append(prevBlocks, height), + } +} + +func sBlockRespEvErrored(current, expected string, + peerID p2p.ID, height int64, prevBlocks []int64, wantErr error, peersRemoved []p2p.ID) fsmStepTestValues { + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + return fsmStepTestValues{ + currentState: current, + event: blockResponseEv, + data: bReactorEventData{ + peerID: peerID, + height: height, + block: types.MakeBlock(int64(height), txs, nil, nil), + length: 100}, + wantState: expected, + wantErr: wantErr, + wantRemovedPeers: peersRemoved, + wantNewBlocks: prevBlocks, + } +} + +func sPeerRemoveEv(current, expected string, peerID p2p.ID, err error, peersRemoved []p2p.ID) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: peerRemoveEv, + data: bReactorEventData{ + peerID: peerID, + err: err, + }, + wantState: expected, + wantRemovedPeers: peersRemoved, + } +} + +// -------------------------------------------- + +func newTestReactor(height int64) *testReactor { + testBcR := &testReactor{logger: log.TestingLogger(), stateTimerStarts: make(map[string]int)} + testBcR.fsm = NewFSM(height, testBcR) + testBcR.fsm.SetLogger(testBcR.logger) + return testBcR +} + +func fixBlockResponseEvStep(step *fsmStepTestValues, testBcR *testReactor) { + // There is currently no good way to know to which peer a block request was sent. + // So in some cases where it does not matter, before we simulate a block response + // we cheat and look where it is expected from. + if step.event == blockResponseEv { + height := step.data.height + peerID, ok := testBcR.fsm.pool.blocks[height] + if ok { + step.data.peerID = peerID + } + } +} + +type testFields struct { + name string + startingHeight int64 + maxRequestsPerPeer int + maxPendingRequests int + steps []fsmStepTestValues +} + +func executeFSMTests(t *testing.T, tests []testFields, matchRespToReq bool) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test reactor + testBcR := newTestReactor(tt.startingHeight) + + if tt.maxRequestsPerPeer != 0 { + maxRequestsPerPeer = tt.maxRequestsPerPeer + } + + for _, step := range tt.steps { + assert.Equal(t, step.currentState, testBcR.fsm.state.name) + + var heightBefore int64 + if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { + heightBefore = testBcR.fsm.pool.Height + } + oldNumStatusRequests := testBcR.numStatusRequests + oldNumBlockRequests := testBcR.numBlockRequests + if matchRespToReq { + fixBlockResponseEvStep(&step, testBcR) + } + + fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) + assert.Equal(t, step.wantErr, fsmErr) + + if step.wantStatusReqSent { + assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) + } else { + assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) + } + + if step.wantReqIncreased { + assert.True(t, oldNumBlockRequests < testBcR.numBlockRequests) + } else { + assert.Equal(t, oldNumBlockRequests, testBcR.numBlockRequests) + } + + for _, height := range step.wantNewBlocks { + _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(height) + assert.Nil(t, err) + } + if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { + heightAfter := testBcR.fsm.pool.Height + assert.Equal(t, heightBefore, heightAfter) + firstAfter, err1 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) + secondAfter, err2 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) + assert.NotNil(t, err1) + assert.NotNil(t, err2) + assert.Nil(t, firstAfter) + assert.Nil(t, secondAfter) + } + + assert.Equal(t, step.wantState, testBcR.fsm.state.name) + + if step.wantState == "finished" { + assert.True(t, testBcR.fsm.isCaughtUp()) + } + } + }) + } +} + +func TestFSMBasic(t *testing.T) { + tests := []testFields{ + { + name: "one block, one peer - TS2", + startingHeight: 1, + maxRequestsPerPeer: 2, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 2, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + { + name: "multi block, multi peer - TS2", + startingHeight: 1, + maxRequestsPerPeer: 2, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 4, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 4, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 4, []int64{1, 2, 3}), + + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMBlockVerificationFailure(t *testing.T) { + tests := []testFields{ + { + name: "block verification failure - TS2 variant", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + + // add P1 and get blocks 1-3 from it + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // process block failure, should remove P1 and all blocks + sProcessedBlockEv("waitForBlock", "waitForBlock", errBlockVerificationFailure), + + // get blocks 1-3 from P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), + + // finish after processing blocks 1 and 2 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMBadBlockFromPeer(t *testing.T) { + tests := []testFields{ + { + name: "block we haven't asked for", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and ask for blocks 1-3 + sStatusEv("waitForPeer", "waitForBlock", "P1", 300, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // blockResponseEv for height 100 should cause an error + sBlockRespEvErrored("waitForBlock", "waitForPeer", + "P1", 100, []int64{}, errMissingBlock, []p2p.ID{}), + }, + }, + { + name: "block we already have", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and get block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 100, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 1, []int64{}), + + // Get block 1 again. Since peer is removed together with block 1, + // the blocks present in the pool should be {} + sBlockRespEvErrored("waitForBlock", "waitForPeer", + "P1", 1, []int64{}, errDuplicateBlock, []p2p.ID{"P1"}), + }, + }, + { + name: "block from unknown peer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and get block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + + // get block 1 from unknown peer P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEvErrored("waitForBlock", "waitForBlock", + "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "block from wrong peer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, make requests for blocks 1-3 to P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // receive block 1 from P2 + sBlockRespEvErrored("waitForBlock", "waitForBlock", + "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMBlockAtCurrentHeightDoesNotArriveInTime(t *testing.T) { + tests := []testFields{ + { + name: "block at current height undelivered - TS5", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, get blocks 1 and 2, process block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 2, []int64{1}), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // timeout on block 3, P1 should be removed + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), + + // make requests and finish by receiving blocks 2 and 3 from P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{2}), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + { + name: "block at current height undelivered, at maxPeerHeight after peer removal - TS3", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, request blocks 1-3 from P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 (tallest) + sStatusEv("waitForBlock", "waitForBlock", "P2", 30, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // receive blocks 1-3 from P1 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), + + // process blocks at heights 1 and 2 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // timeout on block at height 4 + sStateTimeoutEv("waitForBlock", "finished", "waitForBlock", nil), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMPeerRelatedEvents(t *testing.T) { + tests := []testFields{ + { + name: "peer remove event with no blocks", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, P2, P3 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + sStatusEv("waitForBlock", "waitForBlock", "P3", 3, nil), + + // switch removes P2 + sPeerRemoveEv("waitForBlock", "waitForBlock", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "only peer removed while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + + // switch removes P1 + sPeerRemoveEv("waitForBlock", "waitForPeer", "P1", errSwitchRemovesPeer, []p2p.ID{"P1"}), + }, + }, + { + name: "highest peer removed while in waitForBlock state, node reaches maxPeerHeight - TS4 ", + startingHeight: 100, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and make requests + sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), + + // get blocks 100 and 101 from P1 and process block at height 100 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // switch removes peer P1, should be finished + sPeerRemoveEv("waitForBlock", "finished", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "highest peer lowers its height in waitForBlock state, node reaches maxPeerHeight - TS4", + startingHeight: 100, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and make requests + sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), + + // get blocks 100 and 101 from P1 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), + + // processed block at heights 100 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // P2 becomes short + sStatusEv("waitForBlock", "finished", "P2", 100, errPeerLowersItsHeight), + }, + }, + { + name: "new short peer while in waitForPeer state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForPeer", "P1", 3, errPeerTooShort), + }, + }, + { + name: "new short peer while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, errPeerTooShort), + }, + }, + { + name: "only peer updated with low height while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + sStatusEv("waitForBlock", "waitForPeer", "P1", 3, errPeerLowersItsHeight), + }, + }, + { + name: "peer does not exist in the switch", + startingHeight: 9999999, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 20000000, nil), + // send request for block 9999999 + // Note: For this block request the "switch missing the peer" error is simulated, + // see implementation of bcReactor interface, sendBlockRequest(), in this file. + sMakeRequestsEvErrored("waitForBlock", "waitForBlock", + maxNumRequests, nil, []p2p.ID{"P1"}), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMStopFSM(t *testing.T) { + tests := []testFields{ + { + name: "stopFSMEv in unknown", + steps: []fsmStepTestValues{ + sStopFSMEv("unknown", "finished"), + }, + }, + { + name: "stopFSMEv in waitForPeer", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStopFSMEv("waitForPeer", "finished"), + }, + }, + { + name: "stopFSMEv in waitForBlock", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sStopFSMEv("waitForBlock", "finished"), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMUnknownElements(t *testing.T) { + tests := []testFields{ + { + name: "unknown event for state unknown", + steps: []fsmStepTestValues{ + sUnknownFSMEv("unknown"), + }, + }, + { + name: "unknown event for state waitForPeer", + steps: []fsmStepTestValues{ + sStartFSMEv(), + sUnknownFSMEv("waitForPeer"), + }, + }, + { + name: "unknown event for state waitForBlock", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sUnknownFSMEv("waitForBlock"), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMPeerStateTimeoutEvent(t *testing.T) { + tests := []testFields{ + { + name: "timeout event for state waitForPeer while in state waitForPeer - TS1", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStateTimeoutEv("waitForPeer", "finished", "waitForPeer", errNoTallerPeer), + }, + }, + { + name: "timeout event for state waitForPeer while in a state != waitForPeer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStateTimeoutEv("waitForPeer", "waitForPeer", "waitForBlock", errTimeoutEventWrongState), + }, + }, + { + name: "timeout event for state waitForBlock while in state waitForBlock ", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStateTimeoutEv("waitForBlock", "waitForPeer", "waitForBlock", errNoPeerResponseForCurrentHeights), + }, + }, + { + name: "timeout event for state waitForBlock while in a state != waitForBlock", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForPeer", errTimeoutEventWrongState), + }, + }, + { + name: "timeout event for state waitForBlock with multiple peers", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func makeCorrectTransitionSequence(startingHeight int64, numBlocks int64, numPeers int, randomPeerHeights bool, + maxRequestsPerPeer int, maxPendingRequests int) testFields { + + // Generate numPeers peers with random or numBlocks heights according to the randomPeerHeights flag. + peerHeights := make([]int64, numPeers) + for i := 0; i < numPeers; i++ { + if i == 0 { + peerHeights[0] = numBlocks + continue + } + if randomPeerHeights { + peerHeights[i] = int64(cmn.MaxInt(cmn.RandIntn(int(numBlocks)), int(startingHeight)+1)) + } else { + peerHeights[i] = numBlocks + } + } + + // Approximate the slice capacity to save time for appends. + testSteps := make([]fsmStepTestValues, 0, 3*numBlocks+int64(numPeers)) + + testName := fmt.Sprintf("%v-blocks %v-startingHeight %v-peers %v-maxRequestsPerPeer %v-maxNumRequests", + numBlocks, startingHeight, numPeers, maxRequestsPerPeer, maxPendingRequests) + + // Add startFSMEv step. + testSteps = append(testSteps, sStartFSMEv()) + + // For each peer, add statusResponseEv step. + for i := 0; i < numPeers; i++ { + peerName := fmt.Sprintf("P%d", i) + if i == 0 { + testSteps = append( + testSteps, + sStatusEv("waitForPeer", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) + } else { + testSteps = append(testSteps, + sStatusEv("waitForBlock", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) + } + } + + height := startingHeight + numBlocksReceived := 0 + prevBlocks := make([]int64, 0, maxPendingRequests) + +forLoop: + for i := 0; i < int(numBlocks); i++ { + + // Add the makeRequestEv step periodically. + if i%int(maxRequestsPerPeer) == 0 { + testSteps = append( + testSteps, + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + ) + } + + // Add the blockRespEv step + testSteps = append( + testSteps, + sBlockRespEv("waitForBlock", "waitForBlock", + "P0", height, prevBlocks)) + prevBlocks = append(prevBlocks, height) + height++ + numBlocksReceived++ + + // Add the processedBlockEv step periodically. + if numBlocksReceived >= int(maxRequestsPerPeer) || height >= numBlocks { + for j := int(height) - numBlocksReceived; j < int(height); j++ { + if j >= int(numBlocks) { + // This is the last block that is processed, we should be in "finished" state. + testSteps = append( + testSteps, + sProcessedBlockEv("waitForBlock", "finished", nil)) + break forLoop + } + testSteps = append( + testSteps, + sProcessedBlockEv("waitForBlock", "waitForBlock", nil)) + } + numBlocksReceived = 0 + prevBlocks = make([]int64, 0, maxPendingRequests) + } + } + + return testFields{ + name: testName, + startingHeight: startingHeight, + maxRequestsPerPeer: maxRequestsPerPeer, + maxPendingRequests: maxPendingRequests, + steps: testSteps, + } +} + +const ( + maxStartingHeightTest = 100 + maxRequestsPerPeerTest = 20 + maxTotalPendingRequestsTest = 600 + maxNumPeersTest = 1000 + maxNumBlocksInChainTest = 10000 //should be smaller than 9999999 +) + +func makeCorrectTransitionSequenceWithRandomParameters() testFields { + // Generate a starting height for fast sync. + startingHeight := int64(cmn.RandIntn(maxStartingHeightTest) + 1) + + // Generate the number of requests per peer. + maxRequestsPerPeer := cmn.RandIntn(maxRequestsPerPeerTest) + 1 + + // Generate the maximum number of total pending requests, >= maxRequestsPerPeer. + maxPendingRequests := cmn.RandIntn(maxTotalPendingRequestsTest-int(maxRequestsPerPeer)) + maxRequestsPerPeer + + // Generate the number of blocks to be synced. + numBlocks := int64(cmn.RandIntn(maxNumBlocksInChainTest)) + startingHeight + + // Generate a number of peers. + numPeers := cmn.RandIntn(maxNumPeersTest) + 1 + + return makeCorrectTransitionSequence(startingHeight, numBlocks, numPeers, true, maxRequestsPerPeer, maxPendingRequests) +} + +func shouldApplyProcessedBlockEvStep(step *fsmStepTestValues, testBcR *testReactor) bool { + if step.event == processedBlockEv { + _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) + if err == errMissingBlock { + return false + } + _, err = testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) + if err == errMissingBlock { + return false + } + } + return true +} + +func TestFSMCorrectTransitionSequences(t *testing.T) { + + tests := []testFields{ + makeCorrectTransitionSequence(1, 100, 10, true, 10, 40), + makeCorrectTransitionSequenceWithRandomParameters(), + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test reactor + testBcR := newTestReactor(tt.startingHeight) + + if tt.maxRequestsPerPeer != 0 { + maxRequestsPerPeer = tt.maxRequestsPerPeer + } + + for _, step := range tt.steps { + assert.Equal(t, step.currentState, testBcR.fsm.state.name) + + oldNumStatusRequests := testBcR.numStatusRequests + fixBlockResponseEvStep(&step, testBcR) + if !shouldApplyProcessedBlockEvStep(&step, testBcR) { + continue + } + + fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) + assert.Equal(t, step.wantErr, fsmErr) + + if step.wantStatusReqSent { + assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) + } else { + assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) + } + + assert.Equal(t, step.wantState, testBcR.fsm.state.name) + if step.wantState == "finished" { + assert.True(t, testBcR.fsm.isCaughtUp()) + } + } + + }) + } +} + +// ---------------------------------------- +// implements the bcRNotifier +func (testR *testReactor) sendPeerError(err error, peerID p2p.ID) { + testR.logger.Info("Reactor received sendPeerError call from FSM", "peer", peerID, "err", err) + testR.lastPeerError.peerID = peerID + testR.lastPeerError.err = err +} + +func (testR *testReactor) sendStatusRequest() { + testR.logger.Info("Reactor received sendStatusRequest call from FSM") + testR.numStatusRequests++ +} + +func (testR *testReactor) sendBlockRequest(peerID p2p.ID, height int64) error { + testR.logger.Info("Reactor received sendBlockRequest call from FSM", "peer", peerID, "height", height) + testR.numBlockRequests++ + testR.lastBlockRequest.peerID = peerID + testR.lastBlockRequest.height = height + if height == 9999999 { + // simulate switch does not have peer + return errNilPeerForBlockRequest + } + return nil +} + +func (testR *testReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { + testR.logger.Info("Reactor received resetStateTimer call from FSM", "state", name, "timeout", timeout) + if _, ok := testR.stateTimerStarts[name]; !ok { + testR.stateTimerStarts[name] = 1 + } else { + testR.stateTimerStarts[name]++ + } +} + +func (testR *testReactor) switchToConsensus() { +} + +// ---------------------------------------- diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go new file mode 100644 index 000000000..9599b7a8a --- /dev/null +++ b/blockchain/v1/reactor_test.go @@ -0,0 +1,413 @@ +package v1 + +import ( + "fmt" + "os" + "sort" + "sync" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" + "github.com/tendermint/tendermint/p2p" + "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" +) + +var config *cfg.Config + +func randGenesisDoc(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: config.ChainID(), + Validators: validators, + }, privValidators +} + +func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { + addr := privVal.GetPubKey().Address() + idx, _ := valset.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: header.Height, + Round: 1, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + + _ = privVal.SignVote(header.ChainID, vote) + + return vote +} + +type BlockchainReactorPair struct { + bcR *BlockchainReactor + conR *consensusReactorTest +} + +func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) *BlockchainReactor { + 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")) + } + + blockDB := dbm.NewMemDB() + stateDB := dbm.NewMemDB() + blockStore := store.NewBlockStore(blockDB) + + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + panic(errors.Wrap(err, "error constructing state from genesis file")) + } + + // Make the BlockchainReactor itself. + // NOTE we have to create and commit the blocks first because + // pool.height is determined from the store. + fastSync := true + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}) + sm.SaveState(db, state) + + // let's add some blocks in + for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { + lastCommit := types.NewCommit(types.BlockID{}, nil) + if blockHeight > 1 { + lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) + lastBlock := blockStore.LoadBlock(blockHeight - 1) + + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + } + + 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) + } + + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor.SetLogger(logger.With("module", "blockchain")) + + return bcReactor +} + +func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockchainReactorPair { + + consensusReactor := &consensusReactorTest{} + consensusReactor.BaseReactor = *p2p.NewBaseReactor("Consensus reactor", consensusReactor) + + return BlockchainReactorPair{ + newBlockchainReactor(logger, genDoc, privVals, maxBlockHeight), + consensusReactor} +} + +type consensusReactorTest struct { + p2p.BaseReactor // BaseService + p2p.Switch + switchedToConsensus bool + mtx sync.Mutex +} + +func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced int) { + conR.mtx.Lock() + defer conR.mtx.Unlock() + conR.switchedToConsensus = true +} + +func TestFastSyncNoBlockResponse(t *testing.T) { + + config = cfg.ResetTestRoot("blockchain_new_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + maxBlockHeight := int64(65) + + reactorPairs := make([]BlockchainReactorPair, 2) + + logger := log.TestingLogger() + reactorPairs[0] = newBlockchainReactorPair(logger, genDoc, privVals, maxBlockHeight) + reactorPairs[1] = newBlockchainReactorPair(logger, genDoc, privVals, 0) + + p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) + s.AddReactor("CONSENSUS", reactorPairs[i].conR) + moduleName := fmt.Sprintf("blockchain-%v", i) + reactorPairs[i].bcR.SetLogger(logger.With("module", moduleName)) + + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + _ = r.bcR.Stop() + _ = r.conR.Stop() + } + }() + + tests := []struct { + height int64 + existent bool + }{ + {maxBlockHeight + 2, false}, + {10, true}, + {1, true}, + {maxBlockHeight + 100, false}, + } + + for { + time.Sleep(10 * time.Millisecond) + reactorPairs[1].conR.mtx.Lock() + if reactorPairs[1].conR.switchedToConsensus { + reactorPairs[1].conR.mtx.Unlock() + break + } + reactorPairs[1].conR.mtx.Unlock() + } + + assert.Equal(t, maxBlockHeight, reactorPairs[0].bcR.store.Height()) + + for _, tt := range tests { + block := reactorPairs[1].bcR.store.LoadBlock(tt.height) + if tt.existent { + assert.True(t, block != nil) + } else { + assert.True(t, block == nil) + } + } +} + +// NOTE: This is too hard to test without +// an easy way to add test peer to switch +// or without significant refactoring of the module. +// Alternatively we could actually dial a TCP conn but +// that seems extreme. +func TestFastSyncBadBlockStopsPeer(t *testing.T) { + numNodes := 4 + maxBlockHeight := int64(148) + + config = cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + otherChain := newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight) + defer func() { + _ = otherChain.bcR.Stop() + _ = otherChain.conR.Stop() + }() + + reactorPairs := make([]BlockchainReactorPair, numNodes) + logger := make([]log.Logger, numNodes) + + for i := 0; i < numNodes; i++ { + logger[i] = log.TestingLogger() + height := int64(0) + if i == 0 { + height = maxBlockHeight + } + reactorPairs[i] = newBlockchainReactorPair(logger[i], genDoc, privVals, height) + } + + switches := p2p.MakeConnectedSwitches(config.P2P, numNodes, func(i int, s *p2p.Switch) *p2p.Switch { + reactorPairs[i].conR.mtx.Lock() + s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) + s.AddReactor("CONSENSUS", reactorPairs[i].conR) + moduleName := fmt.Sprintf("blockchain-%v", i) + reactorPairs[i].bcR.SetLogger(logger[i].With("module", moduleName)) + reactorPairs[i].conR.mtx.Unlock() + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + _ = r.bcR.Stop() + _ = r.conR.Stop() + } + }() + +outerFor: + for { + time.Sleep(10 * time.Millisecond) + for i := 0; i < numNodes; i++ { + reactorPairs[i].conR.mtx.Lock() + if !reactorPairs[i].conR.switchedToConsensus { + reactorPairs[i].conR.mtx.Unlock() + continue outerFor + } + reactorPairs[i].conR.mtx.Unlock() + } + break + } + + //at this time, reactors[0-3] is the newest + assert.Equal(t, numNodes-1, reactorPairs[1].bcR.Switch.Peers().Size()) + + //mark last reactorPair as an invalid peer + reactorPairs[numNodes-1].bcR.store = otherChain.bcR.store + + lastLogger := log.TestingLogger() + lastReactorPair := newBlockchainReactorPair(lastLogger, genDoc, privVals, 0) + reactorPairs = append(reactorPairs, lastReactorPair) + + switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("BLOCKCHAIN", reactorPairs[len(reactorPairs)-1].bcR) + s.AddReactor("CONSENSUS", reactorPairs[len(reactorPairs)-1].conR) + moduleName := fmt.Sprintf("blockchain-%v", len(reactorPairs)-1) + reactorPairs[len(reactorPairs)-1].bcR.SetLogger(lastLogger.With("module", moduleName)) + return s + + }, p2p.Connect2Switches)...) + + for i := 0; i < len(reactorPairs)-1; i++ { + p2p.Connect2Switches(switches, i, len(reactorPairs)-1) + } + + for { + time.Sleep(1 * time.Second) + lastReactorPair.conR.mtx.Lock() + if lastReactorPair.conR.switchedToConsensus { + lastReactorPair.conR.mtx.Unlock() + break + } + lastReactorPair.conR.mtx.Unlock() + + if lastReactorPair.bcR.Switch.Peers().Size() == 0 { + break + } + } + + assert.True(t, lastReactorPair.bcR.Switch.Peers().Size() < len(reactorPairs)-1) +} + +func TestBcBlockRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcBlockRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + nonResponseHeight int64 + expectErr bool + }{ + {"Valid Non-Response Message", 0, false}, + {"Valid Non-Response Message", 1, false}, + {"Invalid Non-Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} + assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcStatusRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + responseHeight int64 + expectErr bool + }{ + {"Valid Response Message", 0, false}, + {"Valid Response Message", 1, false}, + {"Invalid Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + response := bcStatusResponseMessage{Height: tc.responseHeight} + assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +//---------------------------------------------- +// 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 +} diff --git a/blockchain/v2/schedule.go b/blockchain/v2/schedule.go new file mode 100644 index 000000000..329557492 --- /dev/null +++ b/blockchain/v2/schedule.go @@ -0,0 +1,387 @@ +// nolint:unused +package v2 + +import ( + "fmt" + "math" + "math/rand" + "time" + + "github.com/tendermint/tendermint/p2p" +) + +type Event interface{} + +type blockState int + +const ( + blockStateUnknown blockState = iota + blockStateNew + blockStatePending + blockStateReceived + blockStateProcessed +) + +func (e blockState) String() string { + switch e { + case blockStateUnknown: + return "Unknown" + case blockStateNew: + return "New" + case blockStatePending: + return "Pending" + case blockStateReceived: + return "Received" + case blockStateProcessed: + return "Processed" + default: + return fmt.Sprintf("unknown blockState: %d", e) + } +} + +type peerState int + +const ( + peerStateNew = iota + peerStateReady + peerStateRemoved +) + +func (e peerState) String() string { + switch e { + case peerStateNew: + return "New" + case peerStateReady: + return "Ready" + case peerStateRemoved: + return "Removed" + default: + return fmt.Sprintf("unknown peerState: %d", e) + } +} + +type scPeer struct { + peerID p2p.ID + state peerState + height int64 + lastTouched time.Time + lastRate int64 +} + +func newScPeer(peerID p2p.ID) *scPeer { + return &scPeer{ + peerID: peerID, + state: peerStateNew, + height: -1, + lastTouched: time.Time{}, + } +} + +// The schedule is a composite data structure which allows a scheduler to keep +// track of which blocks have been scheduled into which state. +type schedule struct { + initHeight int64 + // a list of blocks in which blockState + blockStates map[int64]blockState + + // a map of peerID to schedule specific peer struct `scPeer` used to keep + // track of peer specific state + peers map[p2p.ID]*scPeer + + // a map of heights to the peer we are waiting for a response from + pendingBlocks map[int64]p2p.ID + + // the time at which a block was put in blockStatePending + pendingTime map[int64]time.Time + + // the peerID of the peer which put the block in blockStateReceived + receivedBlocks map[int64]p2p.ID +} + +func newSchedule(initHeight int64) *schedule { + sc := schedule{ + initHeight: initHeight, + 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), + } + + sc.setStateAtHeight(initHeight, blockStateNew) + + return &sc +} + +func (sc *schedule) addPeer(peerID p2p.ID) error { + if _, ok := sc.peers[peerID]; ok { + return fmt.Errorf("Cannot add duplicate peer %s", peerID) + } + sc.peers[peerID] = newScPeer(peerID) + return nil +} + +func (sc *schedule) touchPeer(peerID p2p.ID, time time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Couldn't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Tried to touch peer in peerStateRemoved") + } + + peer.lastTouched = time + + return nil +} + +func (sc *schedule) removePeer(peerID p2p.ID) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Couldn't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Tried to remove peer %s in peerStateRemoved", peerID) + } + + for height, pendingPeerID := range sc.pendingBlocks { + if pendingPeerID == peerID { + sc.setStateAtHeight(height, blockStateNew) + delete(sc.pendingTime, height) + delete(sc.pendingBlocks, height) + } + } + + for height, rcvPeerID := range sc.receivedBlocks { + if rcvPeerID == peerID { + sc.setStateAtHeight(height, blockStateNew) + delete(sc.receivedBlocks, height) + } + } + + peer.state = peerStateRemoved + + return nil +} + +func (sc *schedule) setPeerHeight(peerID p2p.ID, height int64) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Cannot set peer height for a peer in peerStateRemoved") + } + + if height < peer.height { + return fmt.Errorf("Cannot move peer height lower. from %d to %d", peer.height, height) + } + + peer.height = height + peer.state = peerStateReady + for i := sc.minHeight(); i <= height; i++ { + if sc.getStateAtHeight(i) == blockStateUnknown { + sc.setStateAtHeight(i, blockStateNew) + } + } + + return nil +} + +func (sc *schedule) getStateAtHeight(height int64) blockState { + if height < sc.initHeight { + return blockStateProcessed + } else if state, ok := sc.blockStates[height]; ok { + return state + } else { + return blockStateUnknown + } +} + +func (sc *schedule) getPeersAtHeight(height int64) []*scPeer { + peers := []*scPeer{} + for _, peer := range sc.peers { + if peer.height >= height { + peers = append(peers, peer) + } + } + + return peers +} + +func (sc *schedule) peersInactiveSince(duration time.Duration, now time.Time) []p2p.ID { + peers := []p2p.ID{} + for _, peer := range sc.peers { + if now.Sub(peer.lastTouched) > duration { + peers = append(peers, peer.peerID) + } + } + + return peers +} + +func (sc *schedule) peersSlowerThan(minSpeed int64) []p2p.ID { + peers := []p2p.ID{} + for _, peer := range sc.peers { + if peer.lastRate < minSpeed { + peers = append(peers, peer.peerID) + } + } + + return peers +} + +func (sc *schedule) setStateAtHeight(height int64, state blockState) { + sc.blockStates[height] = state +} + +func (sc *schedule) markReceived(peerID p2p.ID, height int64, size int64, now time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Cannot receive blocks from removed peer %s", peerID) + } + + if state := sc.getStateAtHeight(height); state != blockStatePending || sc.pendingBlocks[height] != peerID { + return fmt.Errorf("Received block %d from peer %s without being requested", height, peerID) + } + + pendingTime, ok := sc.pendingTime[height] + if !ok || now.Sub(pendingTime) <= 0 { + return fmt.Errorf("Clock error. Block %d received at %s but requested at %s", + height, pendingTime, now) + } + + peer.lastRate = size / int64(now.Sub(pendingTime).Seconds()) + + sc.setStateAtHeight(height, blockStateReceived) + delete(sc.pendingBlocks, height) + delete(sc.pendingTime, height) + + sc.receivedBlocks[height] = peerID + + return nil +} + +func (sc *schedule) markPending(peerID p2p.ID, height int64, time time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + state := sc.getStateAtHeight(height) + if state != blockStateNew { + return fmt.Errorf("Block %d should be in blockStateNew but was %s", height, state) + } + + if peer.state != peerStateReady { + return fmt.Errorf("Cannot schedule %d from %s in %s", height, peerID, peer.state) + } + + if height > peer.height { + return fmt.Errorf("Cannot request height %d from peer %s who is at height %d", + height, peerID, peer.height) + } + + 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 *schedule) markProcessed(height int64) error { + state := sc.getStateAtHeight(height) + if state != blockStateReceived { + return fmt.Errorf("Can't mark height %d received from block state %s", height, state) + } + + delete(sc.receivedBlocks, height) + + sc.setStateAtHeight(height, blockStateProcessed) + + return nil +} + +// allBlockProcessed returns true if all blocks are in blockStateProcessed and +// determines if the schedule has been completed +func (sc *schedule) allBlocksProcessed() bool { + for _, state := range sc.blockStates { + if state != blockStateProcessed { + return false + } + } + return true +} + +// highest block | state == blockStateNew +func (sc *schedule) maxHeight() int64 { + var max int64 = 0 + for height, state := range sc.blockStates { + if state == blockStateNew && height > max { + max = height + } + } + + return max +} + +// lowest block | state == blockStateNew +func (sc *schedule) minHeight() int64 { + var min int64 = math.MaxInt64 + for height, state := range sc.blockStates { + if state == blockStateNew && height < min { + min = height + } + } + + return min +} + +func (sc *schedule) pendingFrom(peerID p2p.ID) []int64 { + heights := []int64{} + for height, pendingPeerID := range sc.pendingBlocks { + if pendingPeerID == peerID { + heights = append(heights, height) + } + } + return heights +} + +func (sc *schedule) selectPeer(peers []*scPeer) *scPeer { + // FIXME: properPeerSelector + s := rand.NewSource(time.Now().Unix()) + r := rand.New(s) + + return peers[r.Intn(len(peers))] +} + +// XXX: this duplicates the logic of peersInactiveSince and peersSlowerThan +func (sc *schedule) prunablePeers(peerTimout time.Duration, minRecvRate int64, now time.Time) []p2p.ID { + prunable := []p2p.ID{} + for peerID, peer := range sc.peers { + if now.Sub(peer.lastTouched) > peerTimout || peer.lastRate < minRecvRate { + prunable = append(prunable, peerID) + } + } + + return prunable +} + +func (sc *schedule) numBlockInState(targetState blockState) uint32 { + var num uint32 = 0 + for _, state := range sc.blockStates { + if state == targetState { + num++ + } + } + return num +} diff --git a/blockchain/v2/schedule_test.go b/blockchain/v2/schedule_test.go new file mode 100644 index 000000000..a1448c528 --- /dev/null +++ b/blockchain/v2/schedule_test.go @@ -0,0 +1,272 @@ +package v2 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/p2p" +) + +func TestScheduleInit(t *testing.T) { + var ( + initHeight int64 = 5 + sc = newSchedule(initHeight) + ) + + assert.Equal(t, blockStateNew, sc.getStateAtHeight(initHeight)) + assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight-1)) + assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1)) +} + +func TestAddPeer(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + sc = newSchedule(initHeight) + ) + + assert.Nil(t, sc.addPeer(peerID)) + assert.Nil(t, sc.addPeer(peerIDTwo)) + assert.Error(t, sc.addPeer(peerID)) +} + +func TestTouchPeer(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.Error(t, sc.touchPeer(peerID, now), + "Touching an unknown peer should return errPeerNotFound") + + assert.Nil(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.Nil(t, sc.touchPeer(peerID, now), + "Touching a peer should return no error") + + threshold := 10 * time.Second + assert.Empty(t, sc.peersInactiveSince(threshold, now.Add(9*time.Second)), + "Expected no peers to have been touched over 9 seconds") + assert.Containsf(t, sc.peersInactiveSince(threshold, now.Add(11*time.Second)), peerID, + "Expected one %s to have been touched over 10 seconds ago", peerID) +} + +func TestPeerHeight(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight)) + for i := initHeight; i <= peerHeight; i++ { + assert.Equal(t, sc.getStateAtHeight(i), blockStateNew, + "Expected all blocks to be in blockStateNew") + peerIDs := []p2p.ID{} + for _, peer := range sc.getPeersAtHeight(i) { + peerIDs = append(peerIDs, peer.peerID) + } + + assert.Containsf(t, peerIDs, peerID, + "Expected %s to have block %d", peerID, i) + } +} + +func TestTransitionPending(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.Nil(t, sc.addPeer(peerIDTwo), + "Adding a peer should return no error") + + assert.Error(t, sc.markPending(peerID, peerHeight, now), + "Expected scheduling a block from a peer in peerStateNew to fail") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + assert.NoError(t, sc.setPeerHeight(peerIDTwo, peerHeight), + "Expected setPeerHeight to return no error") + + assert.NoError(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending new block to succeed") + assert.Error(t, sc.markPending(peerIDTwo, peerHeight, now), + "Expected markingPending by a second peer to fail") + + assert.Equal(t, blockStatePending, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStatePending") + + assert.NoError(t, sc.removePeer(peerID), + "Expected removePeer to return no error") + + assert.Equal(t, blockStateNew, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStateNew") + + assert.Error(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending removed peer to fail") + + assert.NoError(t, sc.markPending(peerIDTwo, peerHeight, now), + "Expected markingPending on a ready peer to succeed") + + assert.Equal(t, blockStatePending, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStatePending") +} + +func TestTransitionReceived(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Expected adding peer %s to succeed", peerID) + assert.NoError(t, sc.addPeer(peerIDTwo), + "Expected adding peer %s to succeed", peerIDTwo) + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + assert.NoErrorf(t, sc.setPeerHeight(peerIDTwo, peerHeight), + "Expected setPeerHeight on %s to %d to succeed", peerIDTwo, peerHeight) + assert.NoError(t, sc.markPending(peerID, initHeight, now), + "Expected markingPending new block to succeed") + + assert.Error(t, sc.markReceived(peerIDTwo, initHeight, blockSize, receivedAt), + "Expected marking markReceived from a non requesting peer to fail") + + assert.NoError(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on a pending block to succeed") + + assert.Error(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on received block to fail") + + assert.Equalf(t, blockStateReceived, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockHeightReceived", initHeight) + + assert.NoErrorf(t, sc.removePeer(peerID), + "Expected removePeer removing %s to succeed", peerID) + + assert.Equalf(t, blockStateNew, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateNew", initHeight) + + assert.NoErrorf(t, sc.markPending(peerIDTwo, initHeight, now), + "Expected markingPending %d from %s to succeed", initHeight, peerIDTwo) + assert.NoErrorf(t, sc.markReceived(peerIDTwo, initHeight, blockSize, receivedAt), + "Expected marking markReceived %d from %s to succeed", initHeight, peerIDTwo) + assert.Equalf(t, blockStateReceived, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateReceived", initHeight) +} + +func TestTransitionProcessed(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Expected adding peer %s to succeed", peerID) + assert.NoErrorf(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight on %s to %d to succeed", peerID, peerHeight) + assert.NoError(t, sc.markPending(peerID, initHeight, now), + "Expected markingPending new block to succeed") + assert.NoError(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on a pending block to succeed") + + assert.Error(t, sc.markProcessed(initHeight+1), + "Expected marking %d as processed to fail", initHeight+1) + assert.NoError(t, sc.markProcessed(initHeight), + "Expected marking %d as processed to succeed", initHeight) + + assert.Equalf(t, blockStateProcessed, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateProcessed", initHeight) + + assert.NoError(t, sc.removePeer(peerID), + "Expected removing peer %s to succeed", peerID) + + assert.Equalf(t, blockStateProcessed, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateProcessed", initHeight) +} + +func TestMinMaxHeight(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.Equal(t, initHeight, sc.minHeight(), + "Expected min height to be the initialized height") + + assert.Equal(t, initHeight, sc.maxHeight(), + "Expected max height to be the initialized height") + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + + assert.Equal(t, peerHeight, sc.maxHeight(), + "Expected max height to increase to peerHeight") + + assert.Nil(t, sc.markPending(peerID, initHeight, now.Add(1*time.Second)), + "Expected marking initHeight as pending to return no error") + + assert.Equal(t, initHeight+1, sc.minHeight(), + "Expected marking initHeight as pending to move minHeight forward") +} + +func TestPeersSlowerThan(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + + assert.NoError(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending on to return no error") + + assert.NoError(t, sc.markReceived(peerID, peerHeight, blockSize, receivedAt), + "Expected markingPending on to return no error") + + assert.Empty(t, sc.peersSlowerThan(blockSize-1), + "expected no peers to be slower than blockSize-1 bytes/sec") + + assert.Containsf(t, sc.peersSlowerThan(blockSize+1), peerID, + "expected %s to be slower than blockSize+1 bytes/sec", peerID) +} diff --git a/cmd/priv_val_server/main.go b/cmd/priv_val_server/main.go index c86bced81..22af6418f 100644 --- a/cmd/priv_val_server/main.go +++ b/cmd/priv_val_server/main.go @@ -48,15 +48,17 @@ func main() { os.Exit(1) } - rs := privval.NewSignerServiceEndpoint(logger, *chainID, pv, dialer) - err := rs.Start() + sd := privval.NewSignerDialerEndpoint(logger, dialer) + ss := privval.NewSignerServer(sd, *chainID, pv) + + err := ss.Start() if err != nil { panic(err) } // Stop upon receiving SIGTERM or CTRL-C. cmn.TrapSignal(logger, func() { - err := rs.Stop() + err := ss.Stop() if err != nil { panic(err) } diff --git a/cmd/tendermint/commands/wire.go b/cmd/tendermint/commands/codec.go similarity index 100% rename from cmd/tendermint/commands/wire.go rename to cmd/tendermint/commands/codec.go diff --git a/cmd/tendermint/commands/lite.go b/cmd/tendermint/commands/lite.go index 3e1a43d30..6c7f6f395 100644 --- a/cmd/tendermint/commands/lite.go +++ b/cmd/tendermint/commands/lite.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" + "github.com/pkg/errors" "github.com/spf13/cobra" cmn "github.com/tendermint/tendermint/libs/common" @@ -81,7 +82,7 @@ func runProxy(cmd *cobra.Command, args []string) error { logger.Info("Constructing verifying provider...") cert, err := verifying.NewProvider(chainID, home, node, logger, cacheSize, verifying.TrustOptions{}) if err != nil { - return cmn.ErrorWrap(err, "constructing verifying provider") + return errors.Wrap(err, "constructing verifying provider") } cert.SetLogger(logger) sc := proxy.SecureClient(node, cert) @@ -89,7 +90,7 @@ func runProxy(cmd *cobra.Command, args []string) error { logger.Info("Starting proxy...") err = proxy.StartProxy(sc, listenAddr, logger, maxOpenConnections) if err != nil { - return cmn.ErrorWrap(err, "starting proxy") + return errors.Wrap(err, "starting proxy") } // Run forever diff --git a/cmd/tendermint/commands/root_test.go b/cmd/tendermint/commands/root_test.go index 892a49b74..229385af9 100644 --- a/cmd/tendermint/commands/root_test.go +++ b/cmd/tendermint/commands/root_test.go @@ -165,7 +165,7 @@ func TestRootConfig(t *testing.T) { func WriteConfigVals(dir string, vals map[string]string) error { data := "" for k, v := range vals { - data = data + fmt.Sprintf("%s = \"%s\"\n", k, v) + data += fmt.Sprintf("%s = \"%s\"\n", k, v) } cfile := filepath.Join(dir, "config.toml") return ioutil.WriteFile(cfile, []byte(data), 0666) diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index fa63b4944..70de9aba7 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -19,7 +19,7 @@ func AddNodeFlags(cmd *cobra.Command) { cmd.Flags().String("priv_validator_laddr", config.PrivValidatorListenAddr, "Socket address to listen on for connections from external priv_validator process") // node flags - cmd.Flags().Bool("fast_sync", config.FastSync, "Fast blockchain syncing") + cmd.Flags().Bool("fast_sync", config.FastSyncMode, "Fast blockchain syncing") // abci flags cmd.Flags().String("proxy_app", config.ProxyApp, "Proxy app address, or one of: 'kvstore', 'persistent_kvstore', 'counter', 'counter_serial' or 'noop' for local testing.") diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index f1dd6f16e..5e2dc1a3a 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -161,9 +161,10 @@ func testnetFiles(cmd *cobra.Command, args []string) error { // Generate genesis doc from generated validators genDoc := &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: "chain-" + cmn.RandStr(6), - Validators: genVals, + ChainID: "chain-" + cmn.RandStr(6), + ConsensusParams: types.DefaultConsensusParams(), + GenesisTime: tmtime.Now(), + Validators: genVals, } // Write genesis file. diff --git a/config/config.go b/config/config.go index 32e37f3e1..d414d0560 100644 --- a/config/config.go +++ b/config/config.go @@ -64,6 +64,7 @@ type Config struct { RPC *RPCConfig `mapstructure:"rpc"` P2P *P2PConfig `mapstructure:"p2p"` Mempool *MempoolConfig `mapstructure:"mempool"` + FastSync *FastSyncConfig `mapstructure:"fastsync"` Consensus *ConsensusConfig `mapstructure:"consensus"` TxIndex *TxIndexConfig `mapstructure:"tx_index"` Instrumentation *InstrumentationConfig `mapstructure:"instrumentation"` @@ -76,6 +77,7 @@ func DefaultConfig() *Config { RPC: DefaultRPCConfig(), P2P: DefaultP2PConfig(), Mempool: DefaultMempoolConfig(), + FastSync: DefaultFastSyncConfig(), Consensus: DefaultConsensusConfig(), TxIndex: DefaultTxIndexConfig(), Instrumentation: DefaultInstrumentationConfig(), @@ -89,6 +91,7 @@ func TestConfig() *Config { RPC: TestRPCConfig(), P2P: TestP2PConfig(), Mempool: TestMempoolConfig(), + FastSync: TestFastSyncConfig(), Consensus: TestConsensusConfig(), TxIndex: TestTxIndexConfig(), Instrumentation: TestInstrumentationConfig(), @@ -120,6 +123,9 @@ func (cfg *Config) ValidateBasic() error { if err := cfg.Mempool.ValidateBasic(); err != nil { return errors.Wrap(err, "Error in [mempool] section") } + if err := cfg.FastSync.ValidateBasic(); err != nil { + return errors.Wrap(err, "Error in [fastsync] section") + } if err := cfg.Consensus.ValidateBasic(); err != nil { return errors.Wrap(err, "Error in [consensus] section") } @@ -151,7 +157,7 @@ type BaseConfig struct { // If this node is many blocks behind the tip of the chain, FastSync // allows them to catchup quickly by downloading blocks in parallel // and verifying their commits - FastSync bool `mapstructure:"fast_sync"` + FastSyncMode bool `mapstructure:"fast_sync"` // Database backend: goleveldb | cleveldb | boltdb // * goleveldb (github.com/syndtr/goleveldb - most popular implementation) @@ -216,7 +222,7 @@ func DefaultBaseConfig() BaseConfig { LogLevel: DefaultPackageLogLevels(), LogFormat: LogFormatPlain, ProfListenAddress: "", - FastSync: true, + FastSyncMode: true, FilterPeers: false, DBBackend: "goleveldb", DBPath: "data", @@ -228,7 +234,7 @@ func TestBaseConfig() BaseConfig { cfg := DefaultBaseConfig() cfg.chainID = "tendermint_test" cfg.ProxyApp = "kvstore" - cfg.FastSync = false + cfg.FastSyncMode = false cfg.DBBackend = "memdb" return cfg } @@ -351,6 +357,12 @@ type RPCConfig struct { // See https://github.com/tendermint/tendermint/issues/3435 TimeoutBroadcastTxCommit time.Duration `mapstructure:"timeout_broadcast_tx_commit"` + // Maximum size of request body, in bytes + MaxBodyBytes int64 `mapstructure:"max_body_bytes"` + + // Maximum size of request header, in bytes + MaxHeaderBytes int `mapstructure:"max_header_bytes"` + // The path to a file containing certificate that is used to create the HTTPS server. // Migth be either absolute path or path related to tendermint's config directory. // @@ -385,6 +397,9 @@ func DefaultRPCConfig() *RPCConfig { MaxSubscriptionsPerClient: 5, TimeoutBroadcastTxCommit: 10 * time.Second, + MaxBodyBytes: int64(1000000), // 1MB + MaxHeaderBytes: 1 << 20, // same as the net/http default + TLSCertFile: "", TLSKeyFile: "", } @@ -417,6 +432,12 @@ func (cfg *RPCConfig) ValidateBasic() error { if cfg.TimeoutBroadcastTxCommit < 0 { return errors.New("timeout_broadcast_tx_commit can't be negative") } + if cfg.MaxBodyBytes < 0 { + return errors.New("max_body_bytes can't be negative") + } + if cfg.MaxHeaderBytes < 0 { + return errors.New("max_header_bytes can't be negative") + } return nil } @@ -616,6 +637,7 @@ type MempoolConfig struct { Size int `mapstructure:"size"` MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` CacheSize int `mapstructure:"cache_size"` + MaxTxBytes int `mapstructure:"max_tx_bytes"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -629,6 +651,7 @@ func DefaultMempoolConfig() *MempoolConfig { Size: 5000, MaxTxsBytes: 1024 * 1024 * 1024, // 1GB CacheSize: 10000, + MaxTxBytes: 1024 * 1024, // 1MB } } @@ -661,9 +684,44 @@ func (cfg *MempoolConfig) ValidateBasic() error { if cfg.CacheSize < 0 { return errors.New("cache_size can't be negative") } + if cfg.MaxTxBytes < 0 { + return errors.New("max_tx_bytes can't be negative") + } return nil } +//----------------------------------------------------------------------------- +// FastSyncConfig + +// FastSyncConfig defines the configuration for the Tendermint fast sync service +type FastSyncConfig struct { + Version string `mapstructure:"version"` +} + +// DefaultFastSyncConfig returns a default configuration for the fast sync service +func DefaultFastSyncConfig() *FastSyncConfig { + return &FastSyncConfig{ + Version: "v0", + } +} + +// TestFastSyncConfig returns a default configuration for the fast sync. +func TestFastSyncConfig() *FastSyncConfig { + return DefaultFastSyncConfig() +} + +// ValidateBasic performs basic validation. +func (cfg *FastSyncConfig) ValidateBasic() error { + switch cfg.Version { + case "v0": + return nil + case "v1": + return nil + default: + return fmt.Errorf("unknown fastsync version %s", cfg.Version) + } +} + //----------------------------------------------------------------------------- // ConsensusConfig diff --git a/config/config_test.go b/config/config_test.go index 6f9e3783e..6da032d07 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "reflect" "testing" "time" @@ -52,3 +53,116 @@ func TestTLSConfiguration(t *testing.T) { cfg.RPC.TLSKeyFile = "/abs/path/to/file.key" assert.Equal("/abs/path/to/file.key", cfg.RPC.KeyFile()) } + +func TestBaseConfigValidateBasic(t *testing.T) { + cfg := TestBaseConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with log format + cfg.LogFormat = "invalid" + assert.Error(t, cfg.ValidateBasic()) +} + +func TestRPCConfigValidateBasic(t *testing.T) { + cfg := TestRPCConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "GRPCMaxOpenConnections", + "MaxOpenConnections", + "MaxSubscriptionClients", + "MaxSubscriptionsPerClient", + "TimeoutBroadcastTxCommit", + "MaxBodyBytes", + "MaxHeaderBytes", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestP2PConfigValidateBasic(t *testing.T) { + cfg := TestP2PConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "MaxNumInboundPeers", + "MaxNumOutboundPeers", + "FlushThrottleTimeout", + "MaxPacketMsgPayloadSize", + "SendRate", + "RecvRate", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestMempoolConfigValidateBasic(t *testing.T) { + cfg := TestMempoolConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "Size", + "MaxTxsBytes", + "CacheSize", + "MaxTxBytes", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestFastSyncConfigValidateBasic(t *testing.T) { + cfg := TestFastSyncConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with version + cfg.Version = "v1" + assert.NoError(t, cfg.ValidateBasic()) + + cfg.Version = "invalid" + assert.Error(t, cfg.ValidateBasic()) +} + +func TestConsensusConfigValidateBasic(t *testing.T) { + cfg := TestConsensusConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "TimeoutPropose", + "TimeoutProposeDelta", + "TimeoutPrevote", + "TimeoutPrevoteDelta", + "TimeoutPrecommit", + "TimeoutPrecommitDelta", + "TimeoutCommit", + "CreateEmptyBlocksInterval", + "PeerGossipSleepDuration", + "PeerQueryMaj23SleepDuration", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestInstrumentationConfigValidateBasic(t *testing.T) { + cfg := TestInstrumentationConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with maximum open connections + cfg.MaxOpenConnections = -1 + assert.Error(t, cfg.ValidateBasic()) +} diff --git a/config/toml.go b/config/toml.go index 09117a0fb..268c96ff9 100644 --- a/config/toml.go +++ b/config/toml.go @@ -79,7 +79,7 @@ moniker = "{{ .BaseConfig.Moniker }}" # If this node is many blocks behind the tip of the chain, FastSync # allows them to catchup quickly by downloading blocks in parallel # and verifying their commits -fast_sync = {{ .BaseConfig.FastSync }} +fast_sync = {{ .BaseConfig.FastSyncMode }} # Database backend: goleveldb | cleveldb | boltdb # * goleveldb (github.com/syndtr/goleveldb - most popular implementation) @@ -192,6 +192,12 @@ max_subscriptions_per_client = {{ .RPC.MaxSubscriptionsPerClient }} # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "{{ .RPC.TimeoutBroadcastTxCommit }}" +# Maximum size of request body, in bytes +max_body_bytes = {{ .RPC.MaxBodyBytes }} + +# Maximum size of request header, in bytes +max_header_bytes = {{ .RPC.MaxHeaderBytes }} + # The path to a file containing certificate that is used to create the HTTPS server. # Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, @@ -288,6 +294,18 @@ max_txs_bytes = {{ .Mempool.MaxTxsBytes }} # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = {{ .Mempool.CacheSize }} +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes} + {amino overhead}. +max_tx_bytes = {{ .Mempool.MaxTxBytes }} + +##### fast sync configuration options ##### +[fastsync] + +# Fast Sync version to use: +# 1) "v0" (default) - the legacy fast sync implementation +# 2) "v1" - refactor of v0 version for better testability +version = "{{ .FastSync.Version }}" + ##### consensus configuration options ##### [consensus] diff --git a/consensus/wire.go b/consensus/codec.go similarity index 100% rename from consensus/wire.go rename to consensus/codec.go diff --git a/consensus/common_test.go b/consensus/common_test.go index 29db524ec..61d29d849 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -20,19 +20,19 @@ import ( "github.com/tendermint/tendermint/abci/example/counter" "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cstypes "github.com/tendermint/tendermint/consensus/types" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/privval" 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" ) const ( @@ -280,7 +280,7 @@ func newConsensusStateWithConfig(thisConfig *cfg.Config, state sm.State, pv type func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.State, pv types.PrivValidator, app abci.Application, blockDB dbm.DB) *ConsensusState { // Get BlockStore - blockStore := bc.NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus mtx := new(sync.Mutex) diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index d9feef9b4..c1d4f69a7 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -11,10 +11,10 @@ import ( "github.com/tendermint/tendermint/abci/example/code" abci "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tendermint/libs/db" mempl "github.com/tendermint/tendermint/mempool" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) // for testing @@ -82,14 +82,14 @@ func TestMempoolProgressInHigherRound(t *testing.T) { ensureNewRound(newRoundCh, height, round) // first round at first height ensureNewEventOnChannel(newBlockCh) // first block gets committed - height = height + 1 // moving to the next height + height++ // moving to the next height round = 0 ensureNewRound(newRoundCh, height, round) // first round at next height deliverTxsRange(cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round ensureNewTimeout(timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) // wait for the next round ensureNewEventOnChannel(newBlockCh) // now we can commit the block } @@ -155,12 +155,14 @@ func TestMempoolRmBadTx(t *testing.T) { // and the tx should get removed from the pool err := assertMempool(cs.txNotifier).CheckTx(txBytes, func(r *abci.Response) { if r.GetCheckTx().Code != code.CodeTypeBadNonce { - t.Fatalf("expected checktx to return bad nonce, got %v", r) + t.Errorf("expected checktx to return bad nonce, got %v", r) + return } checkTxRespCh <- struct{}{} }) if err != nil { - t.Fatalf("Error after CheckTx: %v", err) + t.Errorf("Error after CheckTx: %v", err) + return } // check for the tx @@ -180,7 +182,8 @@ func TestMempoolRmBadTx(t *testing.T) { case <-checkTxRespCh: // success case <-ticker: - t.Fatalf("Timed out waiting for tx to return") + t.Errorf("Timed out waiting for tx to return") + return } // Wait until the tx is removed @@ -189,7 +192,8 @@ func TestMempoolRmBadTx(t *testing.T) { case <-emptyMempoolCh: // success case <-ticker: - t.Fatalf("Timed out waiting for tx to be removed") + t.Errorf("Timed out waiting for tx to be removed") + return } } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index b237da6b5..0c76f7b4d 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -17,29 +17,31 @@ import ( abcicli "github.com/tendermint/tendermint/abci/client" "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" - dbm "github.com/tendermint/tendermint/libs/db" + cstypes "github.com/tendermint/tendermint/consensus/types" + cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/p2p/mock" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) //---------------------------------------------- // in-process testnets -func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( +func startConsensusNet(t *testing.T, css []*ConsensusState, n int) ( []*ConsensusReactor, []types.Subscription, []*types.EventBus, ) { - reactors := make([]*ConsensusReactor, N) + reactors := make([]*ConsensusReactor, n) blocksSubs := make([]types.Subscription, 0) - eventBuses := make([]*types.EventBus, N) - for i := 0; i < N; i++ { + eventBuses := make([]*types.EventBus, n) + for i := 0; i < n; i++ { /*logger, err := tmflags.ParseLogLevel("consensus:info,*:error", logger, "info") if err != nil { t.Fatal(err)}*/ reactors[i] = NewConsensusReactor(css[i], true) // so we dont start the consensus states @@ -58,7 +60,7 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( } } // make connected switches and start all reactors - p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { + p2p.MakeConnectedSwitches(config.P2P, n, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("CONSENSUS", reactors[i]) s.SetLogger(reactors[i].conS.Logger.With("module", "p2p")) return s @@ -68,7 +70,7 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( // If we started the state machines before everyone was connected, // we'd block when the cs fires NewBlockEvent and the peers are trying to start their reactors // TODO: is this still true with new pubsub? - for i := 0; i < N; i++ { + for i := 0; i < n; i++ { s := reactors[i].conS.GetState() reactors[i].SwitchToConsensus(s, 0) } @@ -133,7 +135,7 @@ func TestReactorWithEvidence(t *testing.T) { // css[i] = newConsensusStateWithConfig(thisConfig, state, privVals[i], app) blockDB := dbm.NewMemDB() - blockStore := bc.NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus mtx := new(sync.Mutex) @@ -235,7 +237,7 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { // send a tx if err := assertMempool(css[3].txNotifier).CheckTx([]byte{1, 2, 3}, nil); err != nil { - //t.Fatal(err) + t.Error(err) } // wait till everyone makes the first new block @@ -632,3 +634,253 @@ func capture() { count := runtime.Stack(trace, true) fmt.Printf("Stack of %d bytes: %s\n", count, trace) } + +//------------------------------------------------------------- +// Ensure basic validation of structs is functioning + +func TestNewRoundStepMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageStep cstypes.RoundStepType + messageLastCommitRound int + expectErr bool + }{ + {"Valid Message", 0, 0, 0x01, 1, false}, + {"Invalid Message", -1, 0, 0x01, 1, true}, + {"Invalid Message", 0, -1, 0x01, 1, true}, + {"Invalid Message", 0, 0, 0x00, 1, true}, + {"Invalid Message", 0, 0, 0x00, 0, true}, + {"Invalid Message", 1, 0, 0x01, 0, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := NewRoundStepMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Step: tc.messageStep, + LastCommitRound: tc.messageLastCommitRound, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestNewValidBlockMessageValidateBasic(t *testing.T) { + testBitArray := cmn.NewBitArray(1) + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageBlockParts *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, testBitArray, false}, + {"Invalid Message", -1, 0, testBitArray, true}, + {"Invalid Message", 0, -1, testBitArray, true}, + {"Invalid Message", 0, 0, cmn.NewBitArray(0), true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := NewValidBlockMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + BlockParts: tc.messageBlockParts, + } + + message.BlockPartsHeader.Total = 1 + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestProposalPOLMessageValidateBasic(t *testing.T) { + testBitArray := cmn.NewBitArray(1) + testCases := []struct { + testName string + messageHeight int64 + messageProposalPOLRound int + messageProposalPOL *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, testBitArray, false}, + {"Invalid Message", -1, 0, testBitArray, true}, + {"Invalid Message", 0, -1, testBitArray, true}, + {"Invalid Message", 0, 0, cmn.NewBitArray(0), true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := ProposalPOLMessage{ + Height: tc.messageHeight, + ProposalPOLRound: tc.messageProposalPOLRound, + ProposalPOL: tc.messageProposalPOL, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBlockPartMessageValidateBasic(t *testing.T) { + testPart := new(types.Part) + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messagePart *types.Part + expectErr bool + }{ + {"Valid Message", 0, 0, testPart, false}, + {"Invalid Message", -1, 0, testPart, true}, + {"Invalid Message", 0, -1, testPart, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := BlockPartMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Part: tc.messagePart, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } + + message := BlockPartMessage{Height: 0, Round: 0, Part: new(types.Part)} + message.Part.Index = -1 + + assert.Equal(t, true, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") +} + +func TestHasVoteMessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageIndex int + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, 0, false}, + {"Invalid Message", -1, 0, validSignedMsgType, 0, true}, + {"Invalid Message", 0, -1, validSignedMsgType, 0, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, 0, true}, + {"Invalid Message", 0, 0, validSignedMsgType, -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := HasVoteMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + Index: tc.messageIndex, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestVoteSetMaj23MessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + validBlockID := types.BlockID{} + invalidBlockID := types.BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: types.PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageBlockID types.BlockID + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, validBlockID, false}, + {"Invalid Message", -1, 0, validSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, -1, validSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, 0, validSignedMsgType, invalidBlockID, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := VoteSetMaj23Message{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + BlockID: tc.messageBlockID, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestVoteSetBitsMessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + validBlockID := types.BlockID{} + invalidBlockID := types.BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: types.PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + testBitArray := cmn.NewBitArray(1) + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageBlockID types.BlockID + messageVotes *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, validBlockID, testBitArray, false}, + {"Invalid Message", -1, 0, validSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, -1, validSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, 0, validSignedMsgType, invalidBlockID, testBitArray, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := VoteSetBitsMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + // Votes: tc.messageVotes, + BlockID: tc.messageBlockID, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} diff --git a/consensus/replay.go b/consensus/replay.go index 2c4377ffa..43ad557f7 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -13,8 +13,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" //auto "github.com/tendermint/tendermint/libs/autofile" + dbm "github.com/tendermint/tm-db" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" @@ -141,14 +141,16 @@ func (cs *ConsensusState) catchupReplay(csHeight int64) error { var msg *TimedWALMessage dec := WALDecoder{gr} +LOOP: for { msg, err = dec.Decode() - if err == io.EOF { - break - } else if IsDataCorruptionError(err) { + switch { + case err == io.EOF: + break LOOP + case IsDataCorruptionError(err): cs.Logger.Error("data has been corrupted in last height of consensus WAL", "err", err, "height", csHeight) return err - } else if err != nil { + case err != nil: return err } @@ -320,11 +322,9 @@ func (h *Handshaker) ReplayBlocks( } state.Validators = types.NewValidatorSet(vals) state.NextValidators = types.NewValidatorSet(vals) - } else { + } else if len(h.genDoc.Validators) == 0 { // If validator set is not set in genesis and still empty after InitChain, exit. - if len(h.genDoc.Validators) == 0 { - return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") - } + return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") } if res.ConsensusParams != nil { @@ -335,19 +335,20 @@ func (h *Handshaker) ReplayBlocks( } // First handle edge cases and constraints on the storeBlockHeight. - if storeBlockHeight == 0 { + switch { + case storeBlockHeight == 0: assertAppHashEqualsOneFromState(appHash, state) return appHash, nil - } else if storeBlockHeight < appBlockHeight { + case storeBlockHeight < appBlockHeight: // the app should never be ahead of the store (but this is under app's control) return appHash, sm.ErrAppBlockHeightTooHigh{CoreHeight: storeBlockHeight, AppHeight: appBlockHeight} - } else if storeBlockHeight < stateBlockHeight { + case storeBlockHeight < stateBlockHeight: // the state should never be ahead of the store (this is under tendermint's control) panic(fmt.Sprintf("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) - } else if storeBlockHeight > stateBlockHeight+1 { + case storeBlockHeight > stateBlockHeight+1: // store should be at most one ahead of the state (this is under tendermint's control) panic(fmt.Sprintf("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) } @@ -371,12 +372,13 @@ func (h *Handshaker) ReplayBlocks( } else if storeBlockHeight == stateBlockHeight+1 { // We saved the block in the store but haven't updated the state, // so we'll need to replay a block using the WAL. - if appBlockHeight < stateBlockHeight { + switch { + case appBlockHeight < stateBlockHeight: // the app is further behind than it should be, so replay blocks // but leave the last block to go through the WAL return h.replayBlocks(state, proxyApp, appBlockHeight, storeBlockHeight, true) - } else if appBlockHeight == stateBlockHeight { + case appBlockHeight == stateBlockHeight: // We haven't run Commit (both the state and app are one block behind), // so replayBlock with the real app. // NOTE: We could instead use the cs.WAL on cs.Start, @@ -385,7 +387,7 @@ func (h *Handshaker) ReplayBlocks( state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus()) return state.AppHash, err - } else if appBlockHeight == storeBlockHeight { + case appBlockHeight == storeBlockHeight: // We ran Commit, but didn't save the state, so replayBlock with mock app. abciResponses, err := sm.LoadABCIResponses(h.stateDB, storeBlockHeight) if err != nil { diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 5bb73484e..2c0f8a327 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -10,15 +10,15 @@ import ( "strings" "github.com/pkg/errors" + dbm "github.com/tendermint/tm-db" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -231,10 +231,8 @@ func (pb *playback) replayConsoleLoop() int { fmt.Println("back takes an integer argument") } else if i > pb.count { fmt.Printf("argument to back must not be larger than the current count (%d)\n", pb.count) - } else { - if err := pb.replayReset(i, newStepSub); err != nil { - pb.cs.Logger.Error("Replay reset error", "err", err) - } + } else if err := pb.replayReset(i, newStepSub); err != nil { + pb.cs.Logger.Error("Replay reset error", "err", err) } } @@ -280,7 +278,7 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo dbType := dbm.DBBackendType(config.DBBackend) // Get BlockStore blockStoreDB := dbm.NewDB("blockstore", dbType, config.DBDir()) - blockStore := bc.NewBlockStore(blockStoreDB) + blockStore := store.NewBlockStore(blockStoreDB) // Get State stateDB := dbm.NewDB("state", dbType, config.DBDir()) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index bbb5b6678..5b454248d 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -22,15 +22,14 @@ import ( cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" + dbm "github.com/tendermint/tm-db" ) func TestMain(m *testing.M) { @@ -546,8 +545,7 @@ func TestMockProxyApp(t *testing.T) { abciRes.DeliverTx = make([]*abci.ResponseDeliverTx, len(loadedAbciRes.DeliverTx)) // Execute transactions and get hash. proxyCb := func(req *abci.Request, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_DeliverTx: + if r, ok := res.Value.(*abci.Response_DeliverTx); ok { // TODO: make use of res.Log // TODO: make use of this info // Blocks may include invalid txs. @@ -849,31 +847,14 @@ func makeBlocks(n int, state *sm.State, privVal types.PrivValidator) []*types.Bl return blocks } -func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { - addr := privVal.GetPubKey().Address() - idx, _ := valset.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - - privVal.SignVote(header.ChainID, vote) - - return vote -} - func makeBlock(state sm.State, lastBlock *types.Block, lastBlockMeta *types.BlockMeta, privVal types.PrivValidator, height int64) (*types.Block, *types.PartSet) { lastCommit := types.NewCommit(types.BlockID{}, nil) if height > 1 { - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVal).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + vote, _ := types.MakeVote(lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVal, lastBlock.Header.ChainID) + voteCommitSig := vote.CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{voteCommitSig}) } return state.MakeBlock(height, []types.Tx{}, lastCommit, nil, state.Validators.GetProposer().Address) diff --git a/consensus/state.go b/consensus/state.go index 1f6bad9ab..5dfeaf907 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -690,13 +690,13 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { cs.statsMsgQueue <- mi } - if err == ErrAddingVote { - // TODO: punish peer - // We probably don't want to stop the peer here. The vote does not - // necessarily comes from a malicious peer but can be just broadcasted by - // a typical peer. - // https://github.com/tendermint/tendermint/issues/1281 - } + // if err == ErrAddingVote { + // TODO: punish peer + // We probably don't want to stop the peer here. The vote does not + // necessarily comes from a malicious peer but can be just broadcasted by + // a typical peer. + // https://github.com/tendermint/tendermint/issues/1281 + // } // NOTE: the vote is broadcast to peers by the reactor listening // for vote events @@ -709,7 +709,7 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { return } - if err != nil { + if err != nil { // nolint:staticcheck // Causes TestReactorValidatorSetChanges to timeout // https://github.com/tendermint/tendermint/issues/3406 // cs.Logger.Error("Error with msg", "height", cs.Height, "round", cs.Round, @@ -924,10 +924,8 @@ func (cs *ConsensusState) defaultDecideProposal(height int64, round int) { } cs.Logger.Info("Signed proposal", "height", height, "round", round, "proposal", proposal) cs.Logger.Debug(fmt.Sprintf("Signed proposal block: %v", block)) - } else { - if !cs.replayMode { - cs.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err) - } + } else if !cs.replayMode { + cs.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err) } } @@ -954,14 +952,15 @@ func (cs *ConsensusState) isProposalComplete() bool { // NOTE: keep it side-effect free for clarity. func (cs *ConsensusState) createProposalBlock() (block *types.Block, blockParts *types.PartSet) { var commit *types.Commit - if cs.Height == 1 { + switch { + case cs.Height == 1: // We're creating a proposal for the first block. // The commit is empty, but not nil. commit = types.NewCommit(types.BlockID{}, nil) - } else if cs.LastCommit.HasTwoThirdsMajority() { + case cs.LastCommit.HasTwoThirdsMajority(): // Make the commit from LastCommit commit = cs.LastCommit.MakeCommit() - } else { + default: // This shouldn't happen. cs.Logger.Error("enterPropose: Cannot propose anything: No commit for the previous block.") return @@ -1227,9 +1226,10 @@ func (cs *ConsensusState) enterCommit(height int64, commitRound int) { cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartsHeader) cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()) cs.evsw.FireEvent(types.EventValidBlock, &cs.RoundState) - } else { - // We just need to keep waiting. } + // else { + // We just need to keep waiting. + // } } } @@ -1518,9 +1518,11 @@ func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, err cs.evpool.AddEvidence(voteErr.DuplicateVoteEvidence) return added, err } else { - // Probably an invalid signature / Bad peer. - // Seems this can also err sometimes with "Unexpected step" - perhaps not from a bad peer ? - cs.Logger.Error("Error attempting to add vote", "err", err) + // Either + // 1) bad peer OR + // 2) not a bad peer? this can also err sometimes with "Unexpected step" OR + // 3) tmkms use with multiple validators connecting to a single tmkms instance (https://github.com/tendermint/tendermint/issues/3839). + cs.Logger.Info("Error attempting to add vote", "err", err) return added, ErrAddingVote } } @@ -1629,17 +1631,18 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, } // If +2/3 prevotes for *anything* for future round: - if cs.Round < vote.Round && prevotes.HasTwoThirdsAny() { + switch { + case cs.Round < vote.Round && prevotes.HasTwoThirdsAny(): // Round-skip if there is any 2/3+ of votes ahead of us cs.enterNewRound(height, vote.Round) - } else if cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step { // current round + case cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step: // current round blockID, ok := prevotes.TwoThirdsMajority() if ok && (cs.isProposalComplete() || len(blockID.Hash) == 0) { cs.enterPrecommit(height, vote.Round) } else if prevotes.HasTwoThirdsAny() { cs.enterPrevoteWait(height, vote.Round) } - } else if cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round { + case cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round: // If the proposal is now complete, enter prevote of cs.Round. if cs.isProposalComplete() { cs.enterPrevote(height, cs.Round) diff --git a/consensus/state_test.go b/consensus/state_test.go index 93ef0d4cb..8409f2235 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -181,7 +181,7 @@ func TestStateBadProposal(t *testing.T) { propBlock, _ := cs1.createProposalBlock() //changeProposer(t, cs1, vs2) // make the second validator the proposer by incrementing round - round = round + 1 + round++ incrementRound(vss[1:]...) // make the block bad by tampering with statehash @@ -374,7 +374,7 @@ func TestStateLockNoPOL(t *testing.T) { /// - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 1") /* @@ -418,7 +418,7 @@ func TestStateLockNoPOL(t *testing.T) { // then we enterPrecommitWait and timeout into NewRound ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // entering new round + round++ // entering new round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 2") /* @@ -460,7 +460,7 @@ func TestStateLockNoPOL(t *testing.T) { incrementRound(vs2) - round = round + 1 // entering new round + round++ // entering new round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 3") /* @@ -544,7 +544,7 @@ func TestStateLockPOLRelock(t *testing.T) { // timeout to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round //XXX: this isnt guaranteed to get there before the timeoutPropose ... if err := cs1.SetProposalAndBlock(prop, propBlock, propBlockParts, "some peer"); err != nil { t.Fatal(err) @@ -621,8 +621,6 @@ func TestStateLockPOLUnlock(t *testing.T) { // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits from the rest signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs4) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) @@ -637,7 +635,7 @@ func TestStateLockPOLUnlock(t *testing.T) { lockedBlockHash := rs.LockedBlock.Hash() incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 1") @@ -720,7 +718,7 @@ func TestStateLockPOLSafety1(t *testing.T) { incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) //XXX: this isnt guaranteed to get there before the timeoutPropose ... @@ -757,7 +755,7 @@ func TestStateLockPOLSafety1(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -823,7 +821,7 @@ func TestStateLockPOLSafety2(t *testing.T) { incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round t.Log("### ONTO Round 1") // jump in at round 1 startTestRound(cs1, height, round) @@ -852,7 +850,7 @@ func TestStateLockPOLSafety2(t *testing.T) { // timeout of precommit wait to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round // in round 2 we see the polkad block from round 0 newProp := types.NewProposal(height, round, 0, propBlockID0) if err := vs3.SignProposal(config.ChainID(), newProp); err != nil { @@ -922,7 +920,7 @@ func TestProposeValidBlock(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -947,14 +945,14 @@ func TestProposeValidBlock(t *testing.T) { signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 2 // moving to the next round + round += 2 // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("### ONTO ROUND 3") ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -1046,7 +1044,7 @@ func TestSetValidBlockOnDelayedProposal(t *testing.T) { voteCh := subscribeToVoter(cs1, addr) proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) - round = round + 1 // move to round in which P0 is not proposer + round++ // move to round in which P0 is not proposer incrementRound(vs2, vs3, vs4) startTestRound(cs1, cs1.Height, round) @@ -1125,7 +1123,7 @@ func TestWaitingTimeoutProposeOnNewRound(t *testing.T) { incrementRound(vss[1:]...) signAddVotes(cs1, types.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) rs := cs1.GetRoundState() @@ -1159,7 +1157,7 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { incrementRound(vss[1:]...) signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) ensurePrecommit(voteCh, height, round) @@ -1167,7 +1165,7 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) } @@ -1317,8 +1315,6 @@ func TestStartNextHeightCorrectly(t *testing.T) { // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) @@ -1370,8 +1366,6 @@ func TestResetTimeoutPrecommitUponNewHeight(t *testing.T) { ensurePrecommit(voteCh, height, round) validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) @@ -1517,7 +1511,7 @@ func TestStateHalt1(t *testing.T) { // timeout to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) rs = cs1.GetRoundState() diff --git a/consensus/types/wire.go b/consensus/types/codec.go similarity index 100% rename from consensus/types/wire.go rename to consensus/types/codec.go diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index 2faff27b5..8b5bbc2f0 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -12,16 +12,16 @@ import ( "github.com/pkg/errors" "github.com/tendermint/tendermint/abci/example/kvstore" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" + db "github.com/tendermint/tm-db" ) // WALGenerateNBlocks generates a consensus WAL. It does this by spinning up a @@ -55,7 +55,8 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { } state.Version.Consensus.App = kvstore.ProtocolVersion sm.SaveState(stateDB, state) - blockStore := bc.NewBlockStore(blockStoreDB) + blockStore := store.NewBlockStore(blockStoreDB) + proxyApp := proxy.NewAppConns(proxy.NewLocalClientCreator(app)) proxyApp.SetLogger(logger.With("module", "proxy")) if err := proxyApp.Start(); err != nil { diff --git a/crypto/ed25519/ed25519.go b/crypto/ed25519/ed25519.go index bc60838d5..8947608ae 100644 --- a/crypto/ed25519/ed25519.go +++ b/crypto/ed25519/ed25519.go @@ -54,7 +54,7 @@ func (privKey PrivKeyEd25519) Bytes() []byte { // incorrect signature. func (privKey PrivKeyEd25519) Sign(msg []byte) ([]byte, error) { signatureBytes := ed25519.Sign(privKey[:], msg) - return signatureBytes[:], nil + return signatureBytes, nil } // PubKey gets the corresponding public key from the private key. @@ -100,7 +100,7 @@ func GenPrivKey() PrivKeyEd25519 { // genPrivKey generates a new ed25519 private key using the provided reader. func genPrivKey(rand io.Reader) PrivKeyEd25519 { seed := make([]byte, 32) - _, err := io.ReadFull(rand, seed[:]) + _, err := io.ReadFull(rand, seed) if err != nil { panic(err) } diff --git a/crypto/internal/benchmarking/bench.go b/crypto/internal/benchmarking/bench.go index c988de48e..43ab312f0 100644 --- a/crypto/internal/benchmarking/bench.go +++ b/crypto/internal/benchmarking/bench.go @@ -24,10 +24,10 @@ func (zeroReader) Read(buf []byte) (int, error) { // BenchmarkKeyGeneration benchmarks the given key generation algorithm using // a dummy reader. -func BenchmarkKeyGeneration(b *testing.B, GenerateKey func(reader io.Reader) crypto.PrivKey) { +func BenchmarkKeyGeneration(b *testing.B, generateKey func(reader io.Reader) crypto.PrivKey) { var zero zeroReader for i := 0; i < b.N; i++ { - GenerateKey(zero) + generateKey(zero) } } diff --git a/crypto/merkle/wire.go b/crypto/merkle/codec.go similarity index 100% rename from crypto/merkle/wire.go rename to crypto/merkle/codec.go diff --git a/crypto/merkle/proof.go b/crypto/merkle/proof.go index 5e2a3ab12..ad101d94d 100644 --- a/crypto/merkle/proof.go +++ b/crypto/merkle/proof.go @@ -3,7 +3,7 @@ package merkle import ( "bytes" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) //---------------------------------------- @@ -44,11 +44,11 @@ func (poz ProofOperators) Verify(root []byte, keypath string, args [][]byte) (er key := op.GetKey() if len(key) != 0 { if len(keys) == 0 { - return cmn.NewError("Key path has insufficient # of parts: expected no more keys but got %+v", string(key)) + return errors.Errorf("Key path has insufficient # of parts: expected no more keys but got %+v", string(key)) } lastKey := keys[len(keys)-1] if !bytes.Equal(lastKey, key) { - return cmn.NewError("Key mismatch on operation #%d: expected %+v but got %+v", i, string(lastKey), string(key)) + return errors.Errorf("Key mismatch on operation #%d: expected %+v but got %+v", i, string(lastKey), string(key)) } keys = keys[:len(keys)-1] } @@ -58,10 +58,10 @@ func (poz ProofOperators) Verify(root []byte, keypath string, args [][]byte) (er } } if !bytes.Equal(root, args[0]) { - return cmn.NewError("Calculated root hash is invalid: expected %+v but got %+v", root, args[0]) + return errors.Errorf("Calculated root hash is invalid: expected %+v but got %+v", root, args[0]) } if len(keys) != 0 { - return cmn.NewError("Keypath not consumed all") + return errors.New("Keypath not consumed all") } return nil } @@ -92,7 +92,7 @@ func (prt *ProofRuntime) RegisterOpDecoder(typ string, dec OpDecoder) { func (prt *ProofRuntime) Decode(pop ProofOp) (ProofOperator, error) { decoder := prt.decoders[pop.Type] if decoder == nil { - return nil, cmn.NewError("unrecognized proof type %v", pop.Type) + return nil, errors.Errorf("unrecognized proof type %v", pop.Type) } return decoder(pop) } @@ -102,7 +102,7 @@ func (prt *ProofRuntime) DecodeProof(proof *Proof) (ProofOperators, error) { for _, pop := range proof.Ops { operator, err := prt.Decode(pop) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding a proof operator") + return nil, errors.Wrap(err, "decoding a proof operator") } poz = append(poz, operator) } @@ -122,7 +122,7 @@ func (prt *ProofRuntime) VerifyAbsence(proof *Proof, root []byte, keypath string func (prt *ProofRuntime) Verify(proof *Proof, root []byte, keypath string, args [][]byte) (err error) { poz, err := prt.DecodeProof(proof) if err != nil { - return cmn.ErrorWrap(err, "decoding proof") + return errors.Wrap(err, "decoding proof") } return poz.Verify(root, keypath, args) } diff --git a/crypto/merkle/proof_key_path.go b/crypto/merkle/proof_key_path.go index aec93e826..7ea67853b 100644 --- a/crypto/merkle/proof_key_path.go +++ b/crypto/merkle/proof_key_path.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) /* @@ -87,7 +87,7 @@ func (pth KeyPath) String() string { // Each key must use a known encoding. func KeyPathToKeys(path string) (keys [][]byte, err error) { if path == "" || path[0] != '/' { - return nil, cmn.NewError("key path string must start with a forward slash '/'") + return nil, errors.New("key path string must start with a forward slash '/'") } parts := strings.Split(path[1:], "/") keys = make([][]byte, len(parts)) @@ -96,13 +96,13 @@ func KeyPathToKeys(path string) (keys [][]byte, err error) { hexPart := part[2:] key, err := hex.DecodeString(hexPart) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding hex-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding hex-encoded part #%d: /%s", i, part) } keys[i] = key } else { key, err := url.PathUnescape(part) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding url-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding url-encoded part #%d: /%s", i, part) } keys[i] = []byte(key) // TODO Test this with random bytes, I'm not sure that it works for arbitrary bytes... } diff --git a/crypto/merkle/proof_simple_value.go b/crypto/merkle/proof_simple_value.go index 247921ad5..2c89bb5fd 100644 --- a/crypto/merkle/proof_simple_value.go +++ b/crypto/merkle/proof_simple_value.go @@ -4,8 +4,9 @@ import ( "bytes" "fmt" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto/tmhash" - cmn "github.com/tendermint/tendermint/libs/common" ) const ProofOpSimpleValue = "simple:v" @@ -39,12 +40,12 @@ func NewSimpleValueOp(key []byte, proof *SimpleProof) SimpleValueOp { func SimpleValueOpDecoder(pop ProofOp) (ProofOperator, error) { if pop.Type != ProofOpSimpleValue { - return nil, cmn.NewError("unexpected ProofOp.Type; got %v, want %v", pop.Type, ProofOpSimpleValue) + return nil, errors.Errorf("unexpected ProofOp.Type; got %v, want %v", pop.Type, ProofOpSimpleValue) } var op SimpleValueOp // a bit strange as we'll discard this, but it works. err := cdc.UnmarshalBinaryLengthPrefixed(pop.Data, &op) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding ProofOp.Data into SimpleValueOp") + return nil, errors.Wrap(err, "decoding ProofOp.Data into SimpleValueOp") } return NewSimpleValueOp(pop.Key, op.Proof), nil } @@ -64,7 +65,7 @@ func (op SimpleValueOp) String() string { func (op SimpleValueOp) Run(args [][]byte) ([][]byte, error) { if len(args) != 1 { - return nil, cmn.NewError("expected 1 arg, got %v", len(args)) + return nil, errors.Errorf("expected 1 arg, got %v", len(args)) } value := args[0] hasher := tmhash.New() @@ -78,7 +79,7 @@ func (op SimpleValueOp) Run(args [][]byte) ([][]byte, error) { kvhash := leafHash(bz.Bytes()) if !bytes.Equal(kvhash, op.Proof.LeafHash) { - return nil, cmn.NewError("leaf hash mismatch: want %X got %X", op.Proof.LeafHash, kvhash) + return nil, errors.Errorf("leaf hash mismatch: want %X got %X", op.Proof.LeafHash, kvhash) } return [][]byte{ diff --git a/crypto/merkle/proof_test.go b/crypto/merkle/proof_test.go index 4de3246f1..4dc916ac9 100644 --- a/crypto/merkle/proof_test.go +++ b/crypto/merkle/proof_test.go @@ -3,9 +3,9 @@ package merkle import ( "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" amino "github.com/tendermint/go-amino" - cmn "github.com/tendermint/tendermint/libs/common" ) const ProofOpDomino = "test:domino" @@ -34,7 +34,7 @@ func DominoOpDecoder(pop ProofOp) (ProofOperator, error) { var op DominoOp // a bit strange as we'll discard this, but it works. err := amino.UnmarshalBinaryLengthPrefixed(pop.Data, &op) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding ProofOp.Data into SimpleValueOp") + return nil, errors.Wrap(err, "decoding ProofOp.Data into SimpleValueOp") } return NewDominoOp(string(pop.Key), op.Input, op.Output), nil } @@ -50,10 +50,10 @@ func (dop DominoOp) ProofOp() ProofOp { func (dop DominoOp) Run(input [][]byte) (output [][]byte, err error) { if len(input) != 1 { - return nil, cmn.NewError("Expected input of length 1") + return nil, errors.New("Expected input of length 1") } if string(input[0]) != dop.Input { - return nil, cmn.NewError("Expected input %v, got %v", + return nil, errors.Errorf("Expected input %v, got %v", dop.Input, string(input[0])) } return [][]byte{[]byte(dop.Output)}, nil diff --git a/crypto/merkle/simple_proof.go b/crypto/merkle/simple_proof.go index f01dcdca1..da32157db 100644 --- a/crypto/merkle/simple_proof.go +++ b/crypto/merkle/simple_proof.go @@ -2,10 +2,9 @@ package merkle import ( "bytes" - "errors" "fmt" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) // SimpleProof represents a simple Merkle proof. @@ -75,11 +74,11 @@ func (sp *SimpleProof) Verify(rootHash []byte, leaf []byte) error { return errors.New("Proof index cannot be negative") } if !bytes.Equal(sp.LeafHash, leafHash) { - return cmn.NewError("invalid leaf hash: wanted %X got %X", leafHash, sp.LeafHash) + return errors.Errorf("invalid leaf hash: wanted %X got %X", leafHash, sp.LeafHash) } computedHash := sp.ComputeRootHash() if !bytes.Equal(computedHash, rootHash) { - return cmn.NewError("invalid root hash: wanted %X got %X", rootHash, computedHash) + return errors.Errorf("invalid root hash: wanted %X got %X", rootHash, computedHash) } return nil } @@ -162,11 +161,12 @@ func (spn *SimpleProofNode) FlattenAunts() [][]byte { // Nonrecursive impl. innerHashes := [][]byte{} for spn != nil { - if spn.Left != nil { + switch { + case spn.Left != nil: innerHashes = append(innerHashes, spn.Left.Hash) - } else if spn.Right != nil { + case spn.Right != nil: innerHashes = append(innerHashes, spn.Right.Hash) - } else { + default: break } spn = spn.Parent diff --git a/crypto/multisig/wire.go b/crypto/multisig/codec.go similarity index 100% rename from crypto/multisig/wire.go rename to crypto/multisig/codec.go diff --git a/crypto/secp256k1/internal/secp256k1/curve.go b/crypto/secp256k1/internal/secp256k1/curve.go index 5409ee1d2..df87200f2 100644 --- a/crypto/secp256k1/internal/secp256k1/curve.go +++ b/crypto/secp256k1/internal/secp256k1/curve.go @@ -30,6 +30,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// nolint:gocritic package secp256k1 import ( diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 9adfc5953..70b404c42 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -34,6 +34,14 @@ module.exports = { "/introduction/what-is-tendermint" ] }, + { + title: "Guides", + collapsable: false, + children: [ + "/guides/go-built-in", + "/guides/go" + ] + }, { title: "Apps", collapsable: false, diff --git a/docs/DOCS_README.md b/docs/DOCS_README.md index 49c2030a2..5b743cfa9 100644 --- a/docs/DOCS_README.md +++ b/docs/DOCS_README.md @@ -6,14 +6,12 @@ The documentation for Tendermint Core is hosted at: - https://tendermint-staging.interblock.io/docs/ built from the files in this (`/docs`) directory for -[master](https://github.com/tendermint/tendermint/tree/master/docs) -and [develop](https://github.com/tendermint/tendermint/tree/develop/docs), -respectively. +[master](https://github.com/tendermint/tendermint/tree/master/docs) respectively. ## How It Works There is a CircleCI job listening for changes in the `/docs` directory, on both -the `master` and `develop` branches. Any updates to files in this directory +the `master` branch. Any updates to files in this directory on those branches will automatically trigger a website deployment. Under the hood, the private website repository has a `make build-docs` target consumed by a CircleCI job in that repo. @@ -35,7 +33,7 @@ of the sidebar. **NOTE:** Strongly consider the existing links - both within this directory and to the website docs - when moving or deleting files. -Links to directories *MUST* end in a `/`. +Links to directories _MUST_ end in a `/`. Relative links should be used nearly everywhere, having discovered and weighed the following: @@ -101,4 +99,4 @@ We are using [Algolia](https://www.algolia.com) to power full-text search. This ## Consistency Because the build processes are identical (as is the information contained herein), this file should be kept in sync as -much as possible with its [counterpart in the Cosmos SDK repo](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/DOCS_README.md). +much as possible with its [counterpart in the Cosmos SDK repo](https://github.com/cosmos/cosmos-sdk/blob/master/docs/DOCS_README.md). diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index 7e9db91b9..4b21a4b2d 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -62,7 +62,7 @@ as `abci-cli` above. The kvstore just stores transactions in a merkle tree. Its code can be found -[here](https://github.com/tendermint/tendermint/blob/develop/abci/cmd/abci-cli/abci-cli.go) +[here](https://github.com/tendermint/tendermint/blob/master/abci/cmd/abci-cli/abci-cli.go) and looks like: ``` @@ -137,7 +137,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/develop/abci/server). See the +Golang](https://github.com/tendermint/tendermint/tree/master/abci/server). See the [list of other ABCI implementations](./ecosystem.md) for servers in other languages. @@ -324,7 +324,7 @@ But the ultimate flexibility comes from being able to write the application easily in any language. We have implemented the counter in a number of languages [see the -example directory](https://github.com/tendermint/tendermint/tree/develop/abci/example). +example directory](https://github.com/tendermint/tendermint/tree/master/abci/example). To run the Node.js version, fist download & install [the Javascript ABCI server](https://github.com/tendermint/js-abci): diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index c9983beaa..ba21d3a37 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -48,9 +48,9 @@ open ABCI connection with the application, which hosts an ABCI server. Shown are the request and response types sent on each connection. Most of the examples below are from [kvstore -application](https://github.com/tendermint/tendermint/blob/develop/abci/example/kvstore/kvstore.go), +application](https://github.com/tendermint/tendermint/blob/master/abci/example/kvstore/kvstore.go), which is a part of the abci repo. [persistent_kvstore -application](https://github.com/tendermint/tendermint/blob/develop/abci/example/kvstore/persistent_kvstore.go) +application](https://github.com/tendermint/tendermint/blob/master/abci/example/kvstore/persistent_kvstore.go) is used to show `BeginBlock`, `EndBlock` and `InitChain` example implementations. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1cfc7ddce..0ff6682ac 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -20,3 +20,41 @@ it stands today. If recorded decisions turned out to be lacking, convene a discussion, record the new decisions here, and then modify the code to match. Note the context/background should be written in the present tense. + +### Table of Contents: + +- [ADR-001-Logging](./adr-001-logging.md) +- [ADR-002-Event-Subscription](./adr-002-event-subscription.md) +- [ADR-003-ABCI-APP-RPC](./adr-003-abci-app-rpc.md) +- [ADR-004-Historical-Validators](./adr-004-historical-validators.md) +- [ADR-005-Consensus-Params](./adr-005-consensus-params.md) +- [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-010-Crypto-Changes](./adr-010-crypto-changes.md) +- [ADR-011-Monitoring](./adr-011-monitoring.md) +- [ADR-012-Peer-Transport](./adr-012-peer-transport.md) +- [ADR-013-Symmetric-Crypto](./adr-013-symmetric-crypto.md) +- [ADR-014-Secp-Malleability](./adr-014-secp-malleability.md) +- [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-019-Multisigs](./adr-019-multisigs.md) +- [ADR-020-Block-Size](./adr-020-block-size.md) +- [ADR-021-ABCI-Events](./adr-021-abci-events.md) +- [ADR-022-ABCI-Errors](./adr-022-abci-errors.md) +- [ADR-023-ABCI-Propose-tx](./adr-023-ABCI-propose-tx.md) +- [ADR-024-Sign-Bytes](./adr-024-sign-bytes.md) +- [ADR-025-Commit](./adr-025-commit.md) +- [ADR-026-General-Merkle-Proof](./adr-026-general-merkle-proof.md) +- [ADR-029-Check-Tx-Consensus](./adr-029-check-tx-consensus.md) +- [ADR-030-Consensus-Refactor](./adr-030-consensus-refactor.md) +- [ADR-033-Pubsub](./adr-033-pubsub.md) +- [ADR-034-Priv-Validator-File-Structure](./adr-034-priv-validator-file-structure.md) +- [ADR-035-Documentation](./adr-035-documentation.md) +- [ADR-037-Deliver-Block](./adr-037-deliver-block.md) +- [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) diff --git a/docs/architecture/adr-018-ABCI-Validators.md b/docs/architecture/adr-018-ABCI-Validators.md index b632da855..f40efca15 100644 --- a/docs/architecture/adr-018-ABCI-Validators.md +++ b/docs/architecture/adr-018-ABCI-Validators.md @@ -2,10 +2,7 @@ ## Changelog -016-08-2018: Follow up from review: - - Revert changes to commit round - - Remind about justification for removing pubkey - - Update pros/cons +016-08-2018: Follow up from review: - Revert changes to commit round - Remind about justification for removing pubkey - Update pros/cons 05-08-2018: Initial draft ## Context @@ -35,11 +32,11 @@ message ValidatorUpdate { } ``` -As noted in ADR-009[https://github.com/tendermint/tendermint/blob/develop/docs/architecture/adr-009-ABCI-design.md], +As noted in ADR-009[https://github.com/tendermint/tendermint/blob/master/docs/architecture/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 -are *required* to store pubkeys in state (or use much less efficient lazy means +are _required_ to store pubkeys in state (or use much less efficient lazy means of verifying BeginBlock data). ### RequestBeginBlock diff --git a/docs/architecture/adr-042-state-sync.md b/docs/architecture/adr-042-state-sync.md new file mode 100644 index 000000000..d525a4974 --- /dev/null +++ b/docs/architecture/adr-042-state-sync.md @@ -0,0 +1,239 @@ +# ADR 042: State Sync Design + +## Changelog + +2019-06-27: Init by EB +2019-07-04: Follow up by brapse + +## Context +StateSync is a feature which would allow a new node to receive a +snapshot of the application state without downloading blocks or going +through consensus. Once downloaded, the node could switch to FastSync +and eventually participate in consensus. The goal of StateSync is to +facilitate setting up a new node as quickly as possible. + +## Considerations +Because Tendermint doesn't know anything about the application state, +StateSync will broker messages between nodes and through +the ABCI to an opaque applicaton. The implementation will have multiple +touch points on both the tendermint code base and ABCI application. + +* A StateSync reactor to facilitate peer communication - Tendermint +* A Set of ABCI messages to transmit application state to the reactor - Tendermint +* A Set of MultiStore APIs for exposing snapshot data to the ABCI - ABCI application +* A Storage format with validation and performance considerations - ABCI application + +### Implementation Properties +Beyond the approach, any implementation of StateSync can be evaluated +across different criteria: + +* Speed: Expected throughput of producing and consuming snapshots +* Safety: Cost of pushing invalid snapshots to a node +* Liveness: Cost of preventing a node from receiving/constructing a snapshot +* Effort: How much effort does an implementation require + +### Implementation Question +* What is the format of a snapshot + * Complete snapshot + * Ordered IAVL key ranges + * Compressed individually chunks which can be validated +* How is data validated + * Trust a peer with it's data blindly + * Trust a majority of peers + * Use light client validation to validate each chunk against consensus + produced merkle tree root +* What are the performance characteristics + * Random vs sequential reads + * How parallelizeable is the scheduling algorithm + +### Proposals +Broadly speaking there are two approaches to this problem which have had +varying degrees of discussion and progress. These approach can be +summarized as: + +**Lazy:** Where snapshots are produced dynamically at request time. This +solution would use the existing data structure. +**Eager:** Where snapshots are produced periodically and served from disk at +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. + +**LightClient** Where a client can aquire the merkle root from the block +headers synchronized from a trusted validator set. Subsets of the application state, +called chunks can therefore be validated on receipt to ensure each chunk +is part of the merkle root. + +**Majority of Peers** Where manifests of chunks along with checksums are +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. +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 +[some concerns](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit) +about this design, somewhat specific to the IAVL tree, and mainly concerning +performance of random reads and of iterating through the tree to determine element numbers +(ie. elements aren't indexed by the element number). + +An alternative design was suggested by Jae Kwon in +[#3639](https://github.com/tendermint/tendermint/issues/3639) where chunking +happens lazily and in a dynamic way: nodes request key ranges from their peers, +and peers respond with some subset of the +requested range and with notes on how to request the rest in parallel from other +peers. Unlike chunk numbers, keys can be verified directly. And if some keys in the +range are ommitted, proofs for the range will fail to verify. +This way a node can start by requesting the entire tree from one peer, +and that peer can respond with say the first few keys, and the ranges to request +from other peers. + +Additionally, per chunk validation tends to come more naturally to the +Lazy approach since it tends to use the existing structure of the tree +(ie. keys or nodes) rather than state-sync specific chunks. Such a +design for tendermint was originally tracked in +[#828](https://github.com/tendermint/tendermint/issues/828). + +#### Eager StateSync +Warp Sync as implemented in Parity +["Warp Sync"](https://wiki.parity.io/Warp-Sync-Snapshot-Format.html) to rapidly +download both blocks and state snapshots from peers. Data is carved into ~4MB +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. + +A similar solution was implemented by Binance in +[#3594](https://github.com/tendermint/tendermint/pull/3594) +based on their initial implementation in +[PR #3243](https://github.com/tendermint/tendermint/pull/3243) +and [some learnings](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit). +Note this still requires the honest majority peer assumption. + +As an eager protocol, warp-sync can efficiently compress larger, more +predicatable chunks once per snapshot and service many new peers. By +comparison lazy chunkers would have to compress each chunk at request +time. + +### Analysis of Lazy vs Eager +Lazy vs Eager have more in common than they differ. They all require +reactors on the tendermint side, a set of ABCI messages and a method for +serializing/deserializing snapshots facilitated by a SnapshotFormat. + +The biggest difference between Lazy and Eager proposals is in the +read/write patterns necessitated by serving a snapshot chunk. +Specifically, Lazy State Sync performs random reads to the underlying data +structure while Eager can optimize for sequential reads. + +This distinctin between approaches was demonstrated by Binance's +[ackratos](https://github.com/ackratos) in their implementation of [Lazy +State sync](https://github.com/tendermint/tendermint/pull/3243), The +[analysis](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/) +of the performance, and follow up implementation of [Warp +Sync](http://github.com/tendermint/tendermint/pull/3594). + +#### Compairing Security Models +There are several different security models which have been +discussed/proposed in the past but generally fall into two categories. + +Light client validation: In which the node receiving data is expected to +first perform a light client sync and have all the nessesary block +headers. Within the trusted block header (trusted in terms of from a +validator set subject to [weak +subjectivity](https://github.com/tendermint/tendermint/pull/3795)) and +can compare any subset of keys called a chunk against the merkle root. +The advantage of light client validation is that the block headers are +signed by validators which have something to lose for malicious +behaviour. If a validator were to provide an invalid proof, they can be +slashed. + +Majority of peer validation: A manifest file containing a list of chunks +along with checksums of each chunk is downloaded from a +trusted source. That source can be a community resource similar to +[sum.golang.org](https://sum.golang.org) or downloaded from the majority +of peers. One disadantage of the majority of peer security model is the +vuliberability to eclipse attacks in which a malicious users looks to +saturate a target node's peer list and produce a manufactured picture of +majority. + +A third option would be to include snapshot related data in the +block header. This could include the manifest with related checksums and be +secured through consensus. One challenge of this approach is to +ensure that creating snapshots does not put undo burden on block +propsers by synchronizing snapshot creation and block creation. One +approach to minimizing the burden is for snapshots for height +`H` to be included in block `H+n` where `n` is some `n` block away, +giving the block propser enough time to complete the snapshot +asynchronousy. + +## Proposal: Eager StateSync With Per Chunk Light Client Validation +The conclusion after some concideration of the advantages/disadvances of +eager/lazy and different security models is to produce a state sync +which eagerly produces snapshots and uses light client validation. This +approach has the performance advantages of pre-computing efficient +snapshots which can streamed to new nodes on demand using sequential IO. +Secondly, by using light client validation we cna validate each chunk on +receipt and avoid the potential eclipse attack of majority of peer based +security. + +### Implementation +Tendermint is responsible for downloading and verifying chunks of +AppState from peers. ABCI Application is responsible for taking +AppStateChunk objects from TM and constructing a valid state tree whose +root corresponds with the AppHash of syncing block. In particular we +will need implement: + +* Build new StateSync reactor brokers message transmission between the peers + and the ABCI application +* A set of ABCI Messages +* Design SnapshotFormat as an interface which can: + * validate chunks + * read/write chunks from file + * read/write chunks to/from application state store + * convert manifests into chunkRequest ABCI messages +* Implement SnapshotFormat for cosmos-hub with concrete implementation for: + * read/write chunks in a way which can be: + * parallelized across peers + * validated on receipt + * read/write to/from IAVL+ tree + +![StateSync Architecture Diagram](img/state-sync.png) + +## Implementation Path +* Create StateSync reactor based on [#3753](https://github.com/tendermint/tendermint/pull/3753) +* Design SnapshotFormat with an eye towards cosmos-hub implementation +* ABCI message to send/receive SnapshotFormat +* IAVL+ changes to support SnapshotFormat +* Deliver Warp sync (no chunk validation) +* light client implementation for weak subjectivity +* Deliver StateSync with chunk validation + +## Status + +Proposed + +## Concequences + +### Neutral + +### Positive +* Safe & performant state sync design substantiated with real world implementation experience +* General interfaces allowing application specific innovation +* Parallizable implementation trajectory with reasonable engineering effort + +### Negative +* Static Scheduling lacks opportunity for real time chunk availability optimizations + +## 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 +[Warp Sync Implementation](https://github.com/tendermint/tendermint/pull/3594) - ackratos +[Chunk Proposal](https://github.com/tendermint/tendermint/pull/3799) - Bucky proposed + + diff --git a/docs/architecture/adr-043-blockchain-riri-org.md b/docs/architecture/adr-043-blockchain-riri-org.md new file mode 100644 index 000000000..3cdf6e31e --- /dev/null +++ b/docs/architecture/adr-043-blockchain-riri-org.md @@ -0,0 +1,391 @@ +# ADR 043: Blockhchain Reactor Riri-Org + +## Changelog +* 18-06-2019: Initial draft +* 08-07-2019: Reviewed + +## 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`. + +![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 + +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). + +## Decision + +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) + +### Reactor changes in detail + +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) + for { + select { + case <-timer.C: + now := evTimeCheck{time.Now()} + schedulerMsgs <- now + processorMsgs <- now + ioMsgs <- now + case msg:= <- msgs: + msg.time = time.Now() + // These channels should produce backpressure before + // being full to avoid starving each other + schedulerMsgs <- msg + processorMsgs <- msg + ioMesgs <- msg + if msg == stop { + break; + } + } + } +} + +func processRoutine(input chan Message, output chan Message) { + processor := NewProcessor(..) + for { + msg := <- input + switch msg := msg.(type) { + case bcBlockRequestMessage: + output <- processor.handleBlockRequest(msg)) + ... + case stop: + processor.stop() + break; + } +} + +func scheduleRoutine(input chan Message, output chan Message) { + schelduer = NewScheduler(...) + for { + msg := <-msgs + switch msg := input.(type) { + case bcBlockResponseMessage: + output <- scheduler.handleBlockResponse(msg) + ... + case stop: + schedule.stop() + break; + } + } +} +``` + +## Lifecycle management + +A set of routines for individual processes allow processes to run in parallel with clear lifecycle management. `Start`, `Stop`, and `AddPeer` hooks currently present in the reactor will delegate to the sub-routines allowing them to manage internal state independent without further coupling to the reactor. + +```go +func (r *BlockChainReactor) Start() { + r.msgs := make(chan Message, maxInFlight) + schedulerMsgs := make(chan Message) + processorMsgs := make(chan Message) + ioMsgs := make(chan Message) + + go processorRoutine(processorMsgs, r.msgs) + go scheduleRoutine(schedulerMsgs, r.msgs) + go ioRoutine(ioMsgs, r.msgs) + ... +} + +func (bcR *BlockchainReactor) Receive(...) { + ... + r.msgs <- msg + ... +} + +func (r *BlockchainReactor) Stop() { + ... + r.msgs <- stop + ... +} + +... +func (r *BlockchainReactor) Stop() { + ... + r.msgs <- stop + ... +} +... + +func (r *BlockchainReactor) AddPeer(peer p2p.Peer) { + ... + r.msgs <- bcAddPeerEv{peer.ID} + ... +} + +``` + +## 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 +func (r *BlockchainReacor) ioRoutine(ioMesgs chan Message, outMsgs chan Message) { + ... + for { + msg := <-ioMsgs + switch msg := msg.(type) { + case scBlockRequestMessage: + queued := r.sendBlockRequestToPeer(...) + if queued { + outMsgs <- ioSendQueued{...} + } + case scStatusRequestMessage + r.sendStatusRequestToPeer(...) + case bcPeerError + r.Swtich.StopPeerForError(msg.src) + ... + ... + case bcFinished + break; + } + } +} + +``` +### 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. + +```go +type Processor struct { + height int64 // the height cursor + state ... + blocks [height]*Block // keep a set of blocks in memory until they are processed + blockPeers [height]PeerID // keep track of which heights came from which peerID + lastTouch timestamp +} + +func (proc *Processor) handleBlockResponse(peerID, block) { + if block.height <= height || block[block.height] { + } else if blocks[block.height] { + return errDuplicateBlock{} + } else { + blocks[block.height] = block + } + + if blocks[height] && blocks[height+1] { + ... = state.Validators.VerifyCommit(...) + ... = store.SaveBlock(...) + state, err = blockExec.ApplyBlock(...) + ... + if err == nil { + delete blocks[height] + height++ + lastTouch = msg.time + return pcBlockProcessed{height-1} + } else { + ... // Delete all unprocessed block from the peer + return pcBlockProcessError{peerID, height} + } + } +} + +func (proc *Processor) handleRemovePeer(peerID) { + events = [] + // Delete all unprocessed blocks from peerID + for i = height; i < len(blocks); i++ { + if blockPeers[i] == peerID { + events = append(events, pcBlockReschedule{height}) + + delete block[height] + } + } + return events +} + +func handleTimeCheckEv(time) { + if time - lastTouch > timeout { + // Timeout the processor + ... + } +} +``` + +## Schedule + +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 + +```go +type blockState int + +const ( + blockStateNew = iota + blockStatePending, + blockStateReceived, + blockStateProcessed +) + +type schedule { + // a list of blocks in which blockState + blockStates map[height]blockState + + // a map of which blocks are available from which peers + blockPeers map[height]map[p2p.ID]scPeer + + // 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 + + targetPending int // the number of blocks we want in blockStatePending + targetReceived int // the number of blocks we want in blockStateReceived + + peerTimeout int + peerMinSpeed int +} + +func (sc *schedule) numBlockInState(state blockState) uint32 { + num := 0 + for i := sc.minHeight(); i <= sc.maxHeight(); i++ { + if sc.blockState[i] == state { + num++ + } + } + return num +} + + +func (sc *schedule) popSchedule(maxRequest int) []scBlockRequestMessage { + // We only want to schedule requests such that we have less than sc.targetPending and sc.targetReceived + // This ensures we don't saturate the network or flood the processor with unprocessed blocks + todo := min(sc.targetPending - sc.numBlockInState(blockStatePending), sc.numBlockInState(blockStateReceived)) + events := []scBlockRequestMessage{} + for i := sc.minHeight(); i < sc.maxMaxHeight(); i++ { + if todo == 0 { + break + } + if blockStates[i] == blockStateNew { + peer = sc.selectPeer(blockPeers[i]) + sc.blockStates[i] = blockStatePending + sc.pending[i] = peer + events = append(events, scBlockRequestMessage{peerID: peer.peerID, height: i}) + todo-- + } + } + return events +} +... + +type scPeer struct { + peerID p2p.ID + numOustandingRequest int + lastTouched time.Time + monitor flow.Monitor +} + +``` + +# 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 +of scBlockRequestMessage at each `timeCheckEv`. + +``` + +func handleStatusResponse(peerID, height, time) { + schedule.touchPeer(peerID, time) + schedule.setPeerHeight(peerID, height) +} + +func handleBlockResponseMessage(peerID, height, block, time) { + schedule.touchPeer(peerID, time) + schedule.markReceived(peerID, height, size(block)) +} + +func handleNoBlockResponseMessage(peerID, height, time) { + schedule.touchPeer(peerID, time) + // reschedule that block, punish peer... + ... +} + +func handlePeerError(peerID) { + // Remove the peer, reschedule the requests + ... +} + +func handleTimeCheckEv(time) { + // clean peer list + + events = [] + for peerID := range schedule.peersNotTouchedSince(time) { + pending = schedule.pendingFrom(peerID) + schedule.setPeerState(peerID, timedout) + schedule.resetBlocks(pending) + events = append(events, peerTimeout{peerID}) + } + + events = append(events, schedule.popSchedule()) + + return events +} +``` + +## Peer +The Peer Stores per peer state based on messages received by the scheduler. + +```go +type Peer struct { + lastTouched timestamp + lastDownloaded timestamp + pending map[height]struct{} + height height // max height for the peer + state { + pending, // we know the peer but not the height + active, // we know the height + timeout // the peer has timed out + } +} +``` + +## Status + +Work in progress + +## 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 + +### Negative + +### Neutral + +### 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 + +## 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) diff --git a/docs/architecture/img/blockchain-reactor-v1.png b/docs/architecture/img/blockchain-reactor-v1.png new file mode 100644 index 000000000..70debcd66 Binary files /dev/null and b/docs/architecture/img/blockchain-reactor-v1.png differ diff --git a/docs/architecture/img/blockchain-reactor-v2.png b/docs/architecture/img/blockchain-reactor-v2.png new file mode 100644 index 000000000..086bf71bd Binary files /dev/null and b/docs/architecture/img/blockchain-reactor-v2.png differ diff --git a/docs/architecture/img/state-sync.png b/docs/architecture/img/state-sync.png new file mode 100644 index 000000000..08b6eac43 Binary files /dev/null and b/docs/architecture/img/state-sync.png differ diff --git a/docs/guides/go-built-in.md b/docs/guides/go-built-in.md new file mode 100644 index 000000000..705022c90 --- /dev/null +++ b/docs/guides/go-built-in.md @@ -0,0 +1,639 @@ +# Creating a built-in application in Go + +## Guide assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine - written in any programming language - and securely +replicates it on many machines. + +Although Tendermint Core is written in the Golang programming language, prior +knowledge of it is not required for this guide. You can learn it as we go due +to it's simplicity. However, you may want to go through [Learn X in Y minutes +Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize +yourself with the syntax. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. + +## Built-in app vs external app + +Running your application inside the same process as Tendermint Core will give +you the best possible performance. + +For other languages, your application have to communicate with Tendermint Core +through a TCP, Unix domain socket or gRPC. + +## 1.1 Installing Go + +Please refer to [the official guide for installing +Go](https://golang.org/doc/install). + +Verify that you have the latest version of Go installed: + +```sh +$ go version +go version go1.12.7 darwin/amd64 +``` + +Make sure you have `$GOPATH` environment variable set: + +```sh +$ echo $GOPATH +/Users/melekes/go +``` + +## 1.2 Creating a new Go project + +We'll start by creating a new Go project. + +```sh +$ mkdir -p $GOPATH/src/github.com/me/kvstore +$ cd $GOPATH/src/github.com/me/kvstore +``` + +Inside the example directory create a `main.go` file with the following content: + +```go +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, Tendermint Core") +} +``` + +When run, this should print "Hello, Tendermint Core" to the standard output. + +```sh +$ go run main.go +Hello, Tendermint Core +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +Create a file called `app.go` with the following content: + +```go +package main + +import ( + abcitypes "github.com/tendermint/tendermint/abci/types" +) + +type KVStoreApplication struct {} + +var _ abcitypes.Application = (*KVStoreApplication)(nil) + +func NewKVStoreApplication() *KVStoreApplication { + return &KVStoreApplication{} +} + +func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { + return abcitypes.ResponseInfo{} +} + +func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { + return abcitypes.ResponseSetOption{} +} + +func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + return abcitypes.ResponseDeliverTx{Code: 0} +} + +func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + return abcitypes.ResponseCheckTx{Code: 0} +} + +func (KVStoreApplication) Commit() abcitypes.ResponseCommit { + return abcitypes.ResponseCommit{} +} + +func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { + return abcitypes.ResponseQuery{Code: 0} +} + +func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { + return abcitypes.ResponseInitChain{} +} + +func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + return abcitypes.ResponseBeginBlock{} +} + +func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { + return abcitypes.ResponseEndBlock{} +} +``` + +Now I will go through each method explaining when it's called and adding +required business logic. + +### 1.3.1 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```go +func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } + + key, value := parts[0], parts[1] + + // check if the same key=value already exists + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(key) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == nil { + return item.Value(func(val []byte) error { + if bytes.Equal(val, value) { + code = 2 + } + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + + return code +} + +func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + code := app.isValid(req.Tx) + return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[badger](https://github.com/dgraph-io/badger), which is an embeddable, +persistent and fast key-value (KV) database. + +```go +import "github.com/dgraph-io/badger" + +type KVStoreApplication struct { + db *badger.DB + currentBatch *badger.Txn +} + +func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { + return &KVStoreApplication{ + db: db, + } +} +``` + +### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transfered to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +responses are expected to come in order. + +``` +func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + app.currentBatch = app.db.NewTransaction(true) + return abcitypes.ResponseBeginBlock{} +} + +``` + +Here we create a batch, which will store block's transactions. + +```go +func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + code := app.isValid(req.Tx) + if code != 0 { + return abcitypes.ResponseDeliverTx{Code: code} + } + + parts := bytes.Split(req.Tx, []byte("=")) + key, value := parts[0], parts[1] + + err := app.currentBatch.Set(key, value) + if err != nil { + panic(err) + } + + return abcitypes.ResponseDeliverTx{Code: 0} +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the current batch. + +In the current design, a block can include incorrect transactions (those who +passed CheckTx, but failed DeliverTx or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```go +func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { + app.currentBatch.Commit() + return abcitypes.ResponseCommit{Data: []byte{}} +} +``` + +### 1.3.3 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```go +func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { + resQuery.Key = reqQuery.Data + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(reqQuery.Data) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == badger.ErrKeyNotFound { + resQuery.Log = "does not exist" + } else { + return item.Value(func(val []byte) error { + resQuery.Log = "exists" + resQuery.Value = val + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + return +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instance in the same process + +Put the following code into the "main.go" file: + +```go +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/dgraph-io/badger" + "github.com/pkg/errors" + "github.com/spf13/viper" + + abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" + tmflags "github.com/tendermint/tendermint/libs/cli/flags" + "github.com/tendermint/tendermint/libs/log" + nm "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" +) + +var configFile string + +func init() { + flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml") +} + +func main() { + db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) + } + defer db.Close() + app := NewKVStoreApplication(db) + + flag.Parse() + + node, err := newTendermint(app, configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(2) + } + + node.Start() + defer func() { + node.Stop() + node.Wait() + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + os.Exit(0) +} + +func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { + // read config + config := cfg.DefaultConfig() + config.RootDir = filepath.Dir(filepath.Dir(configFile)) + viper.SetConfigFile(configFile) + if err := viper.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "viper failed to read config file") + } + if err := viper.Unmarshal(config); err != nil { + return nil, errors.Wrap(err, "viper failed to unmarshal config") + } + if err := config.ValidateBasic(); err != nil { + return nil, errors.Wrap(err, "config is invalid") + } + + // create logger + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + var err error + logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) + if err != nil { + return nil, errors.Wrap(err, "failed to parse log level") + } + + // read private validator + pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), + ) + + // read node key + nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) + if err != nil { + return nil, errors.Wrap(err, "failed to load node's key") + } + + // create node + node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger) + if err != nil { + return nil, errors.Wrap(err, "failed to create new Tendermint node") + } + + return node, nil +} +``` + +This is a huge blob of code, so let's break it down into pieces. + +First, we initialize the Badger database and create an app instance: + +```go +db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) +if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) +} +defer db.Close() +app := NewKVStoreApplication(db) +``` + +Then we use it to create a Tendermint Core `Node` instance: + +```go +flag.Parse() + +node, err := newTendermint(app, configFile) +if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(2) +} + +... + +// create node +node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger) +if err != nil { + return nil, errors.Wrap(err, "failed to create new Tendermint node") +} +``` + +`NewNode` requires a few things including a configuration file, a private +validator, a node key and a few others in order to construct the full node. + +Note we use `proxy.NewLocalClientCreator` here to create a local client instead +of one communicating through a socket or gRPC. + +[viper](https://github.com/spf13/viper) is being used for reading the config, +which we will generate later using the `tendermint init` command. + +```go +config := cfg.DefaultConfig() +config.RootDir = filepath.Dir(filepath.Dir(configFile)) +viper.SetConfigFile(configFile) +if err := viper.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "viper failed to read config file") +} +if err := viper.Unmarshal(config); err != nil { + return nil, errors.Wrap(err, "viper failed to unmarshal config") +} +if err := config.ValidateBasic(); err != nil { + return nil, errors.Wrap(err, "config is invalid") +} +``` + +We use `FilePV`, which is a private validator (i.e. thing which signs consensus +messages). Normally, you would use `SignerRemote` to connect to an external +[HSM](https://kb.certus.one/hsm.html). + +```go +pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), +) + +``` + +`nodeKey` is needed to identify the node in a p2p network. + +```go +nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) +if err != nil { + return nil, errors.Wrap(err, "failed to load node's key") +} +``` + +As for the logger, we use the build-in library, which provides a nice +abstraction over [go-kit's +logger](https://github.com/go-kit/kit/tree/master/log). + +```go +logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) +var err error +logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) +if err != nil { + return nil, errors.Wrap(err, "failed to parse log level") +} +``` + +Finally, we start the node and add some signal handling to gracefully stop it +upon receiving SIGTERM or Ctrl-C. + +```go +node.Start() +defer func() { + node.Stop() + node.Wait() +}() + +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c +os.Exit(0) +``` + +## 1.5 Getting Up and Running + +We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for +dependency management. + +```sh +$ export GO111MODULE=on +$ go mod init github.com/me/example +$ go build +``` + +This should build the binary. + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +We are ready to start our application: + +```sh +$ ./example -config "/tmp/example/config/config.toml" + +badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s +badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0 +badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s +E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p +I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0 +I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash= +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6", + "height": "128" + } +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +``` +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3M=" + } + } +} +``` + +"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of +"tendermint" and "rocks" accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). diff --git a/docs/guides/go.md b/docs/guides/go.md new file mode 100644 index 000000000..ada84adfc --- /dev/null +++ b/docs/guides/go.md @@ -0,0 +1,523 @@ +# Creating an application in Go + +## Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine - written in any programming language - and securely +replicates it on many machines. + +Although Tendermint Core is written in the Golang programming language, prior +knowledge of it is not required for this guide. You can learn it as we go due +to it's simplicity. However, you may want to go through [Learn X in Y minutes +Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize +yourself with the syntax. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. + +## Built-in app vs external app + +To get maximum performance it is better to run your application alongside +Tendermint Core. [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written +this way. Please refer to [Writing a built-in Tendermint Core application in +Go](./go-built-in.md) guide for details. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Go + +Please refer to [the official guide for installing +Go](https://golang.org/doc/install). + +Verify that you have the latest version of Go installed: + +```sh +$ go version +go version go1.12.7 darwin/amd64 +``` + +Make sure you have `$GOPATH` environment variable set: + +```sh +$ echo $GOPATH +/Users/melekes/go +``` + +## 1.2 Creating a new Go project + +We'll start by creating a new Go project. + +```sh +$ mkdir -p $GOPATH/src/github.com/me/kvstore +$ cd $GOPATH/src/github.com/me/kvstore +``` + +Inside the example directory create a `main.go` file with the following content: + +```go +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, Tendermint Core") +} +``` + +When run, this should print "Hello, Tendermint Core" to the standard output. + +```sh +$ go run main.go +Hello, Tendermint Core +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +Create a file called `app.go` with the following content: + +```go +package main + +import ( + abcitypes "github.com/tendermint/tendermint/abci/types" +) + +type KVStoreApplication struct {} + +var _ abcitypes.Application = (*KVStoreApplication)(nil) + +func NewKVStoreApplication() *KVStoreApplication { + return &KVStoreApplication{} +} + +func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { + return abcitypes.ResponseInfo{} +} + +func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { + return abcitypes.ResponseSetOption{} +} + +func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + return abcitypes.ResponseDeliverTx{Code: 0} +} + +func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + return abcitypes.ResponseCheckTx{Code: 0} +} + +func (KVStoreApplication) Commit() abcitypes.ResponseCommit { + return abcitypes.ResponseCommit{} +} + +func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { + return abcitypes.ResponseQuery{Code: 0} +} + +func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { + return abcitypes.ResponseInitChain{} +} + +func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + return abcitypes.ResponseBeginBlock{} +} + +func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { + return abcitypes.ResponseEndBlock{} +} +``` + +Now I will go through each method explaining when it's called and adding +required business logic. + +### 1.3.1 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```go +func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } + + key, value := parts[0], parts[1] + + // check if the same key=value already exists + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(key) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == nil { + return item.Value(func(val []byte) error { + if bytes.Equal(val, value) { + code = 2 + } + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + + return code +} + +func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + code := app.isValid(req.Tx) + return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[badger](https://github.com/dgraph-io/badger), which is an embeddable, +persistent and fast key-value (KV) database. + +```go +import "github.com/dgraph-io/badger" + +type KVStoreApplication struct { + db *badger.DB + currentBatch *badger.Txn +} + +func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { + return &KVStoreApplication{ + db: db, + } +} +``` + +### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transfered to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +responses are expected to come in order. + +``` +func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + app.currentBatch = app.db.NewTransaction(true) + return abcitypes.ResponseBeginBlock{} +} + +``` + +Here we create a batch, which will store block's transactions. + +```go +func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + code := app.isValid(req.Tx) + if code != 0 { + return abcitypes.ResponseDeliverTx{Code: code} + } + + parts := bytes.Split(req.Tx, []byte("=")) + key, value := parts[0], parts[1] + + err := app.currentBatch.Set(key, value) + if err != nil { + panic(err) + } + + return abcitypes.ResponseDeliverTx{Code: 0} +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the current batch. + +In the current design, a block can include incorrect transactions (those who +passed CheckTx, but failed DeliverTx or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```go +func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { + app.currentBatch.Commit() + return abcitypes.ResponseCommit{Data: []byte{}} +} +``` + +### 1.3.3 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```go +func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { + resQuery.Key = reqQuery.Data + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(reqQuery.Data) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == badger.ErrKeyNotFound { + resQuery.Log = "does not exist" + } else { + return item.Value(func(val []byte) error { + resQuery.Log = "exists" + resQuery.Value = val + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + return +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the "main.go" file: + +```go +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/dgraph-io/badger" + + abciserver "github.com/tendermint/tendermint/abci/server" + "github.com/tendermint/tendermint/libs/log" +) + +var socketAddr string + +func init() { + flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address") +} + +func main() { + db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) + } + defer db.Close() + app := NewKVStoreApplication(db) + + flag.Parse() + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + + server := abciserver.NewSocketServer(socketAddr, app) + server.SetLogger(logger) + if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + os.Exit(1) + } + defer server.Stop() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + os.Exit(0) +} +``` + +This is a huge blob of code, so let's break it down into pieces. + +First, we initialize the Badger database and create an app instance: + +```go +db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) +if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) +} +defer db.Close() +app := NewKVStoreApplication(db) +``` + +Then we start the ABCI server and add some signal handling to gracefully stop +it upon receiving SIGTERM or Ctrl-C. Tendermint Core will act as a client, +which connects to our server and send us transactions and other messages. + +```go +server := abciserver.NewSocketServer(socketAddr, app) +server.SetLogger(logger) +if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + os.Exit(1) +} +defer server.Stop() + +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c +os.Exit(0) +``` + +## 1.5 Getting Up and Running + +We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for +dependency management. + +```sh +$ export GO111MODULE=on +$ go mod init github.com/me/example +$ go build +``` + +This should build the binary. + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +$ rm example.sock +$ ./example + +badger 2019/07/16 18:25:11 INFO: All 0 tables opened in 0s +badger 2019/07/16 18:25:11 INFO: Replaying file id: 0 at offset: 0 +badger 2019/07/16 18:25:11 INFO: Replay took: 300.4s +I[2019-07-16|18:25:11.523] Starting ABCIServer impl=ABCIServ +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --proxy_app=unix://example.sock + +I[2019-07-16|18:26:20.362] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-16|18:26:20.383] Starting Node module=main impl=Node +E[2019-07-16|18:26:20.392] Couldn't connect to any seeds module=p2p +I[2019-07-16|18:26:20.394] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:8dab80770ae8e295d4ce905d86af78c4ff634b79 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-nIO96P Version:0.32.1 Channels:4020212223303800 Moniker:app48.fun-box.ru Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-16|18:26:21.440] Executed block module=state height=1 validTxs=0 invalidTxs=0 +I[2019-07-16|18:26:21.446] Committed state module=state height=1 txs=0 appHash= +``` + +This should start the full node and connect to our ABCI application. + +``` +I[2019-07-16|18:25:11.525] Waiting for new connection... +I[2019-07-16|18:26:20.329] Accepted a new connection +I[2019-07-16|18:26:20.329] Waiting for new connection... +I[2019-07-16|18:26:20.330] Accepted a new connection +I[2019-07-16|18:26:20.330] Waiting for new connection... +I[2019-07-16|18:26:20.330] Accepted a new connection +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +``` +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of +"tendermint" and "rocks" accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). diff --git a/docs/guides/java.md b/docs/guides/java.md new file mode 100644 index 000000000..162b40fd7 --- /dev/null +++ b/docs/guides/java.md @@ -0,0 +1,600 @@ +# Creating an application in Java + +## Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine (your application) - written in any programming language - and securely +replicates it on many machines. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. The application (which should +implementing the blockchain interface (ABCI)) will be written in Java. + +This guide assumes that you are not new to JVM world. If you are new please see [JVM Minimal Survival Guide](https://hadihariri.com/2013/12/29/jvm-minimal-survival-guide-for-the-dotnet-developer/#java-the-language-java-the-ecosystem-java-the-jvm) and [Gradle Docs](https://docs.gradle.org/current/userguide/userguide.html). + +## Built-in app vs external app + +If you use Golang, you can run your app and Tendermint Core in the same process to get maximum performance. +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way. +Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details. + +If you choose another language, like we did in this guide, you have to write a separate app, +which will communicate with Tendermint Core via a socket (UNIX or TCP) or gRPC. +This guide will show you how to build external application using RPC server. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Java and Gradle + +Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html). + +Verify that you have installed Java successfully: + +```sh +$ java -version +java version "12.0.2" 2019-07-16 +Java(TM) SE Runtime Environment (build 12.0.2+10) +Java HotSpot(TM) 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing) +``` + +You can choose any version of Java higher or equal to 8. +This guide is written using Java SE Development Kit 12. + +Make sure you have `$JAVA_HOME` environment variable set: + +```sh +$ echo $JAVA_HOME +/Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home +``` + +For Gradle installation, please refer to [their official guide](https://gradle.org/install/). + +## 1.2 Creating a new Java project + +We'll start by creating a new Gradle project. + +```sh +$ export KVSTORE_HOME=~/kvstore +$ mkdir $KVSTORE_HOME +$ cd $KVSTORE_HOME +``` + +Inside the example directory run: +```sh +gradle init --dsl groovy --package io.example --project-name example --type java-application --test-framework junit +``` +This will create a new project for you. The tree of files should look like: +```sh +$ tree +. +|-- build.gradle +|-- gradle +| `-- wrapper +| |-- gradle-wrapper.jar +| `-- gradle-wrapper.properties +|-- gradlew +|-- gradlew.bat +|-- settings.gradle +`-- src + |-- main + | |-- java + | | `-- io + | | `-- example + | | `-- App.java + | `-- resources + `-- test + |-- java + | `-- io + | `-- example + | `-- AppTest.java + `-- resources +``` + +When run, this should print "Hello world." to the standard output. + +```sh +$ ./gradlew run +> Task :run +Hello world. +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +### 1.3.1 Compile .proto files + +Add the following piece to the top of the `build.gradle`: +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' + } +} +``` + +Enable the protobuf plugin in the `plugins` section of the `build.gradle`: +```groovy +plugins { + id 'com.google.protobuf' version '0.8.8' +} +``` + +Add the following code to `build.gradle`: +```groovy +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.7.1" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.22.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} +``` + +Now we should be ready to compile the `*.proto` files. + + +Copy the necessary `.proto` files to your project: +```sh +mkdir -p \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto + +cp $GOPATH/src/github.com/tendermint/tendermint/abci/types/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types/types.proto +cp $GOPATH/src/github.com/tendermint/tendermint/crypto/merkle/merkle.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle/merkle.proto +cp $GOPATH/src/github.com/tendermint/tendermint/libs/common/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common/types.proto +cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto +``` + +Add these dependencies to `build.gradle`: +```groovy +dependencies { + implementation 'io.grpc:grpc-protobuf:1.22.1' + implementation 'io.grpc:grpc-netty-shaded:1.22.1' + implementation 'io.grpc:grpc-stub:1.22.1' +} +``` + +To generate all protobuf-type classes run: +```sh +./gradlew generateProto +``` +To verify that everything went smoothly, you can inspect the `build/generated/` directory: +```sh +$ tree build/generated/ +build/generated/ +|-- source +| `-- proto +| `-- main +| |-- grpc +| | `-- types +| | `-- ABCIApplicationGrpc.java +| `-- java +| |-- com +| | `-- google +| | `-- protobuf +| | `-- GoGoProtos.java +| |-- common +| | `-- Types.java +| |-- merkle +| | `-- Merkle.java +| `-- types +| `-- Types.java +``` + +### 1.3.2 Implementing ABCI + +The resulting `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java` file +contains the abstract class `ABCIApplicationImplBase`, which is an interface we'll need to implement. + +Create `$KVSTORE_HOME/src/main/java/io/example/KVStoreApp.java` file with the following content: +```java +package io.example; + +import io.grpc.stub.StreamObserver; +import types.ABCIApplicationGrpc; +import types.Types.*; + +class KVStoreApp extends ABCIApplicationGrpc.ABCIApplicationImplBase { + + // methods implementation + +} +``` + +Now I will go through each method of `ABCIApplicationImplBase` explaining when it's called and adding +required business logic. + +### 1.3.3 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```java +@Override +public void checkTx(RequestCheckTx req, StreamObserver responseObserver) { + var tx = req.getTx(); + int code = validate(tx); + var resp = ResponseCheckTx.newBuilder() + .setCode(code) + .setGasWanted(1) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} + +private int validate(ByteString tx) { + List parts = split(tx, '='); + if (parts.size() != 2) { + return 1; + } + byte[] key = parts.get(0); + byte[] value = parts.get(1); + + // check if the same key=value already exists + var stored = getPersistedValue(key); + if (stored != null && Arrays.equals(stored, value)) { + return 2; + } + + return 0; +} + +private List split(ByteString tx, char separator) { + var arr = tx.toByteArray(); + int i; + for (i = 0; i < tx.size(); i++) { + if (arr[i] == (byte)separator) { + break; + } + } + if (i == tx.size()) { + return Collections.emptyList(); + } + return List.of( + tx.substring(0, i).toByteArray(), + tx.substring(i + 1).toByteArray() + ); +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[JetBrains Xodus](https://github.com/JetBrains/xodus), which is a transactional schema-less embedded high-performance database written in Java. + +`build.gradle`: +```groovy +dependencies { + implementation 'org.jetbrains.xodus:xodus-environment:1.3.91' +} +``` + +```java +... +import jetbrains.exodus.ArrayByteIterable; +import jetbrains.exodus.ByteIterable; +import jetbrains.exodus.env.Environment; +import jetbrains.exodus.env.Store; +import jetbrains.exodus.env.StoreConfig; +import jetbrains.exodus.env.Transaction; + +class KVStoreApp extends ABCIApplicationGrpc.ABCIApplicationImplBase { + private Environment env; + private Transaction txn = null; + private Store store = null; + + KVStoreApp(Environment env) { + this.env = env; + } + + ... + + private byte[] getPersistedValue(byte[] k) { + return env.computeInReadonlyTransaction(txn -> { + var store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn); + ByteIterable byteIterable = store.get(txn, new ArrayByteIterable(k)); + if (byteIterable == null) { + return null; + } + return byteIterable.getBytesUnsafe(); + }); + } +} +``` + +### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transferred to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. `DeliverTx` are being transferred asynchronously, but the +responses are expected to come in order. + +```java +@Override +public void beginBlock(RequestBeginBlock req, StreamObserver responseObserver) { + txn = env.beginTransaction(); + store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn); + var resp = ResponseBeginBlock.newBuilder().build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` +Here we begin a new transaction, which will accumulate the block's transactions and open the corresponding store. + +```java +@Override +public void deliverTx(RequestDeliverTx req, StreamObserver responseObserver) { + var tx = req.getTx(); + int code = validate(tx); + if (code == 0) { + List parts = split(tx, '='); + var key = new ArrayByteIterable(parts.get(0)); + var value = new ArrayByteIterable(parts.get(1)); + store.put(txn, key, value); + } + var resp = ResponseDeliverTx.newBuilder() + .setCode(code) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the store. + +In the current design, a block can include incorrect transactions (those who +passed `CheckTx`, but failed `DeliverTx` or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```java +@Override +public void commit(RequestCommit req, StreamObserver responseObserver) { + txn.commit(); + var resp = ResponseCommit.newBuilder() + .setData(ByteString.copyFrom(new byte[8])) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` + +### 1.3.5 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```java +@Override +public void query(RequestQuery req, StreamObserver responseObserver) { + var k = req.getData().toByteArray(); + var v = getPersistedValue(k); + var builder = ResponseQuery.newBuilder(); + if (v == null) { + builder.setLog("does not exist"); + } else { + builder.setLog("exists"); + builder.setKey(ByteString.copyFrom(k)); + builder.setValue(ByteString.copyFrom(v)); + } + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the `$KVSTORE_HOME/src/main/java/io/example/App.java` file: + +```java +package io.example; + +import jetbrains.exodus.env.Environment; +import jetbrains.exodus.env.Environments; + +import java.io.IOException; + +public class App { + public static void main(String[] args) throws IOException, InterruptedException { + try (Environment env = Environments.newInstance("tmp/storage")) { + var app = new KVStoreApp(env); + var server = new GrpcServer(app, 26658); + server.start(); + server.blockUntilShutdown(); + } + } +} +``` + +It is the entry point of the application. +Here we create a special object `Environment`, which knows where to store the application state. +Then we create and start the gRPC server to handle Tendermint Core requests. + +Create the `$KVSTORE_HOME/src/main/java/io/example/GrpcServer.java` file with the following content: +```java +package io.example; + +import io.grpc.BindableService; +import io.grpc.Server; +import io.grpc.ServerBuilder; + +import java.io.IOException; + +class GrpcServer { + private Server server; + + GrpcServer(BindableService service, int port) { + this.server = ServerBuilder.forPort(port) + .addService(service) + .build(); + } + + void start() throws IOException { + server.start(); + System.out.println("gRPC server started, listening on $port"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("shutting down gRPC server since JVM is shutting down"); + GrpcServer.this.stop(); + System.out.println("server shut down"); + })); + } + + private void stop() { + server.shutdown(); + } + + /** + * Await termination on the main thread since the grpc library uses daemon threads. + */ + void blockUntilShutdown() throws InterruptedException { + server.awaitTermination(); + } +} +``` + +## 1.5 Getting Up and Running + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +./gradlew run + +gRPC server started, listening on 26658 +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --abci grpc --proxy_app tcp://127.0.0.1:26658 + +I[2019-07-28|15:44:53.632] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-28|15:44:53.677] Starting Node module=main impl=Node +I[2019-07-28|15:44:53.681] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:7639e2841ccd47d5ae0f5aad3011b14049d3f452 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-Nhl3zk Version:0.32.1 Channels:4020212223303800 Moniker:Ivans-MacBook-Pro.local Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-28|15:44:54.801] Executed block module=state height=8 validTxs=0 invalidTxs=0 +I[2019-07-28|15:44:54.814] Committed state module=state height=8 txs=0 appHash=0000000000000000 +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +```sh +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of `tendermint` and `rocks` accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). + +The full source code of this example project can be found [here](https://github.com/climber73/tendermint-abci-grpc-java). diff --git a/docs/guides/kotlin.md b/docs/guides/kotlin.md new file mode 100644 index 000000000..fa9e10b35 --- /dev/null +++ b/docs/guides/kotlin.md @@ -0,0 +1,574 @@ +# Creating an application in Kotlin + +## Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine (your application) - written in any programming language - and securely +replicates it on many machines. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. The application (which should +implementing the blockchain interface (ABCI)) will be written in Kotlin. + +This guide assumes that you are not new to JVM world. If you are new please see [JVM Minimal Survival Guide](https://hadihariri.com/2013/12/29/jvm-minimal-survival-guide-for-the-dotnet-developer/#java-the-language-java-the-ecosystem-java-the-jvm) and [Gradle Docs](https://docs.gradle.org/current/userguide/userguide.html). + +## Built-in app vs external app + +If you use Golang, you can run your app and Tendermint Core in the same process to get maximum performance. +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way. +Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details. + +If you choose another language, like we did in this guide, you have to write a separate app, +which will communicate with Tendermint Core via a socket (UNIX or TCP) or gRPC. +This guide will show you how to build external application using RPC server. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Java and Gradle + +Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html). + +Verify that you have installed Java successfully: + +```sh +$ java -version +java version "1.8.0_162" +Java(TM) SE Runtime Environment (build 1.8.0_162-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) +``` + +You can choose any version of Java higher or equal to 8. +In my case it is Java SE Development Kit 8. + +Make sure you have `$JAVA_HOME` environment variable set: + +```sh +$ echo $JAVA_HOME +/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home +``` + +For Gradle installation, please refer to [their official guide](https://gradle.org/install/). + +## 1.2 Creating a new Kotlin project + +We'll start by creating a new Gradle project. + +```sh +$ export KVSTORE_HOME=~/kvstore +$ mkdir $KVSTORE_HOME +$ cd $KVSTORE_HOME +``` + +Inside the example directory run: +```sh +gradle init --dsl groovy --package io.example --project-name example --type kotlin-application +``` +This will create a new project for you. The tree of files should look like: +```sh +$ tree +. +|-- build.gradle +|-- gradle +| `-- wrapper +| |-- gradle-wrapper.jar +| `-- gradle-wrapper.properties +|-- gradlew +|-- gradlew.bat +|-- settings.gradle +`-- src + |-- main + | |-- kotlin + | | `-- io + | | `-- example + | | `-- App.kt + | `-- resources + `-- test + |-- kotlin + | `-- io + | `-- example + | `-- AppTest.kt + `-- resources +``` + +When run, this should print "Hello world." to the standard output. + +```sh +$ ./gradlew run +> Task :run +Hello world. +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +### 1.3.1 Compile .proto files + +Add the following piece to the top of the `build.gradle`: +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' + } +} +``` + +Enable the protobuf plugin in the `plugins` section of the `build.gradle`: +```groovy +plugins { + id 'com.google.protobuf' version '0.8.8' +} +``` + +Add the following code to `build.gradle`: +```groovy +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.7.1" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.22.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} +``` + +Now we should be ready to compile the `*.proto` files. + + +Copy the necessary `.proto` files to your project: +```sh +mkdir -p \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto + +cp $GOPATH/src/github.com/tendermint/tendermint/abci/types/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types/types.proto +cp $GOPATH/src/github.com/tendermint/tendermint/crypto/merkle/merkle.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle/merkle.proto +cp $GOPATH/src/github.com/tendermint/tendermint/libs/common/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common/types.proto +cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto +``` + +Add these dependencies to `build.gradle`: +```groovy +dependencies { + implementation 'io.grpc:grpc-protobuf:1.22.1' + implementation 'io.grpc:grpc-netty-shaded:1.22.1' + implementation 'io.grpc:grpc-stub:1.22.1' +} +``` + +To generate all protobuf-type classes run: +```sh +./gradlew generateProto +``` +To verify that everything went smoothly, you can inspect the `build/generated/` directory: +```sh +$ tree build/generated/ +build/generated/ +`-- source + `-- proto + `-- main + |-- grpc + | `-- types + | `-- ABCIApplicationGrpc.java + `-- java + |-- com + | `-- google + | `-- protobuf + | `-- GoGoProtos.java + |-- common + | `-- Types.java + |-- merkle + | `-- Merkle.java + `-- types + `-- Types.java +``` + +### 1.3.2 Implementing ABCI + +The resulting `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java` file +contains the abstract class `ABCIApplicationImplBase`, which is an interface we'll need to implement. + +Create `$KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt` file with the following content: +```kotlin +package io.example + +import io.grpc.stub.StreamObserver +import types.ABCIApplicationGrpc +import types.Types.* + +class KVStoreApp : ABCIApplicationGrpc.ABCIApplicationImplBase() { + + // methods implementation + +} +``` + +Now I will go through each method of `ABCIApplicationImplBase` explaining when it's called and adding +required business logic. + +### 1.3.3 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```kotlin +override fun checkTx(req: RequestCheckTx, responseObserver: StreamObserver) { + val code = req.tx.validate() + val resp = ResponseCheckTx.newBuilder() + .setCode(code) + .setGasWanted(1) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} + +private fun ByteString.validate(): Int { + val parts = this.split('=') + if (parts.size != 2) { + return 1 + } + val key = parts[0] + val value = parts[1] + + // check if the same key=value already exists + val stored = getPersistedValue(key) + if (stored != null && stored.contentEquals(value)) { + return 2 + } + + return 0 +} + +private fun ByteString.split(separator: Char): List { + val arr = this.toByteArray() + val i = (0 until this.size()).firstOrNull { arr[it] == separator.toByte() } + ?: return emptyList() + return listOf( + this.substring(0, i).toByteArray(), + this.substring(i + 1).toByteArray() + ) +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[JetBrains Xodus](https://github.com/JetBrains/xodus), which is a transactional schema-less embedded high-performance database written in Java. + +`build.gradle`: +```groovy +dependencies { + implementation 'org.jetbrains.xodus:xodus-environment:1.3.91' +} +``` + +```kotlin +... +import jetbrains.exodus.ArrayByteIterable +import jetbrains.exodus.env.Environment +import jetbrains.exodus.env.Store +import jetbrains.exodus.env.StoreConfig +import jetbrains.exodus.env.Transaction + +class KVStoreApp( + private val env: Environment +) : ABCIApplicationGrpc.ABCIApplicationImplBase() { + + private var txn: Transaction? = null + private var store: Store? = null + + ... + + private fun getPersistedValue(k: ByteArray): ByteArray? { + return env.computeInReadonlyTransaction { txn -> + val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn) + store.get(txn, ArrayByteIterable(k))?.bytesUnsafe + } + } +} +``` + +### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transferred to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. `DeliverTx` are being transferred asynchronously, but the +responses are expected to come in order. + +```kotlin +override fun beginBlock(req: RequestBeginBlock, responseObserver: StreamObserver) { + txn = env.beginTransaction() + store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn!!) + val resp = ResponseBeginBlock.newBuilder().build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` +Here we begin a new transaction, which will accumulate the block's transactions and open the corresponding store. + +```kotlin +override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver) { + val code = req.tx.validate() + if (code == 0) { + val parts = req.tx.split('=') + val key = ArrayByteIterable(parts[0]) + val value = ArrayByteIterable(parts[1]) + store!!.put(txn!!, key, value) + } + val resp = ResponseDeliverTx.newBuilder() + .setCode(code) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the store. + +In the current design, a block can include incorrect transactions (those who +passed `CheckTx`, but failed `DeliverTx` or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```kotlin +override fun commit(req: RequestCommit, responseObserver: StreamObserver) { + txn!!.commit() + val resp = ResponseCommit.newBuilder() + .setData(ByteString.copyFrom(ByteArray(8))) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` + +### 1.3.5 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```kotlin +override fun query(req: RequestQuery, responseObserver: StreamObserver) { + val k = req.data.toByteArray() + val v = getPersistedValue(k) + val builder = ResponseQuery.newBuilder() + if (v == null) { + builder.log = "does not exist" + } else { + builder.log = "exists" + builder.key = ByteString.copyFrom(k) + builder.value = ByteString.copyFrom(v) + } + responseObserver.onNext(builder.build()) + responseObserver.onCompleted() +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the `$KVSTORE_HOME/src/main/kotlin/io/example/App.kt` file: + +```kotlin +package io.example + +import jetbrains.exodus.env.Environments + +fun main() { + Environments.newInstance("tmp/storage").use { env -> + val app = KVStoreApp(env) + val server = GrpcServer(app, 26658) + server.start() + server.blockUntilShutdown() + } +} +``` + +It is the entry point of the application. +Here we create a special object `Environment`, which knows where to store the application state. +Then we create and start the gRPC server to handle Tendermint Core requests. + +Create `$KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt` file with the following content: +```kotlin +package io.example + +import io.grpc.BindableService +import io.grpc.ServerBuilder + +class GrpcServer( + private val service: BindableService, + private val port: Int +) { + private val server = ServerBuilder + .forPort(port) + .addService(service) + .build() + + fun start() { + server.start() + println("gRPC server started, listening on $port") + Runtime.getRuntime().addShutdownHook(object : Thread() { + override fun run() { + println("shutting down gRPC server since JVM is shutting down") + this@GrpcServer.stop() + println("server shut down") + } + }) + } + + fun stop() { + server.shutdown() + } + + /** + * Await termination on the main thread since the grpc library uses daemon threads. + */ + fun blockUntilShutdown() { + server.awaitTermination() + } + +} +``` + +## 1.5 Getting Up and Running + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +./gradlew run + +gRPC server started, listening on 26658 +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --abci grpc --proxy_app tcp://127.0.0.1:26658 + +I[2019-07-28|15:44:53.632] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-28|15:44:53.677] Starting Node module=main impl=Node +I[2019-07-28|15:44:53.681] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:7639e2841ccd47d5ae0f5aad3011b14049d3f452 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-Nhl3zk Version:0.32.1 Channels:4020212223303800 Moniker:Ivans-MacBook-Pro.local Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-28|15:44:54.801] Executed block module=state height=8 validTxs=0 invalidTxs=0 +I[2019-07-28|15:44:54.814] Committed state module=state height=8 txs=0 appHash=0000000000000000 +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +```sh +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of `tendermint` and `rocks` accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). + +The full source code of this example project can be found [here](https://github.com/climber73/tendermint-abci-grpc-kotlin). diff --git a/docs/introduction/install.md b/docs/introduction/install.md index 00e04fa0b..0a013bed1 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -1,9 +1,9 @@ # Install Tendermint The fastest and easiest way to install the `tendermint` binary -is to run [this script](https://github.com/tendermint/tendermint/blob/develop/scripts/install/install_tendermint_ubuntu.sh) on +is to run [this script](https://github.com/tendermint/tendermint/blob/master/scripts/install/install_tendermint_ubuntu.sh) on a fresh Ubuntu instance, -or [this script](https://github.com/tendermint/tendermint/blob/develop/scripts/install/install_tendermint_bsd.sh) +or [this script](https://github.com/tendermint/tendermint/blob/master/scripts/install/install_tendermint_bsd.sh) on a fresh FreeBSD instance. Read the comments / instructions carefully (i.e., reset your terminal after running the script, make sure you are okay with the network connections being made). diff --git a/docs/introduction/introduction.md b/docs/introduction/introduction.md index f80a159ca..4f435bbf5 100644 --- a/docs/introduction/introduction.md +++ b/docs/introduction/introduction.md @@ -122,7 +122,7 @@ consensus engine, and provides a particular application state. ## ABCI Overview The [Application BlockChain Interface -(ABCI)](https://github.com/tendermint/tendermint/tree/develop/abci) +(ABCI)](https://github.com/tendermint/tendermint/tree/master/abci) allows for Byzantine Fault Tolerant replication of applications written in any programming language. @@ -190,7 +190,7 @@ core to the application. The application replies with corresponding response messages. The messages are specified here: [ABCI Message -Types](https://github.com/tendermint/tendermint/blob/develop/abci/README.md#message-types). +Types](https://github.com/tendermint/tendermint/blob/master/abci/README.md#message-types). The **DeliverTx** message is the work horse of the application. Each transaction in the blockchain is delivered with this message. The diff --git a/docs/introduction/what-is-tendermint.md b/docs/introduction/what-is-tendermint.md index a35dd9ec1..0371afc63 100644 --- a/docs/introduction/what-is-tendermint.md +++ b/docs/introduction/what-is-tendermint.md @@ -116,7 +116,7 @@ consensus engine, and provides a particular application state. ## ABCI Overview The [Application BlockChain Interface -(ABCI)](https://github.com/tendermint/tendermint/tree/develop/abci) +(ABCI)](https://github.com/tendermint/tendermint/tree/master/abci) allows for Byzantine Fault Tolerant replication of applications written in any programming language. @@ -184,7 +184,7 @@ core to the application. The application replies with corresponding response messages. The messages are specified here: [ABCI Message -Types](https://github.com/tendermint/tendermint/blob/develop/abci/README.md#message-types). +Types](https://github.com/tendermint/tendermint/blob/master/abci/README.md#message-types). The **DeliverTx** message is the work horse of the application. Each transaction in the blockchain is delivered with this message. The diff --git a/docs/networks/docker-compose.md b/docs/networks/docker-compose.md index 8db49af5e..37b53fafe 100644 --- a/docs/networks/docker-compose.md +++ b/docs/networks/docker-compose.md @@ -78,9 +78,9 @@ cd $GOPATH/src/github.com/tendermint/tendermint rm -rf ./build/node* ``` -## Configuring abci containers +## Configuring abci containers -To use your own abci applications with 4-node setup edit the [docker-compose.yaml](https://github.com/tendermint/tendermint/blob/develop/docker-compose.yml) file and add image to your abci application. +To use your own abci applications with 4-node setup edit the [docker-compose.yaml](https://github.com/tendermint/tendermint/blob/master/docker-compose.yml) file and add image to your abci application. ``` abci0: @@ -129,7 +129,7 @@ To use your own abci applications with 4-node setup edit the [docker-compose.yam ``` -Override the [command](https://github.com/tendermint/tendermint/blob/master/networks/local/localnode/Dockerfile#L12) in each node to connect to it's abci. +Override the [command](https://github.com/tendermint/tendermint/blob/master/networks/local/localnode/Dockerfile#L12) in each node to connect to it's abci. ``` node0: diff --git a/docs/networks/terraform-and-ansible.md b/docs/networks/terraform-and-ansible.md index 122591be0..3ef6056a0 100644 --- a/docs/networks/terraform-and-ansible.md +++ b/docs/networks/terraform-and-ansible.md @@ -8,7 +8,7 @@ testnets on those servers. ## Install NOTE: see the [integration bash -script](https://github.com/tendermint/tendermint/blob/develop/networks/remote/integration.sh) +script](https://github.com/tendermint/tendermint/blob/master/networks/remote/integration.sh) that can be run on a fresh DO droplet and will automatically spin up a 4 node testnet. The script more or less does everything described below. diff --git a/docs/spec/abci/README.md b/docs/spec/abci/README.md index bb1c38b6e..56d5e8aaf 100644 --- a/docs/spec/abci/README.md +++ b/docs/spec/abci/README.md @@ -2,11 +2,11 @@ ABCI is the interface between Tendermint (a state-machine replication engine) and your application (the actual state machine). It consists of a set of -*methods*, where each method has a corresponding `Request` and `Response` +_methods_, where each method has a corresponding `Request` and `Response` message type. Tendermint calls the ABCI methods on the ABCI application by sending the `Request*` messages and receiving the `Response*` messages in return. -All message types are defined in a [protobuf file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +All message types are defined in a [protobuf file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). This allows Tendermint to run applications written in any programming language. This specification is split as follows: diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index abab7f548..275c4dcd8 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -3,9 +3,9 @@ ## Overview The ABCI message types are defined in a [protobuf -file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). -ABCI methods are split across 3 separate ABCI *connections*: +ABCI methods are split across 3 separate ABCI _connections_: - `Consensus Connection`: `InitChain, BeginBlock, DeliverTx, EndBlock, Commit` - `Mempool Connection`: `CheckTx` @@ -85,7 +85,7 @@ Example: cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, cmn.KVPair{Key: []byte("reason"), Value: []byte("...")}, }, - }, + }, // ... }, } @@ -115,19 +115,19 @@ non-determinism must be fixed and the nodes restarted. Sources of non-determinism in applications may include: - Hardware failures - - Cosmic rays, overheating, etc. + - Cosmic rays, overheating, etc. - Node-dependent state - - Random numbers - - Time + - Random numbers + - Time - Underspecification - - Library version changes - - Race conditions - - Floating point numbers - - JSON serialization - - Iterating through hash-tables/maps/dictionaries + - Library version changes + - Race conditions + - Floating point numbers + - JSON serialization + - Iterating through hash-tables/maps/dictionaries - External Sources - - Filesystem - - Network calls (eg. some external REST API service) + - Filesystem + - Network calls (eg. some external REST API service) See [#56](https://github.com/tendermint/abci/issues/56) for original discussion. @@ -240,9 +240,9 @@ Commit are included in the header of the next block. - `Path (string)`: Path of request, like an HTTP GET path. Can be used with or in liue of Data. - Apps MUST interpret '/store' as a query by key on the - underlying store. The key SHOULD be specified in the Data field. + underlying store. The key SHOULD be specified in the Data field. - Apps SHOULD allow queries over specific types like - '/accounts/...' or '/votes/...' + '/accounts/...' or '/votes/...' - `Height (int64)`: The block height for which you want the query (default=0 returns data for the latest committed block). Note that this is the height of the block containing the @@ -269,7 +269,7 @@ Commit are included in the header of the next block. - Query for data from the application at current or past height. - Optionally return Merkle proof. - Merkle proof includes self-describing `type` field to support many types - of Merkle trees and encoding formats. + of Merkle trees and encoding formats. ### BeginBlock @@ -297,11 +297,9 @@ Commit are included in the header of the next block. - **Request**: - `Tx ([]byte)`: The request transaction bytes - `Type (CheckTxType)`: What type of `CheckTx` request is this? At present, - there are two possible values: `CheckTx_Unchecked` (the default, which says - that a full check is required), and `CheckTx_Checked` (when the mempool is + there are two possible values: `CheckTx_New` (the default, which says + that a full check is required), and `CheckTx_Recheck` (when the mempool is initiating a normal recheck of a transaction). - - `AdditionalData ([]byte)`: Reserved for future use. See - [here](https://github.com/tendermint/tendermint/issues/2127#issuecomment-456661420). - **Response**: - `Code (uint32)`: Response code - `Data ([]byte)`: Result bytes, if any. @@ -486,7 +484,7 @@ Commit are included in the header of the next block. - `Votes ([]VoteInfo)`: List of validators addresses in the last validator set with their voting power and whether or not they signed a vote. -### ConsensusParams +### ConsensusParams - **Fields**: - `Block (BlockParams)`: Parameters limiting the size of a block and time between consecutive blocks. @@ -500,17 +498,17 @@ Commit are included in the header of the next block. - `MaxBytes (int64)`: Max size of a block, in bytes. - `MaxGas (int64)`: Max sum of `GasWanted` in a proposed block. - NOTE: blocks that violate this may be committed if there are Byzantine proposers. - It's the application's responsibility to handle this when processing a - block! + It's the application's responsibility to handle this when processing a + block! ### EvidenceParams - **Fields**: - `MaxAge (int64)`: Max age of evidence, in blocks. Evidence older than this is considered stale and ignored. - - This should correspond with an app's "unbonding period" or other - similar mechanism for handling Nothing-At-Stake attacks. - - NOTE: this should change to time (instead of blocks)! + - This should correspond with an app's "unbonding period" or other + similar mechanism for handling Nothing-At-Stake attacks. + - NOTE: this should change to time (instead of blocks)! ### ValidatorParams @@ -532,4 +530,3 @@ Commit are included in the header of the next block. - `Type (string)`: Type of Merkle proof and how it's encoded. - `Key ([]byte)`: Key in the Merkle tree that this proof is for. - `Data ([]byte)`: Encoded Merkle proof for the key. - diff --git a/docs/spec/abci/apps.md b/docs/spec/abci/apps.md index 908ad3eaf..5d4a678d4 100644 --- a/docs/spec/abci/apps.md +++ b/docs/spec/abci/apps.md @@ -65,7 +65,10 @@ begin. After `Commit`, CheckTx is run again on all transactions that remain in the node's local mempool after filtering those included in the block. To prevent the mempool from rechecking all transactions every time a block is committed, set -the configuration option `mempool.recheck=false`. +the configuration option `mempool.recheck=false`. As of Tendermint v0.32.1, +an additional `Type` parameter is made available to the CheckTx function that +indicates whether an incoming transaction is new (`CheckTxType_New`), or a +recheck (`CheckTxType_Recheck`). Finally, the mempool will unlock and new transactions can be processed through CheckTx again. diff --git a/docs/spec/abci/client-server.md b/docs/spec/abci/client-server.md index 5ac7b3eb4..94485f0d9 100644 --- a/docs/spec/abci/client-server.md +++ b/docs/spec/abci/client-server.md @@ -9,7 +9,7 @@ Applications](./apps.md). ## Message Protocol The message protocol consists of pairs of requests and responses defined in the -[protobuf file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +[protobuf file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). Some messages have no fields, while others may include byte-arrays, strings, integers, or custom protobuf types. @@ -33,9 +33,9 @@ The latter two can be tested using the `abci-cli` by setting the `--abci` flag appropriately (ie. to `socket` or `grpc`). See examples, in various stages of maintenance, in -[Go](https://github.com/tendermint/tendermint/tree/develop/abci/server), +[Go](https://github.com/tendermint/tendermint/tree/master/abci/server), [JavaScript](https://github.com/tendermint/js-abci), -[Python](https://github.com/tendermint/tendermint/tree/develop/abci/example/python3/abci), +[Python](https://github.com/tendermint/tendermint/tree/master/abci/example/python3/abci), [C++](https://github.com/mdyring/cpp-tmsp), and [Java](https://github.com/jTendermint/jabci). @@ -44,14 +44,13 @@ See examples, in various stages of maintenance, in The simplest implementation uses function calls within Golang. This means ABCI applications written in Golang can be compiled with TendermintCore and run as a single binary. - ### GRPC If GRPC is available in your language, this is the easiest approach, though it will have significant performance overhead. To get started with GRPC, copy in the [protobuf -file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto) +file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto) and compile it using the GRPC plugin for your language. For instance, for golang, the command is `protoc --go_out=plugins=grpc:. types.proto`. See the [grpc documentation for more details](http://www.grpc.io/docs/). @@ -107,4 +106,4 @@ received or a block is committed. It is unlikely that you will need to implement a client. For details of our client, see -[here](https://github.com/tendermint/tendermint/tree/develop/abci/client). +[here](https://github.com/tendermint/tendermint/tree/master/abci/client). diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index 14d0e786b..170e91605 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -59,20 +59,20 @@ familiar with amino encoding. You can simply use below table and concatenate Prefix || Length (of raw bytes) || raw bytes ( while || stands for byte concatenation here). -| Type | Name | Prefix | Length | Notes | -| ------------------ | ----------------------------- | ---------- | -------- | ----- | -| PubKeyEd25519 | tendermint/PubKeyEd25519 | 0x1624DE64 | 0x20 | | -| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | | -| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | | -| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | | -| PubKeyMultisigThreshold | tendermint/PubKeyMultisigThreshold | 0x22C1F7E2 | variable | | +| Type | Name | Prefix | Length | Notes | +| ----------------------- | ---------------------------------- | ---------- | -------- | ----- | +| PubKeyEd25519 | tendermint/PubKeyEd25519 | 0x1624DE64 | 0x20 | | +| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | | +| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | | +| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | | +| PubKeyMultisigThreshold | tendermint/PubKeyMultisigThreshold | 0x22C1F7E2 | variable | | ### Example For example, the 33-byte (or 0x21-byte in hex) Secp256k1 pubkey - `020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` - would be encoded as - `EB5AE98721020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` +`020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` +would be encoded as +`EB5AE98721020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` ### Key Types @@ -170,11 +170,11 @@ We use the RFC 6962 specification of a merkle tree, with sha256 as the hash func Merkle trees are used throughout Tendermint to compute a cryptographic digest of a data structure. The differences between RFC 6962 and the simplest form a merkle tree are that: -1) leaf nodes and inner nodes have different hashes. +1. leaf nodes and inner nodes have different hashes. This is for "second pre-image resistance", to prevent the proof to an inner node being valid as the proof of a leaf. The leaf nodes are `SHA256(0x00 || leaf_data)`, and inner nodes are `SHA256(0x01 || left_hash || right_hash)`. -2) When the number of items isn't a power of two, the left half of the tree is as big as it could be. +2. When the number of items isn't a power of two, the left half of the tree is as big as it could be. (The largest power of two less than the number of items) This allows new leaves to be added with less recomputation. For example: @@ -290,7 +290,7 @@ func computeHashFromAunts(index, total int, leafHash []byte, innerHashes [][]byt ### IAVL+ Tree -Because Tendermint only uses a Simple Merkle Tree, application developers are expect to use their own Merkle tree in their applications. For example, the IAVL+ Tree - an immutable self-balancing binary tree for persisting application state is used by the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/sdk/core/multistore.md) +Because Tendermint only uses a Simple Merkle Tree, application developers are expect to use their own Merkle tree in their applications. For example, the IAVL+ Tree - an immutable self-balancing binary tree for persisting application state is used by the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/blob/master/docs/clients/lite/specification.md) ## JSON diff --git a/docs/spec/consensus/consensus.md b/docs/spec/consensus/consensus.md index acd07397a..7b424dc6b 100644 --- a/docs/spec/consensus/consensus.md +++ b/docs/spec/consensus/consensus.md @@ -73,11 +73,11 @@ parameters over each successive round. |(When +2/3 Precommits for block found) | v | +--------------------------------------------------------------------+ - | Commit | - | | - | * Set CommitTime = now; | - | * Wait for block, then stage/save/commit block; | - +--------------------------------------------------------------------+ +| Commit | +| | +| * Set CommitTime = now; | +| * Wait for block, then stage/save/commit block; | ++--------------------------------------------------------------------+ ``` # Background Gossip @@ -120,7 +120,7 @@ A proposal is signed and published by the designated proposer at each round. The proposer is chosen by a deterministic and non-choking round robin selection algorithm that selects proposers in proportion to their voting power (see -[implementation](https://github.com/tendermint/tendermint/blob/develop/types/validator_set.go)). +[implementation](https://github.com/tendermint/tendermint/blob/master/types/validator_set.go)). A proposal at `(H,R)` is composed of a block and an optional latest `PoLC-Round < R` which is included iff the proposer knows of one. This @@ -131,13 +131,15 @@ liveness property. ### Propose Step (height:H,round:R) -Upon entering `Propose`: - The designated proposer proposes a block at -`(H,R)`. +Upon entering `Propose`: +- The designated proposer proposes a block at `(H,R)`. -The `Propose` step ends: - After `timeoutProposeR` after entering -`Propose`. --> goto `Prevote(H,R)` - After receiving proposal block -and all prevotes at `PoLC-Round`. --> goto `Prevote(H,R)` - After -[common exit conditions](#common-exit-conditions) +The `Propose` step ends: +- After `timeoutProposeR` after entering `Propose`. --> goto + `Prevote(H,R)` +- After receiving proposal block and all prevotes at `PoLC-Round`. --> + goto `Prevote(H,R)` +- After [common exit conditions](#common-exit-conditions) ### Prevote Step (height:H,round:R) @@ -152,10 +154,12 @@ Upon entering `Prevote`, each validator broadcasts its prevote vote. - Else, if the proposal is invalid or wasn't received on time, it prevotes ``. -The `Prevote` step ends: - After +2/3 prevotes for a particular block or -``. -->; goto `Precommit(H,R)` - After `timeoutPrevote` after -receiving any +2/3 prevotes. --> goto `Precommit(H,R)` - After -[common exit conditions](#common-exit-conditions) +The `Prevote` step ends: +- After +2/3 prevotes for a particular block or ``. -->; goto + `Precommit(H,R)` +- After `timeoutPrevote` after receiving any +2/3 prevotes. --> goto + `Precommit(H,R)` +- After [common exit conditions](#common-exit-conditions) ### Precommit Step (height:H,round:R) @@ -163,17 +167,19 @@ Upon entering `Precommit`, each validator broadcasts its precommit vote. - If the validator has a PoLC at `(H,R)` for a particular block `B`, it (re)locks (or changes lock to) and precommits `B` and sets - `LastLockRound = R`. - Else, if the validator has a PoLC at `(H,R)` for - ``, it unlocks and precommits ``. - Else, it keeps the lock - unchanged and precommits ``. + `LastLockRound = R`. +- Else, if the validator has a PoLC at `(H,R)` for ``, it unlocks + and precommits ``. +- Else, it keeps the lock unchanged and precommits ``. A precommit for `` means "I didn’t see a PoLC for this round, but I did get +2/3 prevotes and waited a bit". -The Precommit step ends: - After +2/3 precommits for ``. --> -goto `Propose(H,R+1)` - After `timeoutPrecommit` after receiving any -+2/3 precommits. --> goto `Propose(H,R+1)` - After [common exit -conditions](#common-exit-conditions) +The Precommit step ends: +- After +2/3 precommits for ``. --> goto `Propose(H,R+1)` +- After `timeoutPrecommit` after receiving any +2/3 precommits. --> goto + `Propose(H,R+1)` +- After [common exit conditions](#common-exit-conditions) ### Common exit conditions diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png new file mode 100644 index 000000000..1a92871a5 Binary files /dev/null and b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png differ diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png new file mode 100644 index 000000000..87d6fad93 Binary files /dev/null and b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png differ diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png new file mode 100644 index 000000000..ee853ea93 Binary files /dev/null and b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png differ diff --git a/docs/spec/reactors/block_sync/bcv1/impl-v1.md b/docs/spec/reactors/block_sync/bcv1/impl-v1.md new file mode 100644 index 000000000..0ffaaea69 --- /dev/null +++ b/docs/spec/reactors/block_sync/bcv1/impl-v1.md @@ -0,0 +1,237 @@ +# Blockchain Reactor v1 + +### Data Structures +The data structures used are illustrated below. + +![Data Structures](img/bc-reactor-new-datastructs.png) + +#### BlockchainReactor +- is a `p2p.BaseReactor`. +- has a `store.BlockStore` for persistence. +- executes blocks using an `sm.BlockExecutor`. +- starts the FSM and the `poolRoutine()`. +- relays the fast-sync responses and switch messages to the FSM. +- handles errors from the FSM and when necessarily reports them to the switch. +- implements the blockchain reactor interface used by the FSM to send requests, errors to the switch and state timer resets. +- registers all the concrete types and interfaces for serialisation. + +```go +type BlockchainReactor struct { + p2p.BaseReactor + + initialState sm.State // immutable + state sm.State + + blockExec *sm.BlockExecutor + store *store.BlockStore + + fastSync bool + + fsm *BcReactorFSM + blocksSynced int + + // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. + messagesForFSMCh chan bcReactorMessage + + // Switch goroutine may send RemovePeer to the blockchain reactor. This is an error message that is relayed + // to this channel to be processed in the context of the poolRoutine. + errorsForFSMCh chan bcReactorMessage + + // This channel is used by the FSM and indirectly the block pool to report errors to the blockchain reactor and + // the switch. + eventsFromFSMCh chan bcFsmMessage +} +``` + +#### BcReactorFSM +- implements a simple finite state machine. +- has a state and a state timer. +- has a `BlockPool` to keep track of block requests sent to peers and blocks received from peers. +- uses an interface to send status requests, block requests and reporting errors. The interface is implemented by the `BlockchainReactor` and tests. + +```go +type BcReactorFSM struct { + logger log.Logger + mtx sync.Mutex + + startTime time.Time + + state *bcReactorFSMState + stateTimer *time.Timer + pool *BlockPool + + // interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc. + toBcR bcReactor +} +``` + +#### BlockPool +- maintains a peer set, implemented as a map of peer ID to `BpPeer`. +- maintains a set of requests made to peers, implemented as a map of block request heights to peer IDs. +- maintains a list of future block requests needed to advance the fast-sync. This is a list of block heights. +- keeps track of the maximum height of the peers in the set. +- uses an interface to send requests and report errors to the reactor (via FSM). + +```go +type BlockPool struct { + logger log.Logger + // Set of peers that have sent status responses, with height bigger than pool.Height + peers map[p2p.ID]*BpPeer + // Set of block heights and the corresponding peers from where a block response is expected or has been received. + blocks map[int64]p2p.ID + + plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest + nextRequestHeight int64 // next height to be added to plannedRequests + + Height int64 // height of next block to execute + MaxPeerHeight int64 // maximum height of all peers + toBcR bcReactor +} +``` +Some reasons for the `BlockPool` data structure content: +1. If a peer is removed by the switch fast access is required to the peer and the block requests made to that peer in order to redo them. +2. When block verification fails fast access is required from the block height to the peer and the block requests made to that peer in order to redo them. +3. The `BlockchainReactor` main routine decides when the block pool is running low and asks the `BlockPool` (via FSM) to make more requests. The `BlockPool` creates a list of requests and triggers the sending of the block requests (via the interface). The reason it maintains a list of requests is the redo operations that may occur during error handling. These are redone when the `BlockchainReactor` requires more blocks. + +#### BpPeer +- keeps track of a single peer, with height bigger than the initial height. +- maintains the block requests made to the peer and the blocks received from the peer until they are executed. +- monitors the peer speed when there are pending requests. +- it has an active timer when pending requests are present and reports error on timeout. + +```go +type BpPeer struct { + logger log.Logger + ID p2p.ID + + Height int64 // the peer reported height + NumPendingBlockRequests int // number of requests still waiting for block responses + blocks map[int64]*types.Block // blocks received or expected to be received from this peer + blockResponseTimer *time.Timer + recvMonitor *flow.Monitor + params *BpPeerParams // parameters for timer and monitor + + onErr func(err error, peerID p2p.ID) // function to call on error +} +``` + +### Concurrency Model + +The diagram below shows the goroutines (depicted by the gray blocks), timers (shown on the left with their values) and channels (colored rectangles). The FSM box shows some of the functionality and it is not a separate goroutine. + +The interface used by the FSM is shown in light red with the `IF` block. This is used to: +- send block requests +- report peer errors to the switch - this results in the reactor calling `switch.StopPeerForError()` and, if triggered by the peer timeout routine, a `removePeerEv` is sent to the FSM and action is taken from the context of the `poolRoutine()` +- ask the reactor to reset the state timers. The timers are owned by the FSM while the timeout routine is defined by the reactor. This was done in order to avoid running timers in tests and will change in the next revision. + +There are two main goroutines implemented by the blockchain reactor. All I/O operations are performed from the `poolRoutine()` context while the CPU intensive operations related to the block execution are performed from the context of the `executeBlocksRoutine()`. All goroutines are detailed in the next sections. + +![Go Routines Diagram](img/bc-reactor-new-goroutines.png) + +#### Receive() +Fast-sync messages from peers are received by this goroutine. It performs basic validation and: +- in helper mode (i.e. for request message) it replies immediately. This is different than the proposal in adr-040 that specifies having the FSM handling these. +- forwards response messages to the `poolRoutine()`. + +#### poolRoutine() +(named kept as in the previous reactor). +It starts the `executeBlocksRoutine()` and the FSM. It then waits in a loop for events. These are received from the following channels: +- `sendBlockRequestTicker.C` - every 10msec the reactor asks FSM to make more block requests up to a maximum. Note: currently this value is constant but could be changed based on low/ high watermark thresholds for the number of blocks received and waiting to be processed, the number of blockResponse messages waiting in messagesForFSMCh, etc. +- `statusUpdateTicker.C` - every 10 seconds the reactor broadcasts status requests to peers. While adr-040 specifies this to run within the FSM, at this point this functionality is kept in the reactor. +- `messagesForFSMCh` - the `Receive()` goroutine sends status and block response messages to this channel and the reactor calls FSM to handle them. +- `errorsForFSMCh` - this channel receives the following events: + - peer remove - when the switch removes a peer + - sate timeout event - when FSM state timers trigger + The reactor forwards this messages to the FSM. +- `eventsFromFSMCh` - there are two type of events sent over this channel: + - `syncFinishedEv` - triggered when FSM enters `finished` state and calls the switchToConsensus() interface function. + - `peerErrorEv`- peer timer expiry goroutine sends this event over the channel for processing from poolRoutine() context. + +#### executeBlocksRoutine() +Started by the `poolRoutine()`, it retrieves blocks from the pool and executes them: +- `processReceivedBlockTicker.C` - a ticker event is received over the channel every 10msec and its handling results in a signal being sent to the doProcessBlockCh channel. +- doProcessBlockCh - events are received on this channel as described as above and upon processing blocks are retrieved from the pool and executed. + + +### FSM + +![fsm](img/bc-reactor-new-fsm.png) + +#### States +##### init (aka unknown) +The FSM is created in `unknown` state. When started, by the reactor (`startFSMEv`), it broadcasts Status requests and transitions to `waitForPeer` state. + +##### waitForPeer +In this state, the FSM waits for a Status responses from a "tall" peer. A timer is running in this state to allow the FSM to finish if there are no useful peers. + +If the timer expires, it moves to `finished` state and calls the reactor to switch to consensus. +If a Status response is received from a peer within the timeout, the FSM transitions to `waitForBlock` state. + +##### waitForBlock +In this state the FSM makes Block requests (triggered by a ticker in reactor) and waits for Block responses. There is a timer running in this state to detect if a peer is not sending the block at current processing height. If the timer expires, the FSM removes the peer where the request was sent and all requests made to that peer are redone. + +As blocks are received they are stored by the pool. Block execution is independently performed by the reactor and the result reported to the FSM: +- if there are no errors, the FSM increases the pool height and resets the state timer. +- if there are errors, the peers that delivered the two blocks (at height and height+1) are removed and the requests redone. + +In this state the FSM may receive peer remove events in any of the following scenarios: +- the switch is removing a peer +- a peer is penalized because it has not responded to some block requests for a long time +- a peer is penalized for being slow + +When processing of the last block (the one with height equal to the highest peer height minus one) is successful, the FSM transitions to `finished` state. +If after a peer update or removal the pool height is same as maxPeerHeight, the FSM transitions to `finished` state. + +##### finished +When entering this state, the FSM calls the reactor to switch to consensus and performs cleanup. + +#### Events + +The following events are handled by the FSM: + +```go +const ( + startFSMEv = iota + 1 + statusResponseEv + blockResponseEv + processedBlockEv + makeRequestsEv + stopFSMEv + peerRemoveEv = iota + 256 + stateTimeoutEv +) +``` + +### Examples of Scenarios and Termination Handling +A few scenarios are covered in this section together with the current/ proposed handling. +In general, the scenarios involving faulty peers are made worse by the fact that they may quickly be re-added. + +#### 1. No Tall Peers + +S: In this scenario a node is started and while there are status responses received, none of the peers are at a height higher than this node. + +H: The FSM times out in `waitForPeer` state, moves to `finished` state where it calls the reactor to switch to consensus. + +#### 2. Typical Fast Sync + +S: A node fast syncs blocks from honest peers and eventually downloads and executes the penultimate block. + +H: The FSM in `waitForBlock` state will receive the processedBlockEv from the reactor and detect that the termination height is achieved. + +#### 3. Peer Claims Big Height but no Blocks + +S: In this scenario a faulty peer claims a big height (for which there are no blocks). + +H: The requests for the non-existing block will timeout, the peer removed and the pool's `MaxPeerHeight` updated. FSM checks if the termination height is achieved when peers are removed. + +#### 4. Highest Peer Removed or Updated to Short + +S: The fast sync node is caught up with all peers except one tall peer. The tall peer is removed or it sends status response with low height. + +H: FSM checks termination condition on peer removal and updates. + +#### 5. Block At Current Height Delayed + +S: A peer can block the progress of fast sync by delaying indefinitely the block response for the current processing height (h1). + +H: Currently, given h1 < h2, there is no enforcement at peer level that the response for h1 should be received before h2. So a peer will timeout only after delivering all blocks except h1. However the `waitForBlock` state timer fires if the block for current processing height is not received within a timeout. The peer is removed and the requests to that peer (including the one for current height) redone. diff --git a/docs/spec/reactors/block_sync/img/bc-reactor-routines.png b/docs/spec/reactors/block_sync/img/bc-reactor-routines.png new file mode 100644 index 000000000..3f574a79b Binary files /dev/null and b/docs/spec/reactors/block_sync/img/bc-reactor-routines.png differ diff --git a/docs/spec/reactors/block_sync/impl.md b/docs/spec/reactors/block_sync/impl.md index 195f9b862..35a37debb 100644 --- a/docs/spec/reactors/block_sync/impl.md +++ b/docs/spec/reactors/block_sync/impl.md @@ -1,4 +1,6 @@ -## Blockchain Reactor +## Blockchain Reactor v0 Modules + +### Blockchain Reactor - coordinates the pool for syncing - coordinates the store for persistence @@ -8,7 +10,7 @@ - starts the pool.Start() and its poolRoutine() - registers all the concrete types and interfaces for serialisation -### poolRoutine +#### poolRoutine - listens to these channels: - pool requests blocks from a specific peer by posting to requestsCh, block reactor then sends @@ -22,7 +24,7 @@ - implements Receive which is called by the switch/peer - calls AddBlock on the pool when it receives a new block from a peer -## Block Pool +### Block Pool - responsible for downloading blocks from peers - makeRequestersRoutine() @@ -36,6 +38,7 @@ - we receive a block - gotBlockCh is strange -## Block Store -- persists blocks to disk +### Go Routines in Blockchain Reactor + +![Go Routines Diagram](img/bc-reactor-routines.png) diff --git a/docs/spec/reactors/mempool/reactor.md b/docs/spec/reactors/mempool/reactor.md index d349fc7cc..7e9a2d8fe 100644 --- a/docs/spec/reactors/mempool/reactor.md +++ b/docs/spec/reactors/mempool/reactor.md @@ -7,7 +7,7 @@ See [this issue](https://github.com/tendermint/tendermint/issues/1503) Mempool maintains a cache of the last 10000 transactions to prevent replaying old transactions (plus transactions coming from other validators, who are continually exchanging transactions). Read [Replay -Protection](../../../../app-development.md#replay-protection) +Protection](../../../app-dev/app-development.md#replay-protection) for details. Sending incorrectly encoded data or data exceeding `maxMsgSize` will result diff --git a/docs/spec/software/wal.md b/docs/spec/software/wal.md index 1f5d712c5..889ce4868 100644 --- a/docs/spec/software/wal.md +++ b/docs/spec/software/wal.md @@ -28,5 +28,5 @@ WAL. Then it will go to precommit, and that time it will work because the private validator contains the `LastSignBytes` and then we’ll replay the precommit from the WAL. -Make sure to read about [WAL corruption](../../../tendermint-core/running-in-production.md#wal-corruption) +Make sure to read about [WAL corruption](../../tendermint-core/running-in-production.md#wal-corruption) and recovery strategies. diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index df05f7c5d..59a259669 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -138,6 +138,12 @@ max_subscriptions_per_client = 5 # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "10s" +# Maximum size of request body, in bytes +max_body_bytes = {{ .RPC.MaxBodyBytes }} + +# Maximum size of request header, in bytes +max_header_bytes = {{ .RPC.MaxHeaderBytes }} + # The path to a file containing certificate that is used to create the HTTPS server. # Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, @@ -234,6 +240,18 @@ max_txs_bytes = 1073741824 # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = 10000 +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes} + {amino overhead}. +max_tx_bytes = 1048576 + +##### fast sync configuration options ##### +[fastsync] + +# Fast Sync version to use: +# 1) "v0" (default) - the legacy fast sync implementation +# 2) "v1" - refactor of v0 version for better testability +version = "v0" + ##### consensus configuration options ##### [consensus] @@ -315,8 +333,7 @@ namespace = "tendermint" If `create_empty_blocks` is set to `true` in your config, blocks will be created ~ every second (with default consensus parameters). You can regulate -the delay between blocks by changing the `timeout_commit`. E.g. `timeout_commit -= "10s"` should result in ~ 10 second blocks. +the delay between blocks by changing the `timeout_commit`. E.g. `timeout_commit = "10s"` should result in ~ 10 second blocks. **create_empty_blocks = false** @@ -342,7 +359,7 @@ Tendermint will only create blocks if there are transactions, or after waiting ## Consensus timeouts explained There's a variety of information about timeouts in [Running in -production](./running-in-production.html) +production](./running-in-production.md) You can also find more detailed technical explanation in the spec: [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938). diff --git a/docs/tendermint-core/how-to-read-logs.md b/docs/tendermint-core/how-to-read-logs.md index 54c2c8a32..e852298b8 100644 --- a/docs/tendermint-core/how-to-read-logs.md +++ b/docs/tendermint-core/how-to-read-logs.md @@ -115,7 +115,7 @@ little overview what they do. - `abci-client` As mentioned in [Application Development Guide](../app-dev/app-development.md), Tendermint acts as an ABCI client with respect to the application and maintains 3 connections: mempool, consensus and query. The code used by Tendermint Core can - be found [here](https://github.com/tendermint/tendermint/tree/develop/abci/client). + be found [here](https://github.com/tendermint/tendermint/tree/master/abci/client). - `blockchain` Provides storage, pool (a group of peers), and reactor for both storing and exchanging blocks between peers. - `consensus` The heart of Tendermint core, which is the diff --git a/docs/tendermint-core/rpc.md b/docs/tendermint-core/rpc.md index 4ea5ab0d9..1b8e24426 100644 --- a/docs/tendermint-core/rpc.md +++ b/docs/tendermint-core/rpc.md @@ -4,4 +4,4 @@ The RPC documentation is hosted here: - [https://tendermint.com/rpc/](https://tendermint.com/rpc/) -To update the documentation, edit the relevant `godoc` comments in the [rpc/core directory](https://github.com/tendermint/tendermint/tree/develop/rpc/core). +To update the documentation, edit the relevant `godoc` comments in the [rpc/core directory](https://github.com/tendermint/tendermint/tree/master/rpc/core). diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 05d481b2c..abf382501 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -20,7 +20,7 @@ Initialize the root directory by running: tendermint init ``` -This will create a new private key (`priv_validator.json`), and a +This will create a new private key (`priv_validator_key.json`), and a genesis file (`genesis.json`) containing the associated public key, in `$TMHOME/config`. This is all that's necessary to run a local testnet with one validator. @@ -43,6 +43,11 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g - `chain_id`: ID of the blockchain. This must be unique for every blockchain. If your testnet blockchains do not have unique chain IDs, you will have a bad time. The ChainID must be less than 50 symbols. +- `consensus_params` + - `block` + - `time_iota_ms`: Minimum time increment between consecutive blocks (in + milliseconds). If the block header timestamp is ahead of the system clock, + decrease this value. - `validators`: List of initial validators. Note this may be overridden entirely by the application, and may be left empty to make explicit that the application will initialize the validator set with ResponseInitChain. @@ -63,9 +68,10 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g "genesis_time": "2018-11-13T18:11:50.277637Z", "chain_id": "test-chain-s4ui7D", "consensus_params": { - "block_size": { + "block": { "max_bytes": "22020096", - "max_gas": "-1" + "max_gas": "-1", + "time_iota_ms": "1000" }, "evidence": { "max_age": "100000" @@ -308,7 +314,7 @@ write-ahead-log](../tendermint-core/running-in-production.md#mempool-wal) ## Tendermint Networks When `tendermint init` is run, both a `genesis.json` and -`priv_validator.json` are created in `~/.tendermint/config`. The +`priv_validator_key.json` are created in `~/.tendermint/config`. The `genesis.json` might look like: ``` @@ -329,7 +335,7 @@ When `tendermint init` is run, both a `genesis.json` and } ``` -And the `priv_validator.json`: +And the `priv_validator_key.json`: ``` { @@ -348,20 +354,20 @@ And the `priv_validator.json`: } ``` -The `priv_validator.json` actually contains a private key, and should +The `priv_validator_key.json` actually contains a private key, and should thus be kept absolutely secret; for now we work with the plain text. Note the `last_` fields, which are used to prevent us from signing conflicting messages. Note also that the `pub_key` (the public key) in the -`priv_validator.json` is also present in the `genesis.json`. +`priv_validator_key.json` is also present in the `genesis.json`. The genesis file contains the list of public keys which may participate in the consensus, and their corresponding voting power. Greater than 2/3 of the voting power must be active (i.e. the corresponding private keys must be producing signatures) for the consensus to make progress. In our case, the genesis file contains the public key of our -`priv_validator.json`, so a Tendermint node started with the default +`priv_validator_key.json`, so a Tendermint node started with the default root directory will be able to make progress. Voting power uses an int64 but must be positive, thus the range is: 0 through 9223372036854775807. Because of how the current proposer selection algorithm works, we do not @@ -447,16 +453,16 @@ not connected to the other peer. The easiest way to add new validators is to do it in the `genesis.json`, before starting the network. For instance, we could make a new -`priv_validator.json`, and copy it's `pub_key` into the above genesis. +`priv_validator_key.json`, and copy it's `pub_key` into the above genesis. -We can generate a new `priv_validator.json` with the command: +We can generate a new `priv_validator_key.json` with the command: ``` tendermint gen_validator ``` Now we can update our genesis file. For instance, if the new -`priv_validator.json` looks like: +`priv_validator_key.json` looks like: ``` { @@ -504,7 +510,7 @@ then the new `genesis.json` will be: ``` Update the `genesis.json` in `~/.tendermint/config`. Copy the genesis -file and the new `priv_validator.json` to the `~/.tendermint/config` on +file and the new `priv_validator_key.json` to the `~/.tendermint/config` on a new machine. Now run `tendermint node` on both machines, and use either diff --git a/evidence/wire.go b/evidence/codec.go similarity index 100% rename from evidence/wire.go rename to evidence/codec.go diff --git a/evidence/pool.go b/evidence/pool.go index 18ccb3344..66b35ef98 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -5,8 +5,8 @@ import ( "sync" clist "github.com/tendermint/tendermint/libs/clist" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 13bc45563..0e35ea29d 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" - dbm "github.com/tendermint/tendermint/libs/db" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-db" ) func TestMain(m *testing.M) { diff --git a/evidence/reactor_test.go b/evidence/reactor_test.go index 635e9553f..9603e6680 100644 --- a/evidence/reactor_test.go +++ b/evidence/reactor_test.go @@ -11,10 +11,10 @@ import ( cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto/secp256k1" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) // evidenceLogger is a TestingLogger which uses a different diff --git a/evidence/store.go b/evidence/store.go index 464d6138e..29054abe3 100644 --- a/evidence/store.go +++ b/evidence/store.go @@ -3,8 +3,8 @@ package evidence import ( "fmt" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) /* diff --git a/evidence/store_test.go b/evidence/store_test.go index 5a7a8bd36..e3603ef5a 100644 --- a/evidence/store_test.go +++ b/evidence/store_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) //------------------------------------------- diff --git a/go.mod b/go.mod index f4a4c6dda..ffa305e38 100644 --- a/go.mod +++ b/go.mod @@ -3,31 +3,29 @@ module github.com/tendermint/tendermint go 1.12 require ( - github.com/BurntSushi/toml v0.3.1 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a - github.com/etcd-io/bbolt v1.3.2 github.com/fortytw2/leaktest v1.2.0 github.com/go-kit/kit v0.6.0 github.com/go-logfmt/logfmt v0.3.0 github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.2.1 - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect - github.com/golang/protobuf v1.3.0 - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/golang/protobuf v1.3.2 github.com/google/gofuzz v1.0.0 // indirect + github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 // indirect github.com/gorilla/websocket v1.2.0 github.com/hashicorp/hcl v1.0.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jmhodges/levigo v1.0.0 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect + github.com/libp2p/go-buffer-pool v0.0.1 github.com/magiconair/properties v1.8.0 github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/pelletier/go-toml v1.2.0 // indirect - github.com/pkg/errors v0.8.0 + github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.1 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 // indirect @@ -40,12 +38,11 @@ require ( github.com/spf13/jwalterweatherman v1.0.0 // indirect github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/viper v1.0.0 - github.com/stretchr/testify v1.2.2 - github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e + github.com/stretchr/testify v1.3.0 github.com/tendermint/go-amino v0.14.1 - go.etcd.io/bbolt v1.3.3 // indirect - golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 - golang.org/x/net v0.0.0-20180906233101-161cd47e91fd - google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 // indirect - google.golang.org/grpc v1.13.0 + github.com/tendermint/tm-db v0.1.1 + golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 + google.golang.org/grpc v1.22.0 ) diff --git a/go.sum b/go.sum index 2a349d300..c6d506f0b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.26.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/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= @@ -15,11 +16,13 @@ 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= -github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -34,19 +37,25 @@ 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/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/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.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/pprof v0.0.0-20190723021845-34ac40c74b70 h1:XTnP8fJpa4Kvpw2qARB4KS9izqxPS0Sd92cDlY3uk+w= +github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -58,6 +67,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/libp2p/go-buffer-pool v0.0.1 h1:9Rrn/H46cXjaA2HQ5Y8lyhOS1NhTkZ4yuEs2r3Eechg= +github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -71,8 +82,8 @@ 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/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= @@ -99,32 +110,50 @@ 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.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/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/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e h1:91EeXI4y4ShkyzkMqZ7QP/ZTIqwXp3RuDu5WFzxcFAs= -github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw= +github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= 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.0.0-20190731085305-94017c88bf1d h1:yCHL2COLGLNfb4sA9AlzIHpapb8UATvAQyJulS6Eg6Q= +github.com/tendermint/tm-db v0.0.0-20190731085305-94017c88bf1d/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= +github.com/tendermint/tm-db v0.1.1 h1:G3Xezy3sOk9+ekhjZ/kjArYIs1SmwV+1OUgNkj7RgV0= +github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c h1:Rx/HTKi09myZ25t1SOlDHmHOy/mKxNAcu0hP1oPX9qM= +golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpdZNbLuQj6FOzCaZe3w+vhLjn5AcFA= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= -google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= @@ -133,3 +162,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/libs/autofile/group.go b/libs/autofile/group.go index ce73466e4..7cc345478 100644 --- a/libs/autofile/group.go +++ b/libs/autofile/group.go @@ -472,7 +472,8 @@ func (gr *GroupReader) Read(p []byte) (n int, err error) { for { nn, err = gr.curReader.Read(p[n:]) n += nn - if err == io.EOF { + switch { + case err == io.EOF: if n >= lenP { return n, nil } @@ -480,9 +481,9 @@ func (gr *GroupReader) Read(p []byte) (n int, err error) { if err1 := gr.openFile(gr.curIndex + 1); err1 != nil { return n, err1 } - } else if err != nil { + case err != nil: return n, err - } else if nn == 0 { // empty file + case nn == 0: // empty file return n, err } } diff --git a/libs/circle.yml b/libs/circle.yml deleted file mode 100644 index 2b7d1266c..000000000 --- a/libs/circle.yml +++ /dev/null @@ -1,21 +0,0 @@ -machine: - environment: - GOPATH: "${HOME}/.go_workspace" - PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" - PROJECT_PATH: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME - hosts: - localhost: 127.0.0.1 - -dependencies: - override: - - mkdir -p "$PROJECT_PARENT_PATH" - - ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$PROJECT_PATH" - post: - - go version - -test: - override: - - cd $PROJECT_PATH && make get_tools && bash ./test.sh - post: - - cd "$PROJECT_PATH" && bash <(curl -s https://codecov.io/bash) -f coverage.txt - - cd "$PROJECT_PATH" && mv coverage.txt "${CIRCLE_ARTIFACTS}" diff --git a/libs/cli/helper.go b/libs/cli/helper.go index 878cf26e5..6bf23750c 100644 --- a/libs/cli/helper.go +++ b/libs/cli/helper.go @@ -14,7 +14,7 @@ import ( func WriteConfigVals(dir string, vals map[string]string) error { data := "" for k, v := range vals { - data = data + fmt.Sprintf("%s = \"%s\"\n", k, v) + data += fmt.Sprintf("%s = \"%s\"\n", k, v) } cfile := filepath.Join(dir, "config.toml") return ioutil.WriteFile(cfile, []byte(data), 0666) diff --git a/libs/common/async.go b/libs/common/async.go index e3293ab4c..326b97248 100644 --- a/libs/common/async.go +++ b/libs/common/async.go @@ -61,9 +61,10 @@ func (trs *TaskResultSet) Reap() *TaskResultSet { TaskResult: result, OK: true, } - } else { - // We already wrote it. } + // else { + // We already wrote it. + // } default: // Do nothing. } @@ -83,9 +84,10 @@ func (trs *TaskResultSet) Wait() *TaskResultSet { TaskResult: result, OK: true, } - } else { - // We already wrote it. } + // else { + // We already wrote it. + // } } return trs } diff --git a/libs/common/async_test.go b/libs/common/async_test.go index f565b4bd3..9ac5ffe3f 100644 --- a/libs/common/async_test.go +++ b/libs/common/async_test.go @@ -31,18 +31,20 @@ func TestParallel(t *testing.T) { var failedTasks int for i := 0; i < len(tasks); i++ { taskResult, ok := trs.LatestResult(i) - if !ok { + switch { + case !ok: assert.Fail(t, "Task #%v did not complete.", i) failedTasks++ - } else if taskResult.Error != nil { + case taskResult.Error != nil: assert.Fail(t, "Task should not have errored but got %v", taskResult.Error) failedTasks++ - } else if !assert.Equal(t, -1*i, taskResult.Value.(int)) { + case !assert.Equal(t, -1*i, taskResult.Value.(int)): assert.Fail(t, "Task should have returned %v but got %v", -1*i, taskResult.Value.(int)) failedTasks++ - } else { - // Good! } + // else { + // Good! + // } } assert.Equal(t, failedTasks, 0, "No task should have failed") assert.Nil(t, trs.FirstError(), "There should be no errors") @@ -132,11 +134,12 @@ func checkResult(t *testing.T, taskResultSet *TaskResultSet, index int, val inte taskName := fmt.Sprintf("Task #%v", index) assert.True(t, ok, "TaskResultCh unexpectedly closed for %v", taskName) assert.Equal(t, val, taskResult.Value, taskName) - if err != nil { + switch { + case err != nil: assert.Equal(t, err, taskResult.Error, taskName) - } else if pnk != nil { + case pnk != nil: assert.Equal(t, pnk, taskResult.Error.(Error).Data(), taskName) - } else { + default: assert.Nil(t, taskResult.Error, taskName) } } diff --git a/libs/common/date.go b/libs/common/date.go deleted file mode 100644 index e017a4b41..000000000 --- a/libs/common/date.go +++ /dev/null @@ -1,43 +0,0 @@ -package common - -import ( - "strings" - "time" - - "github.com/pkg/errors" -) - -// TimeLayout helps to parse a date string of the format YYYY-MM-DD -// Intended to be used with the following function: -// time.Parse(TimeLayout, date) -var TimeLayout = "2006-01-02" //this represents YYYY-MM-DD - -// ParseDateRange parses a date range string of the format start:end -// where the start and end date are of the format YYYY-MM-DD. -// The parsed dates are time.Time and will return the zero time for -// unbounded dates, ex: -// unbounded start: :2000-12-31 -// unbounded end: 2000-12-31: -func ParseDateRange(dateRange string) (startDate, endDate time.Time, err error) { - dates := strings.Split(dateRange, ":") - if len(dates) != 2 { - err = errors.New("bad date range, must be in format date:date") - return - } - parseDate := func(date string) (out time.Time, err error) { - if len(date) == 0 { - return - } - out, err = time.Parse(TimeLayout, date) - return - } - startDate, err = parseDate(dates[0]) - if err != nil { - return - } - endDate, err = parseDate(dates[1]) - if err != nil { - return - } - return -} diff --git a/libs/common/date_test.go b/libs/common/date_test.go deleted file mode 100644 index 2c0632477..000000000 --- a/libs/common/date_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package common - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - date = time.Date(2015, time.Month(12), 31, 0, 0, 0, 0, time.UTC) - date2 = time.Date(2016, time.Month(12), 31, 0, 0, 0, 0, time.UTC) - zero time.Time -) - -func TestParseDateRange(t *testing.T) { - assert := assert.New(t) - - var testDates = []struct { - dateStr string - start time.Time - end time.Time - errNil bool - }{ - {"2015-12-31:2016-12-31", date, date2, true}, - {"2015-12-31:", date, zero, true}, - {":2016-12-31", zero, date2, true}, - {"2016-12-31", zero, zero, false}, - {"2016-31-12:", zero, zero, false}, - {":2016-31-12", zero, zero, false}, - } - - for _, test := range testDates { - start, end, err := ParseDateRange(test.dateStr) - if test.errNil { - assert.Nil(err) - testPtr := func(want, have time.Time) { - assert.True(have.Equal(want)) - } - testPtr(test.start, start) - testPtr(test.end, end) - } else { - assert.NotNil(err) - } - } -} diff --git a/libs/common/errors.go b/libs/common/errors.go index 24af84267..aacfbe274 100644 --- a/libs/common/errors.go +++ b/libs/common/errors.go @@ -9,7 +9,7 @@ import ( // Convenience method. func ErrorWrap(cause interface{}, format string, args ...interface{}) Error { - if causeCmnError, ok := cause.(*cmnError); ok { + if causeCmnError, ok := cause.(*cmnError); ok { //nolint:gocritic msg := fmt.Sprintf(format, args...) return causeCmnError.Stacktrace().Trace(1, msg) } else if cause == nil { diff --git a/libs/common/heap.go b/libs/common/heap.go deleted file mode 100644 index b3bcb9db8..000000000 --- a/libs/common/heap.go +++ /dev/null @@ -1,125 +0,0 @@ -package common - -import ( - "bytes" - "container/heap" -) - -/* - Example usage: - - ``` - h := NewHeap() - - h.Push("msg1", 1) - h.Push("msg3", 3) - h.Push("msg2", 2) - - fmt.Println(h.Pop()) // msg1 - fmt.Println(h.Pop()) // msg2 - fmt.Println(h.Pop()) // msg3 - ``` -*/ -type Heap struct { - pq priorityQueue -} - -func NewHeap() *Heap { - return &Heap{pq: make([]*pqItem, 0)} -} - -func (h *Heap) Len() int64 { - return int64(len(h.pq)) -} - -func (h *Heap) Push(value interface{}, priority int) { - heap.Push(&h.pq, &pqItem{value: value, priority: cmpInt(priority)}) -} - -func (h *Heap) PushBytes(value interface{}, priority []byte) { - heap.Push(&h.pq, &pqItem{value: value, priority: cmpBytes(priority)}) -} - -func (h *Heap) PushComparable(value interface{}, priority Comparable) { - heap.Push(&h.pq, &pqItem{value: value, priority: priority}) -} - -func (h *Heap) Peek() interface{} { - if len(h.pq) == 0 { - return nil - } - return h.pq[0].value -} - -func (h *Heap) Update(value interface{}, priority Comparable) { - h.pq.Update(h.pq[0], value, priority) -} - -func (h *Heap) Pop() interface{} { - item := heap.Pop(&h.pq).(*pqItem) - return item.value -} - -//----------------------------------------------------------------------------- -// From: http://golang.org/pkg/container/heap/#example__priorityQueue - -type pqItem struct { - value interface{} - priority Comparable - index int -} - -type priorityQueue []*pqItem - -func (pq priorityQueue) Len() int { return len(pq) } - -func (pq priorityQueue) Less(i, j int) bool { - return pq[i].priority.Less(pq[j].priority) -} - -func (pq priorityQueue) Swap(i, j int) { - pq[i], pq[j] = pq[j], pq[i] - pq[i].index = i - pq[j].index = j -} - -func (pq *priorityQueue) Push(x interface{}) { - n := len(*pq) - item := x.(*pqItem) - item.index = n - *pq = append(*pq, item) -} - -func (pq *priorityQueue) Pop() interface{} { - old := *pq - n := len(old) - item := old[n-1] - item.index = -1 // for safety - *pq = old[0 : n-1] - return item -} - -func (pq *priorityQueue) Update(item *pqItem, value interface{}, priority Comparable) { - item.value = value - item.priority = priority - heap.Fix(pq, item.index) -} - -//-------------------------------------------------------------------------------- -// Comparable - -type Comparable interface { - Less(o interface{}) bool -} - -type cmpInt int - -func (i cmpInt) Less(o interface{}) bool { - return int(i) < int(o.(cmpInt)) -} - -type cmpBytes []byte - -func (bz cmpBytes) Less(o interface{}) bool { - return bytes.Compare([]byte(bz), []byte(o.(cmpBytes))) < 0 -} diff --git a/libs/common/io.go b/libs/common/io.go deleted file mode 100644 index fa0443e09..000000000 --- a/libs/common/io.go +++ /dev/null @@ -1,74 +0,0 @@ -package common - -import ( - "bytes" - "errors" - "io" -) - -type PrefixedReader struct { - Prefix []byte - reader io.Reader -} - -func NewPrefixedReader(prefix []byte, reader io.Reader) *PrefixedReader { - return &PrefixedReader{prefix, reader} -} - -func (pr *PrefixedReader) Read(p []byte) (n int, err error) { - if len(pr.Prefix) > 0 { - read := copy(p, pr.Prefix) - pr.Prefix = pr.Prefix[read:] - return read, nil - } - return pr.reader.Read(p) -} - -// NOTE: Not goroutine safe -type BufferCloser struct { - bytes.Buffer - Closed bool -} - -func NewBufferCloser(buf []byte) *BufferCloser { - return &BufferCloser{ - *bytes.NewBuffer(buf), - false, - } -} - -func (bc *BufferCloser) Close() error { - if bc.Closed { - return errors.New("BufferCloser already closed") - } - bc.Closed = true - return nil -} - -func (bc *BufferCloser) Write(p []byte) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.Write(p) -} - -func (bc *BufferCloser) WriteByte(c byte) error { - if bc.Closed { - return errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteByte(c) -} - -func (bc *BufferCloser) WriteRune(r rune) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteRune(r) -} - -func (bc *BufferCloser) WriteString(s string) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteString(s) -} diff --git a/libs/common/os.go b/libs/common/os.go index 7c3fad7ee..0e35524cf 100644 --- a/libs/common/os.go +++ b/libs/common/os.go @@ -1,39 +1,13 @@ package common import ( - "bufio" "fmt" - "io" "io/ioutil" "os" - "os/exec" "os/signal" - "strings" "syscall" ) -var gopath string - -// GoPath returns GOPATH env variable value. If it is not set, this function -// will try to call `go env GOPATH` subcommand. -func GoPath() string { - if gopath != "" { - return gopath - } - - path := os.Getenv("GOPATH") - if len(path) == 0 { - goCmd := exec.Command("go", "env", "GOPATH") - out, err := goCmd.Output() - if err != nil { - panic(fmt.Sprintf("failed to determine gopath: %v", err)) - } - path = string(out) - } - gopath = path - return path -} - type logger interface { Info(msg string, keyvals ...interface{}) } @@ -78,25 +52,6 @@ func EnsureDir(dir string, mode os.FileMode) error { return nil } -func IsDirEmpty(name string) (bool, error) { - f, err := os.Open(name) - if err != nil { - if os.IsNotExist(err) { - return true, err - } - // Otherwise perhaps a permission - // error or some other error. - return false, err - } - defer f.Close() - - _, err = f.Readdirnames(1) // Or f.Readdir(1) - if err == io.EOF { - return true, nil - } - return false, err // Either not empty or error, suits both cases -} - func FileExists(filePath string) bool { _, err := os.Stat(filePath) return !os.IsNotExist(err) @@ -125,19 +80,3 @@ func MustWriteFile(filePath string, contents []byte, mode os.FileMode) { Exit(fmt.Sprintf("MustWriteFile failed: %v", err)) } } - -//-------------------------------------------------------------------------------- - -func Prompt(prompt string, defaultValue string) (string, error) { - fmt.Print(prompt) - reader := bufio.NewReader(os.Stdin) - line, err := reader.ReadString('\n') - if err != nil { - return defaultValue, err - } - line = strings.TrimSpace(line) - if line == "" { - return defaultValue, nil - } - return line, nil -} diff --git a/libs/common/os_test.go b/libs/common/os_test.go deleted file mode 100644 index e8a23ebd6..000000000 --- a/libs/common/os_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package common - -import ( - "os" - "testing" -) - -func TestOSGoPath(t *testing.T) { - // restore original gopath upon exit - path := os.Getenv("GOPATH") - defer func() { - _ = os.Setenv("GOPATH", path) - }() - - err := os.Setenv("GOPATH", "~/testgopath") - if err != nil { - t.Fatal(err) - } - path = GoPath() - if path != "~/testgopath" { - t.Fatalf("should get GOPATH env var value, got %v", path) - } - os.Unsetenv("GOPATH") - - path = GoPath() - if path != "~/testgopath" { - t.Fatalf("subsequent calls should return the same value, got %v", path) - } -} - -func TestOSGoPathWithoutEnvVar(t *testing.T) { - // restore original gopath upon exit - path := os.Getenv("GOPATH") - defer func() { - _ = os.Setenv("GOPATH", path) - }() - - os.Unsetenv("GOPATH") - // reset cache - gopath = "" - - path = GoPath() - if path == "" || path == "~/testgopath" { - t.Fatalf("should get nonempty result of calling go env GOPATH, got %v", path) - } -} diff --git a/libs/common/random_test.go b/libs/common/random_test.go index c59a577b8..74dcc04b4 100644 --- a/libs/common/random_test.go +++ b/libs/common/random_test.go @@ -45,11 +45,9 @@ func TestDeterminism(t *testing.T) { output := testThemAll() if i == 0 { firstOutput = output - } else { - if firstOutput != output { - t.Errorf("Run #%d's output was different from first run.\nfirst: %v\nlast: %v", - i, firstOutput, output) - } + } else if firstOutput != output { + t.Errorf("Run #%d's output was different from first run.\nfirst: %v\nlast: %v", + i, firstOutput, output) } } } diff --git a/libs/common/string.go b/libs/common/string.go index ddf350b10..4f8a8f20d 100644 --- a/libs/common/string.go +++ b/libs/common/string.go @@ -51,11 +51,12 @@ func IsASCIIText(s string) bool { func ASCIITrim(s string) string { r := make([]byte, 0, len(s)) for _, b := range []byte(s) { - if b == 32 { + switch { + case b == 32: continue // skip space - } else if 32 < b && b <= 126 { + case 32 < b && b <= 126: r = append(r, b) - } else { + default: panic(fmt.Sprintf("non-ASCII (non-tab) char 0x%X", b)) } } diff --git a/libs/db/backend_test.go b/libs/db/backend_test.go deleted file mode 100644 index d755a6f27..000000000 --- a/libs/db/backend_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package db - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -func cleanupDBDir(dir, name string) { - err := os.RemoveAll(filepath.Join(dir, name) + ".db") - if err != nil { - panic(err) - } -} - -func testBackendGetSetDelete(t *testing.T, backend DBBackendType) { - // Default - dirname, err := ioutil.TempDir("", fmt.Sprintf("test_backend_%s_", backend)) - require.Nil(t, err) - db := NewDB("testdb", backend, dirname) - defer cleanupDBDir(dirname, "testdb") - - // A nonexistent key should return nil, even if the key is empty - require.Nil(t, db.Get([]byte(""))) - - // A nonexistent key should return nil, even if the key is nil - require.Nil(t, db.Get(nil)) - - // A nonexistent key should return nil. - key := []byte("abc") - require.Nil(t, db.Get(key)) - - // Set empty value. - db.Set(key, []byte("")) - require.NotNil(t, db.Get(key)) - require.Empty(t, db.Get(key)) - - // Set nil value. - db.Set(key, nil) - require.NotNil(t, db.Get(key)) - require.Empty(t, db.Get(key)) - - // Delete. - db.Delete(key) - require.Nil(t, db.Get(key)) -} - -func TestBackendsGetSetDelete(t *testing.T) { - for dbType := range backends { - testBackendGetSetDelete(t, dbType) - } -} - -func withDB(t *testing.T, creator dbCreator, fn func(DB)) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - dir := os.TempDir() - db, err := creator(name, dir) - require.Nil(t, err) - defer cleanupDBDir(dir, name) - fn(db) - db.Close() -} - -func TestBackendsNilKeys(t *testing.T) { - - // Test all backends. - for dbType, creator := range backends { - withDB(t, creator, func(db DB) { - t.Run(fmt.Sprintf("Testing %s", dbType), func(t *testing.T) { - - // Nil keys are treated as the empty key for most operations. - expect := func(key, value []byte) { - if len(key) == 0 { // nil or empty - assert.Equal(t, db.Get(nil), db.Get([]byte(""))) - assert.Equal(t, db.Has(nil), db.Has([]byte(""))) - } - assert.Equal(t, db.Get(key), value) - assert.Equal(t, db.Has(key), value != nil) - } - - // Not set - expect(nil, nil) - - // Set nil value - db.Set(nil, nil) - expect(nil, []byte("")) - - // Set empty value - db.Set(nil, []byte("")) - expect(nil, []byte("")) - - // Set nil, Delete nil - db.Set(nil, []byte("abc")) - expect(nil, []byte("abc")) - db.Delete(nil) - expect(nil, nil) - - // Set nil, Delete empty - db.Set(nil, []byte("abc")) - expect(nil, []byte("abc")) - db.Delete([]byte("")) - expect(nil, nil) - - // Set empty, Delete nil - db.Set([]byte(""), []byte("abc")) - expect(nil, []byte("abc")) - db.Delete(nil) - expect(nil, nil) - - // Set empty, Delete empty - db.Set([]byte(""), []byte("abc")) - expect(nil, []byte("abc")) - db.Delete([]byte("")) - expect(nil, nil) - - // SetSync nil, DeleteSync nil - db.SetSync(nil, []byte("abc")) - expect(nil, []byte("abc")) - db.DeleteSync(nil) - expect(nil, nil) - - // SetSync nil, DeleteSync empty - db.SetSync(nil, []byte("abc")) - expect(nil, []byte("abc")) - db.DeleteSync([]byte("")) - expect(nil, nil) - - // SetSync empty, DeleteSync nil - db.SetSync([]byte(""), []byte("abc")) - expect(nil, []byte("abc")) - db.DeleteSync(nil) - expect(nil, nil) - - // SetSync empty, DeleteSync empty - db.SetSync([]byte(""), []byte("abc")) - expect(nil, []byte("abc")) - db.DeleteSync([]byte("")) - expect(nil, nil) - }) - }) - } -} - -func TestGoLevelDBBackend(t *testing.T) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - db := NewDB(name, GoLevelDBBackend, "") - defer cleanupDBDir("", name) - - _, ok := db.(*GoLevelDB) - assert.True(t, ok) -} - -func TestDBIterator(t *testing.T) { - for dbType := range backends { - t.Run(fmt.Sprintf("%v", dbType), func(t *testing.T) { - testDBIterator(t, dbType) - }) - } -} - -func testDBIterator(t *testing.T, backend DBBackendType) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - dir := os.TempDir() - db := NewDB(name, backend, dir) - defer cleanupDBDir(dir, name) - - for i := 0; i < 10; i++ { - if i != 6 { // but skip 6. - db.Set(int642Bytes(int64(i)), nil) - } - } - - verifyIterator(t, db.Iterator(nil, nil), []int64{0, 1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator") - verifyIterator(t, db.ReverseIterator(nil, nil), []int64{9, 8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator") - - verifyIterator(t, db.Iterator(nil, int642Bytes(0)), []int64(nil), "forward iterator to 0") - verifyIterator(t, db.ReverseIterator(int642Bytes(10), nil), []int64(nil), "reverse iterator from 10 (ex)") - - verifyIterator(t, db.Iterator(int642Bytes(0), nil), []int64{0, 1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator from 0") - verifyIterator(t, db.Iterator(int642Bytes(1), nil), []int64{1, 2, 3, 4, 5, 7, 8, 9}, "forward iterator from 1") - verifyIterator(t, db.ReverseIterator(nil, int642Bytes(10)), []int64{9, 8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 10 (ex)") - verifyIterator(t, db.ReverseIterator(nil, int642Bytes(9)), []int64{8, 7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 9 (ex)") - verifyIterator(t, db.ReverseIterator(nil, int642Bytes(8)), []int64{7, 5, 4, 3, 2, 1, 0}, "reverse iterator from 8 (ex)") - - verifyIterator(t, db.Iterator(int642Bytes(5), int642Bytes(6)), []int64{5}, "forward iterator from 5 to 6") - verifyIterator(t, db.Iterator(int642Bytes(5), int642Bytes(7)), []int64{5}, "forward iterator from 5 to 7") - verifyIterator(t, db.Iterator(int642Bytes(5), int642Bytes(8)), []int64{5, 7}, "forward iterator from 5 to 8") - verifyIterator(t, db.Iterator(int642Bytes(6), int642Bytes(7)), []int64(nil), "forward iterator from 6 to 7") - verifyIterator(t, db.Iterator(int642Bytes(6), int642Bytes(8)), []int64{7}, "forward iterator from 6 to 8") - verifyIterator(t, db.Iterator(int642Bytes(7), int642Bytes(8)), []int64{7}, "forward iterator from 7 to 8") - - verifyIterator(t, db.ReverseIterator(int642Bytes(4), int642Bytes(5)), []int64{4}, "reverse iterator from 5 (ex) to 4") - verifyIterator(t, db.ReverseIterator(int642Bytes(4), int642Bytes(6)), []int64{5, 4}, "reverse iterator from 6 (ex) to 4") - verifyIterator(t, db.ReverseIterator(int642Bytes(4), int642Bytes(7)), []int64{5, 4}, "reverse iterator from 7 (ex) to 4") - verifyIterator(t, db.ReverseIterator(int642Bytes(5), int642Bytes(6)), []int64{5}, "reverse iterator from 6 (ex) to 5") - verifyIterator(t, db.ReverseIterator(int642Bytes(5), int642Bytes(7)), []int64{5}, "reverse iterator from 7 (ex) to 5") - verifyIterator(t, db.ReverseIterator(int642Bytes(6), int642Bytes(7)), []int64(nil), "reverse iterator from 7 (ex) to 6") - - verifyIterator(t, db.Iterator(int642Bytes(0), int642Bytes(1)), []int64{0}, "forward iterator from 0 to 1") - verifyIterator(t, db.ReverseIterator(int642Bytes(8), int642Bytes(9)), []int64{8}, "reverse iterator from 9 (ex) to 8") - - verifyIterator(t, db.Iterator(int642Bytes(2), int642Bytes(4)), []int64{2, 3}, "forward iterator from 2 to 4") - verifyIterator(t, db.Iterator(int642Bytes(4), int642Bytes(2)), []int64(nil), "forward iterator from 4 to 2") - verifyIterator(t, db.ReverseIterator(int642Bytes(2), int642Bytes(4)), []int64{3, 2}, "reverse iterator from 4 (ex) to 2") - verifyIterator(t, db.ReverseIterator(int642Bytes(4), int642Bytes(2)), []int64(nil), "reverse iterator from 2 (ex) to 4") - -} - -func verifyIterator(t *testing.T, itr Iterator, expected []int64, msg string) { - var list []int64 - for itr.Valid() { - list = append(list, bytes2Int64(itr.Key())) - itr.Next() - } - assert.Equal(t, expected, list, msg) -} diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go deleted file mode 100644 index 30501dd82..000000000 --- a/libs/db/boltdb.go +++ /dev/null @@ -1,349 +0,0 @@ -// +build boltdb - -package db - -import ( - "bytes" - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/etcd-io/bbolt" -) - -var bucket = []byte("tm") - -func init() { - registerDBCreator(BoltDBBackend, func(name, dir string) (DB, error) { - return NewBoltDB(name, dir) - }, false) -} - -// BoltDB is a wrapper around etcd's fork of bolt -// (https://github.com/etcd-io/bbolt). -// -// NOTE: All operations (including Set, Delete) are synchronous by default. One -// can globally turn it off by using NoSync config option (not recommended). -// -// A single bucket ([]byte("tm")) is used per a database instance. This could -// lead to performance issues when/if there will be lots of keys. -type BoltDB struct { - db *bbolt.DB -} - -// NewBoltDB returns a BoltDB with default options. -func NewBoltDB(name, dir string) (DB, error) { - return NewBoltDBWithOpts(name, dir, bbolt.DefaultOptions) -} - -// NewBoltDBWithOpts allows you to supply *bbolt.Options. ReadOnly: true is not -// supported because NewBoltDBWithOpts creates a global bucket. -func NewBoltDBWithOpts(name string, dir string, opts *bbolt.Options) (DB, error) { - if opts.ReadOnly { - return nil, errors.New("ReadOnly: true is not supported") - } - - dbPath := filepath.Join(dir, name+".db") - db, err := bbolt.Open(dbPath, os.ModePerm, opts) - if err != nil { - return nil, err - } - - // create a global bucket - err = db.Update(func(tx *bbolt.Tx) error { - _, err := tx.CreateBucketIfNotExists(bucket) - return err - }) - if err != nil { - return nil, err - } - - return &BoltDB{db: db}, nil -} - -func (bdb *BoltDB) Get(key []byte) (value []byte) { - key = nonEmptyKey(nonNilBytes(key)) - err := bdb.db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(bucket) - if v := b.Get(key); v != nil { - value = append([]byte{}, v...) - } - return nil - }) - if err != nil { - panic(err) - } - return -} - -func (bdb *BoltDB) Has(key []byte) bool { - return bdb.Get(key) != nil -} - -func (bdb *BoltDB) Set(key, value []byte) { - key = nonEmptyKey(nonNilBytes(key)) - value = nonNilBytes(value) - err := bdb.db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket(bucket) - return b.Put(key, value) - }) - if err != nil { - panic(err) - } -} - -func (bdb *BoltDB) SetSync(key, value []byte) { - bdb.Set(key, value) -} - -func (bdb *BoltDB) Delete(key []byte) { - key = nonEmptyKey(nonNilBytes(key)) - err := bdb.db.Update(func(tx *bbolt.Tx) error { - return tx.Bucket(bucket).Delete(key) - }) - if err != nil { - panic(err) - } -} - -func (bdb *BoltDB) DeleteSync(key []byte) { - bdb.Delete(key) -} - -func (bdb *BoltDB) Close() { - bdb.db.Close() -} - -func (bdb *BoltDB) Print() { - stats := bdb.db.Stats() - fmt.Printf("%v\n", stats) - - err := bdb.db.View(func(tx *bbolt.Tx) error { - tx.Bucket(bucket).ForEach(func(k, v []byte) error { - fmt.Printf("[%X]:\t[%X]\n", k, v) - return nil - }) - return nil - }) - if err != nil { - panic(err) - } -} - -func (bdb *BoltDB) Stats() map[string]string { - stats := bdb.db.Stats() - m := make(map[string]string) - - // Freelist stats - m["FreePageN"] = fmt.Sprintf("%v", stats.FreePageN) - m["PendingPageN"] = fmt.Sprintf("%v", stats.PendingPageN) - m["FreeAlloc"] = fmt.Sprintf("%v", stats.FreeAlloc) - m["FreelistInuse"] = fmt.Sprintf("%v", stats.FreelistInuse) - - // Transaction stats - m["TxN"] = fmt.Sprintf("%v", stats.TxN) - m["OpenTxN"] = fmt.Sprintf("%v", stats.OpenTxN) - - return m -} - -// boltDBBatch stores key values in sync.Map and dumps them to the underlying -// DB upon Write call. -type boltDBBatch struct { - db *BoltDB - ops []operation -} - -// NewBatch returns a new batch. -func (bdb *BoltDB) NewBatch() Batch { - return &boltDBBatch{ - ops: nil, - db: bdb, - } -} - -// It is safe to modify the contents of the argument after Set returns but not -// before. -func (bdb *boltDBBatch) Set(key, value []byte) { - bdb.ops = append(bdb.ops, operation{opTypeSet, key, value}) -} - -// It is safe to modify the contents of the argument after Delete returns but -// not before. -func (bdb *boltDBBatch) Delete(key []byte) { - bdb.ops = append(bdb.ops, operation{opTypeDelete, key, nil}) -} - -// NOTE: the operation is synchronous (see BoltDB for reasons) -func (bdb *boltDBBatch) Write() { - err := bdb.db.db.Batch(func(tx *bbolt.Tx) error { - b := tx.Bucket(bucket) - for _, op := range bdb.ops { - key := nonEmptyKey(nonNilBytes(op.key)) - switch op.opType { - case opTypeSet: - if putErr := b.Put(key, op.value); putErr != nil { - return putErr - } - case opTypeDelete: - if delErr := b.Delete(key); delErr != nil { - return delErr - } - } - } - return nil - }) - if err != nil { - panic(err) - } -} - -func (bdb *boltDBBatch) WriteSync() { - bdb.Write() -} - -func (bdb *boltDBBatch) Close() {} - -// WARNING: Any concurrent writes or reads will block until the iterator is -// closed. -func (bdb *BoltDB) Iterator(start, end []byte) Iterator { - tx, err := bdb.db.Begin(false) - if err != nil { - panic(err) - } - return newBoltDBIterator(tx, start, end, false) -} - -// WARNING: Any concurrent writes or reads will block until the iterator is -// closed. -func (bdb *BoltDB) ReverseIterator(start, end []byte) Iterator { - tx, err := bdb.db.Begin(false) - if err != nil { - panic(err) - } - return newBoltDBIterator(tx, start, end, true) -} - -// boltDBIterator allows you to iterate on range of keys/values given some -// start / end keys (nil & nil will result in doing full scan). -type boltDBIterator struct { - tx *bbolt.Tx - - itr *bbolt.Cursor - start []byte - end []byte - - currentKey []byte - currentValue []byte - - isInvalid bool - isReverse bool -} - -func newBoltDBIterator(tx *bbolt.Tx, start, end []byte, isReverse bool) *boltDBIterator { - itr := tx.Bucket(bucket).Cursor() - - var ck, cv []byte - if isReverse { - if end == nil { - ck, cv = itr.Last() - } else { - _, _ = itr.Seek(end) // after key - ck, cv = itr.Prev() // return to end key - } - } else { - if start == nil { - ck, cv = itr.First() - } else { - ck, cv = itr.Seek(start) - } - } - - return &boltDBIterator{ - tx: tx, - itr: itr, - start: start, - end: end, - currentKey: ck, - currentValue: cv, - isReverse: isReverse, - isInvalid: false, - } -} - -func (itr *boltDBIterator) Domain() ([]byte, []byte) { - return itr.start, itr.end -} - -func (itr *boltDBIterator) Valid() bool { - if itr.isInvalid { - return false - } - - // iterated to the end of the cursor - if len(itr.currentKey) == 0 { - itr.isInvalid = true - return false - } - - if itr.isReverse { - if itr.start != nil && bytes.Compare(itr.currentKey, itr.start) < 0 { - itr.isInvalid = true - return false - } - } else { - if itr.end != nil && bytes.Compare(itr.end, itr.currentKey) <= 0 { - itr.isInvalid = true - return false - } - } - - // Valid - return true -} - -func (itr *boltDBIterator) Next() { - itr.assertIsValid() - if itr.isReverse { - itr.currentKey, itr.currentValue = itr.itr.Prev() - } else { - itr.currentKey, itr.currentValue = itr.itr.Next() - } -} - -func (itr *boltDBIterator) Key() []byte { - itr.assertIsValid() - return append([]byte{}, itr.currentKey...) -} - -func (itr *boltDBIterator) Value() []byte { - itr.assertIsValid() - var value []byte - if itr.currentValue != nil { - value = append([]byte{}, itr.currentValue...) - } - return value -} - -func (itr *boltDBIterator) Close() { - err := itr.tx.Rollback() - if err != nil { - panic(err) - } -} - -func (itr *boltDBIterator) assertIsValid() { - if !itr.Valid() { - panic("Boltdb-iterator is invalid") - } -} - -// nonEmptyKey returns a []byte("nil") if key is empty. -// WARNING: this may collude with "nil" user key! -func nonEmptyKey(key []byte) []byte { - if len(key) == 0 { - return []byte("nil") - } - return key -} diff --git a/libs/db/boltdb_test.go b/libs/db/boltdb_test.go deleted file mode 100644 index 416a8fd03..000000000 --- a/libs/db/boltdb_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build boltdb - -package db - -import ( - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/require" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -func TestBoltDBNewBoltDB(t *testing.T) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - dir := os.TempDir() - defer cleanupDBDir(dir, name) - - db, err := NewBoltDB(name, dir) - require.NoError(t, err) - db.Close() -} - -func BenchmarkBoltDBRandomReadsWrites(b *testing.B) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - db, err := NewBoltDB(name, "") - if err != nil { - b.Fatal(err) - } - defer func() { - db.Close() - cleanupDBDir("", name) - }() - - benchmarkRandomReadsWrites(b, db) -} diff --git a/libs/db/c_level_db.go b/libs/db/c_level_db.go deleted file mode 100644 index 7538166b2..000000000 --- a/libs/db/c_level_db.go +++ /dev/null @@ -1,325 +0,0 @@ -// +build cleveldb - -package db - -import ( - "bytes" - "fmt" - "path/filepath" - - "github.com/jmhodges/levigo" -) - -func init() { - dbCreator := func(name string, dir string) (DB, error) { - return NewCLevelDB(name, dir) - } - registerDBCreator(CLevelDBBackend, dbCreator, false) -} - -var _ DB = (*CLevelDB)(nil) - -type CLevelDB struct { - db *levigo.DB - ro *levigo.ReadOptions - wo *levigo.WriteOptions - woSync *levigo.WriteOptions -} - -func NewCLevelDB(name string, dir string) (*CLevelDB, error) { - dbPath := filepath.Join(dir, name+".db") - - opts := levigo.NewOptions() - opts.SetCache(levigo.NewLRUCache(1 << 30)) - opts.SetCreateIfMissing(true) - db, err := levigo.Open(dbPath, opts) - if err != nil { - return nil, err - } - ro := levigo.NewReadOptions() - wo := levigo.NewWriteOptions() - woSync := levigo.NewWriteOptions() - woSync.SetSync(true) - database := &CLevelDB{ - db: db, - ro: ro, - wo: wo, - woSync: woSync, - } - return database, nil -} - -// Implements DB. -func (db *CLevelDB) Get(key []byte) []byte { - key = nonNilBytes(key) - res, err := db.db.Get(db.ro, key) - if err != nil { - panic(err) - } - return res -} - -// Implements DB. -func (db *CLevelDB) Has(key []byte) bool { - return db.Get(key) != nil -} - -// Implements DB. -func (db *CLevelDB) Set(key []byte, value []byte) { - key = nonNilBytes(key) - value = nonNilBytes(value) - err := db.db.Put(db.wo, key, value) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *CLevelDB) SetSync(key []byte, value []byte) { - key = nonNilBytes(key) - value = nonNilBytes(value) - err := db.db.Put(db.woSync, key, value) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *CLevelDB) Delete(key []byte) { - key = nonNilBytes(key) - err := db.db.Delete(db.wo, key) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *CLevelDB) DeleteSync(key []byte) { - key = nonNilBytes(key) - err := db.db.Delete(db.woSync, key) - if err != nil { - panic(err) - } -} - -func (db *CLevelDB) DB() *levigo.DB { - return db.db -} - -// Implements DB. -func (db *CLevelDB) Close() { - db.db.Close() - db.ro.Close() - db.wo.Close() - db.woSync.Close() -} - -// Implements DB. -func (db *CLevelDB) Print() { - itr := db.Iterator(nil, nil) - defer itr.Close() - for ; itr.Valid(); itr.Next() { - key := itr.Key() - value := itr.Value() - fmt.Printf("[%X]:\t[%X]\n", key, value) - } -} - -// Implements DB. -func (db *CLevelDB) Stats() map[string]string { - keys := []string{ - "leveldb.aliveiters", - "leveldb.alivesnaps", - "leveldb.blockpool", - "leveldb.cachedblock", - "leveldb.num-files-at-level{n}", - "leveldb.openedtables", - "leveldb.sstables", - "leveldb.stats", - } - - stats := make(map[string]string, len(keys)) - for _, key := range keys { - str := db.db.PropertyValue(key) - stats[key] = str - } - return stats -} - -//---------------------------------------- -// Batch - -// Implements DB. -func (db *CLevelDB) NewBatch() Batch { - batch := levigo.NewWriteBatch() - return &cLevelDBBatch{db, batch} -} - -type cLevelDBBatch struct { - db *CLevelDB - batch *levigo.WriteBatch -} - -// Implements Batch. -func (mBatch *cLevelDBBatch) Set(key, value []byte) { - mBatch.batch.Put(key, value) -} - -// Implements Batch. -func (mBatch *cLevelDBBatch) Delete(key []byte) { - mBatch.batch.Delete(key) -} - -// Implements Batch. -func (mBatch *cLevelDBBatch) Write() { - err := mBatch.db.db.Write(mBatch.db.wo, mBatch.batch) - if err != nil { - panic(err) - } -} - -// Implements Batch. -func (mBatch *cLevelDBBatch) WriteSync() { - err := mBatch.db.db.Write(mBatch.db.woSync, mBatch.batch) - if err != nil { - panic(err) - } -} - -// Implements Batch. -func (mBatch *cLevelDBBatch) Close() { - mBatch.batch.Close() -} - -//---------------------------------------- -// Iterator -// NOTE This is almost identical to db/go_level_db.Iterator -// Before creating a third version, refactor. - -func (db *CLevelDB) Iterator(start, end []byte) Iterator { - itr := db.db.NewIterator(db.ro) - return newCLevelDBIterator(itr, start, end, false) -} - -func (db *CLevelDB) ReverseIterator(start, end []byte) Iterator { - itr := db.db.NewIterator(db.ro) - return newCLevelDBIterator(itr, start, end, true) -} - -var _ Iterator = (*cLevelDBIterator)(nil) - -type cLevelDBIterator struct { - source *levigo.Iterator - start, end []byte - isReverse bool - isInvalid bool -} - -func newCLevelDBIterator(source *levigo.Iterator, start, end []byte, isReverse bool) *cLevelDBIterator { - if isReverse { - if end == nil { - source.SeekToLast() - } else { - source.Seek(end) - if source.Valid() { - eoakey := source.Key() // end or after key - if bytes.Compare(end, eoakey) <= 0 { - source.Prev() - } - } else { - source.SeekToLast() - } - } - } else { - if start == nil { - source.SeekToFirst() - } else { - source.Seek(start) - } - } - return &cLevelDBIterator{ - source: source, - start: start, - end: end, - isReverse: isReverse, - isInvalid: false, - } -} - -func (itr cLevelDBIterator) Domain() ([]byte, []byte) { - return itr.start, itr.end -} - -func (itr cLevelDBIterator) Valid() bool { - - // Once invalid, forever invalid. - if itr.isInvalid { - return false - } - - // Panic on DB error. No way to recover. - itr.assertNoError() - - // If source is invalid, invalid. - if !itr.source.Valid() { - itr.isInvalid = true - return false - } - - // If key is end or past it, invalid. - var start = itr.start - var end = itr.end - var key = itr.source.Key() - if itr.isReverse { - if start != nil && bytes.Compare(key, start) < 0 { - itr.isInvalid = true - return false - } - } else { - if end != nil && bytes.Compare(end, key) <= 0 { - itr.isInvalid = true - return false - } - } - - // It's valid. - return true -} - -func (itr cLevelDBIterator) Key() []byte { - itr.assertNoError() - itr.assertIsValid() - return itr.source.Key() -} - -func (itr cLevelDBIterator) Value() []byte { - itr.assertNoError() - itr.assertIsValid() - return itr.source.Value() -} - -func (itr cLevelDBIterator) Next() { - itr.assertNoError() - itr.assertIsValid() - if itr.isReverse { - itr.source.Prev() - } else { - itr.source.Next() - } -} - -func (itr cLevelDBIterator) Close() { - itr.source.Close() -} - -func (itr cLevelDBIterator) assertNoError() { - if err := itr.source.GetError(); err != nil { - panic(err) - } -} - -func (itr cLevelDBIterator) assertIsValid() { - if !itr.Valid() { - panic("cLevelDBIterator is invalid") - } -} diff --git a/libs/db/c_level_db_test.go b/libs/db/c_level_db_test.go deleted file mode 100644 index 1c10fcdef..000000000 --- a/libs/db/c_level_db_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// +build cleveldb - -package db - -import ( - "bytes" - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/assert" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -func BenchmarkRandomReadsWrites2(b *testing.B) { - b.StopTimer() - - numItems := int64(1000000) - internal := map[int64]int64{} - for i := 0; i < int(numItems); i++ { - internal[int64(i)] = int64(0) - } - db, err := NewCLevelDB(fmt.Sprintf("test_%x", cmn.RandStr(12)), "") - if err != nil { - b.Fatal(err.Error()) - return - } - - fmt.Println("ok, starting") - b.StartTimer() - - for i := 0; i < b.N; i++ { - // Write something - { - idx := (int64(cmn.RandInt()) % numItems) - internal[idx]++ - val := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := int642Bytes(int64(val)) - //fmt.Printf("Set %X -> %X\n", idxBytes, valBytes) - db.Set( - idxBytes, - valBytes, - ) - } - // Read something - { - idx := (int64(cmn.RandInt()) % numItems) - val := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := db.Get(idxBytes) - //fmt.Printf("Get %X -> %X\n", idxBytes, valBytes) - if val == 0 { - if !bytes.Equal(valBytes, nil) { - b.Errorf("Expected %v for %v, got %X", - nil, idx, valBytes) - break - } - } else { - if len(valBytes) != 8 { - b.Errorf("Expected length 8 for %v, got %X", - idx, valBytes) - break - } - valGot := bytes2Int64(valBytes) - if val != valGot { - b.Errorf("Expected %v for %v, got %v", - val, idx, valGot) - break - } - } - } - } - - db.Close() -} - -/* -func int642Bytes(i int64) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, uint64(i)) - return buf -} - -func bytes2Int64(buf []byte) int64 { - return int64(binary.BigEndian.Uint64(buf)) -} -*/ - -func TestCLevelDBBackend(t *testing.T) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - // Can't use "" (current directory) or "./" here because levigo.Open returns: - // "Error initializing DB: IO error: test_XXX.db: Invalid argument" - dir := os.TempDir() - db := NewDB(name, CLevelDBBackend, dir) - defer cleanupDBDir(dir, name) - - _, ok := db.(*CLevelDB) - assert.True(t, ok) -} - -func TestCLevelDBStats(t *testing.T) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - dir := os.TempDir() - db := NewDB(name, CLevelDBBackend, dir) - defer cleanupDBDir(dir, name) - - assert.NotEmpty(t, db.Stats()) -} diff --git a/libs/db/common_test.go b/libs/db/common_test.go deleted file mode 100644 index 64a86979c..000000000 --- a/libs/db/common_test.go +++ /dev/null @@ -1,256 +0,0 @@ -package db - -import ( - "bytes" - "encoding/binary" - "fmt" - "io/ioutil" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - cmn "github.com/tendermint/tendermint/libs/common" -) - -//---------------------------------------- -// Helper functions. - -func checkValue(t *testing.T, db DB, key []byte, valueWanted []byte) { - valueGot := db.Get(key) - assert.Equal(t, valueWanted, valueGot) -} - -func checkValid(t *testing.T, itr Iterator, expected bool) { - valid := itr.Valid() - require.Equal(t, expected, valid) -} - -func checkNext(t *testing.T, itr Iterator, expected bool) { - itr.Next() - valid := itr.Valid() - require.Equal(t, expected, valid) -} - -func checkNextPanics(t *testing.T, itr Iterator) { - assert.Panics(t, func() { itr.Next() }, "checkNextPanics expected panic but didn't") -} - -func checkDomain(t *testing.T, itr Iterator, start, end []byte) { - ds, de := itr.Domain() - assert.Equal(t, start, ds, "checkDomain domain start incorrect") - assert.Equal(t, end, de, "checkDomain domain end incorrect") -} - -func checkItem(t *testing.T, itr Iterator, key []byte, value []byte) { - k, v := itr.Key(), itr.Value() - assert.Exactly(t, key, k) - assert.Exactly(t, value, v) -} - -func checkInvalid(t *testing.T, itr Iterator) { - checkValid(t, itr, false) - checkKeyPanics(t, itr) - checkValuePanics(t, itr) - checkNextPanics(t, itr) -} - -func checkKeyPanics(t *testing.T, itr Iterator) { - assert.Panics(t, func() { itr.Key() }, "checkKeyPanics expected panic but didn't") -} - -func checkValuePanics(t *testing.T, itr Iterator) { - assert.Panics(t, func() { itr.Value() }, "checkValuePanics expected panic but didn't") -} - -func newTempDB(t *testing.T, backend DBBackendType) (db DB, dbDir string) { - dirname, err := ioutil.TempDir("", "db_common_test") - require.Nil(t, err) - return NewDB("testdb", backend, dirname), dirname -} - -//---------------------------------------- -// mockDB - -// NOTE: not actually goroutine safe. -// If you want something goroutine safe, maybe you just want a MemDB. -type mockDB struct { - mtx sync.Mutex - calls map[string]int -} - -func newMockDB() *mockDB { - return &mockDB{ - calls: make(map[string]int), - } -} - -func (mdb *mockDB) Mutex() *sync.Mutex { - return &(mdb.mtx) -} - -func (mdb *mockDB) Get([]byte) []byte { - mdb.calls["Get"]++ - return nil -} - -func (mdb *mockDB) Has([]byte) bool { - mdb.calls["Has"]++ - return false -} - -func (mdb *mockDB) Set([]byte, []byte) { - mdb.calls["Set"]++ -} - -func (mdb *mockDB) SetSync([]byte, []byte) { - mdb.calls["SetSync"]++ -} - -func (mdb *mockDB) SetNoLock([]byte, []byte) { - mdb.calls["SetNoLock"]++ -} - -func (mdb *mockDB) SetNoLockSync([]byte, []byte) { - mdb.calls["SetNoLockSync"]++ -} - -func (mdb *mockDB) Delete([]byte) { - mdb.calls["Delete"]++ -} - -func (mdb *mockDB) DeleteSync([]byte) { - mdb.calls["DeleteSync"]++ -} - -func (mdb *mockDB) DeleteNoLock([]byte) { - mdb.calls["DeleteNoLock"]++ -} - -func (mdb *mockDB) DeleteNoLockSync([]byte) { - mdb.calls["DeleteNoLockSync"]++ -} - -func (mdb *mockDB) Iterator(start, end []byte) Iterator { - mdb.calls["Iterator"]++ - return &mockIterator{} -} - -func (mdb *mockDB) ReverseIterator(start, end []byte) Iterator { - mdb.calls["ReverseIterator"]++ - return &mockIterator{} -} - -func (mdb *mockDB) Close() { - mdb.calls["Close"]++ -} - -func (mdb *mockDB) NewBatch() Batch { - mdb.calls["NewBatch"]++ - return &memBatch{db: mdb} -} - -func (mdb *mockDB) Print() { - mdb.calls["Print"]++ - fmt.Printf("mockDB{%v}", mdb.Stats()) -} - -func (mdb *mockDB) Stats() map[string]string { - mdb.calls["Stats"]++ - - res := make(map[string]string) - for key, count := range mdb.calls { - res[key] = fmt.Sprintf("%d", count) - } - return res -} - -//---------------------------------------- -// mockIterator - -type mockIterator struct{} - -func (mockIterator) Domain() (start []byte, end []byte) { - return nil, nil -} - -func (mockIterator) Valid() bool { - return false -} - -func (mockIterator) Next() { -} - -func (mockIterator) Key() []byte { - return nil -} - -func (mockIterator) Value() []byte { - return nil -} - -func (mockIterator) Close() { -} - -func benchmarkRandomReadsWrites(b *testing.B, db DB) { - b.StopTimer() - - // create dummy data - const numItems = int64(1000000) - internal := map[int64]int64{} - for i := 0; i < int(numItems); i++ { - internal[int64(i)] = int64(0) - } - - // fmt.Println("ok, starting") - b.StartTimer() - - for i := 0; i < b.N; i++ { - // Write something - { - idx := int64(cmn.RandInt()) % numItems - internal[idx]++ - val := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := int642Bytes(int64(val)) - //fmt.Printf("Set %X -> %X\n", idxBytes, valBytes) - db.Set(idxBytes, valBytes) - } - - // Read something - { - idx := int64(cmn.RandInt()) % numItems - valExp := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := db.Get(idxBytes) - //fmt.Printf("Get %X -> %X\n", idxBytes, valBytes) - if valExp == 0 { - if !bytes.Equal(valBytes, nil) { - b.Errorf("Expected %v for %v, got %X", nil, idx, valBytes) - break - } - } else { - if len(valBytes) != 8 { - b.Errorf("Expected length 8 for %v, got %X", idx, valBytes) - break - } - valGot := bytes2Int64(valBytes) - if valExp != valGot { - b.Errorf("Expected %v for %v, got %v", valExp, idx, valGot) - break - } - } - } - - } -} - -func int642Bytes(i int64) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, uint64(i)) - return buf -} - -func bytes2Int64(buf []byte) int64 { - return int64(binary.BigEndian.Uint64(buf)) -} diff --git a/libs/db/db.go b/libs/db/db.go deleted file mode 100644 index d88df398c..000000000 --- a/libs/db/db.go +++ /dev/null @@ -1,70 +0,0 @@ -package db - -import ( - "fmt" - "strings" -) - -type DBBackendType string - -// These are valid backend types. -const ( - // GoLevelDBBackend represents goleveldb (github.com/syndtr/goleveldb - most - // popular implementation) - // - pure go - // - stable - GoLevelDBBackend DBBackendType = "goleveldb" - // CLevelDBBackend represents cleveldb (uses levigo wrapper) - // - fast - // - requires gcc - // - use cleveldb build tag (go build -tags cleveldb) - CLevelDBBackend DBBackendType = "cleveldb" - // MemDBBackend represents in-memoty key value store, which is mostly used - // for testing. - MemDBBackend DBBackendType = "memdb" - // FSDBBackend represents filesystem database - // - EXPERIMENTAL - // - slow - FSDBBackend DBBackendType = "fsdb" - // BoltDBBackend represents bolt (uses etcd's fork of bolt - - // github.com/etcd-io/bbolt) - // - EXPERIMENTAL - // - may be faster is some use-cases (random reads - indexer) - // - use boltdb build tag (go build -tags boltdb) - BoltDBBackend DBBackendType = "boltdb" -) - -type dbCreator func(name string, dir string) (DB, error) - -var backends = map[DBBackendType]dbCreator{} - -func registerDBCreator(backend DBBackendType, creator dbCreator, force bool) { - _, ok := backends[backend] - if !force && ok { - return - } - backends[backend] = creator -} - -// NewDB creates a new database of type backend with the given name. -// NOTE: function panics if: -// - backend is unknown (not registered) -// - creator function, provided during registration, returns error -func NewDB(name string, backend DBBackendType, dir string) DB { - dbCreator, ok := backends[backend] - if !ok { - keys := make([]string, len(backends)) - i := 0 - for k := range backends { - keys[i] = string(k) - i++ - } - panic(fmt.Sprintf("Unknown db_backend %s, expected either %s", backend, strings.Join(keys, " or "))) - } - - db, err := dbCreator(name, dir) - if err != nil { - panic(fmt.Sprintf("Error initializing DB: %v", err)) - } - return db -} diff --git a/libs/db/db_test.go b/libs/db/db_test.go deleted file mode 100644 index 22b781f95..000000000 --- a/libs/db/db_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package db - -import ( - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDBIteratorSingleKey(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - db.SetSync(bz("1"), bz("value_1")) - itr := db.Iterator(nil, nil) - - checkValid(t, itr, true) - checkNext(t, itr, false) - checkValid(t, itr, false) - checkNextPanics(t, itr) - - // Once invalid... - checkInvalid(t, itr) - }) - } -} - -func TestDBIteratorTwoKeys(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - db.SetSync(bz("1"), bz("value_1")) - db.SetSync(bz("2"), bz("value_1")) - - { // Fail by calling Next too much - itr := db.Iterator(nil, nil) - checkValid(t, itr, true) - - checkNext(t, itr, true) - checkValid(t, itr, true) - - checkNext(t, itr, false) - checkValid(t, itr, false) - - checkNextPanics(t, itr) - - // Once invalid... - checkInvalid(t, itr) - } - }) - } -} - -func TestDBIteratorMany(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - keys := make([][]byte, 100) - for i := 0; i < 100; i++ { - keys[i] = []byte{byte(i)} - } - - value := []byte{5} - for _, k := range keys { - db.Set(k, value) - } - - itr := db.Iterator(nil, nil) - defer itr.Close() - for ; itr.Valid(); itr.Next() { - assert.Equal(t, db.Get(itr.Key()), itr.Value()) - } - }) - } -} - -func TestDBIteratorEmpty(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - itr := db.Iterator(nil, nil) - - checkInvalid(t, itr) - }) - } -} - -func TestDBIteratorEmptyBeginAfter(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - itr := db.Iterator(bz("1"), nil) - - checkInvalid(t, itr) - }) - } -} - -func TestDBIteratorNonemptyBeginAfter(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - db.SetSync(bz("1"), bz("value_1")) - itr := db.Iterator(bz("2"), nil) - - checkInvalid(t, itr) - }) - } -} - -func TestDBBatchWrite(t *testing.T) { - testCases := []struct { - modify func(batch Batch) - calls map[string]int - }{ - 0: { - func(batch Batch) { - batch.Set(bz("1"), bz("1")) - batch.Set(bz("2"), bz("2")) - batch.Delete(bz("3")) - batch.Set(bz("4"), bz("4")) - batch.Write() - }, - map[string]int{ - "Set": 0, "SetSync": 0, "SetNoLock": 3, "SetNoLockSync": 0, - "Delete": 0, "DeleteSync": 0, "DeleteNoLock": 1, "DeleteNoLockSync": 0, - }, - }, - 1: { - func(batch Batch) { - batch.Set(bz("1"), bz("1")) - batch.Set(bz("2"), bz("2")) - batch.Set(bz("4"), bz("4")) - batch.Delete(bz("3")) - batch.Write() - }, - map[string]int{ - "Set": 0, "SetSync": 0, "SetNoLock": 3, "SetNoLockSync": 0, - "Delete": 0, "DeleteSync": 0, "DeleteNoLock": 1, "DeleteNoLockSync": 0, - }, - }, - 2: { - func(batch Batch) { - batch.Set(bz("1"), bz("1")) - batch.Set(bz("2"), bz("2")) - batch.Delete(bz("3")) - batch.Set(bz("4"), bz("4")) - batch.WriteSync() - }, - map[string]int{ - "Set": 0, "SetSync": 0, "SetNoLock": 2, "SetNoLockSync": 1, - "Delete": 0, "DeleteSync": 0, "DeleteNoLock": 1, "DeleteNoLockSync": 0, - }, - }, - 3: { - func(batch Batch) { - batch.Set(bz("1"), bz("1")) - batch.Set(bz("2"), bz("2")) - batch.Set(bz("4"), bz("4")) - batch.Delete(bz("3")) - batch.WriteSync() - }, - map[string]int{ - "Set": 0, "SetSync": 0, "SetNoLock": 3, "SetNoLockSync": 0, - "Delete": 0, "DeleteSync": 0, "DeleteNoLock": 0, "DeleteNoLockSync": 1, - }, - }, - } - - for i, tc := range testCases { - mdb := newMockDB() - batch := mdb.NewBatch() - - tc.modify(batch) - - for call, exp := range tc.calls { - got := mdb.calls[call] - assert.Equal(t, exp, got, "#%v - key: %s", i, call) - } - } -} diff --git a/libs/db/fsdb.go b/libs/db/fsdb.go deleted file mode 100644 index ca8eefe94..000000000 --- a/libs/db/fsdb.go +++ /dev/null @@ -1,270 +0,0 @@ -package db - -import ( - "fmt" - "io/ioutil" - "net/url" - "os" - "path/filepath" - "sort" - "sync" - - "github.com/pkg/errors" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -const ( - keyPerm = os.FileMode(0600) - dirPerm = os.FileMode(0700) -) - -func init() { - registerDBCreator(FSDBBackend, func(name, dir string) (DB, error) { - dbPath := filepath.Join(dir, name+".db") - return NewFSDB(dbPath), nil - }, false) -} - -var _ DB = (*FSDB)(nil) - -// It's slow. -type FSDB struct { - mtx sync.Mutex - dir string -} - -func NewFSDB(dir string) *FSDB { - err := os.MkdirAll(dir, dirPerm) - if err != nil { - panic(errors.Wrap(err, "Creating FSDB dir "+dir)) - } - database := &FSDB{ - dir: dir, - } - return database -} - -func (db *FSDB) Get(key []byte) []byte { - db.mtx.Lock() - defer db.mtx.Unlock() - key = escapeKey(key) - - path := db.nameToPath(key) - value, err := read(path) - if os.IsNotExist(err) { - return nil - } else if err != nil { - panic(errors.Wrapf(err, "Getting key %s (0x%X)", string(key), key)) - } - return value -} - -func (db *FSDB) Has(key []byte) bool { - db.mtx.Lock() - defer db.mtx.Unlock() - key = escapeKey(key) - - path := db.nameToPath(key) - return cmn.FileExists(path) -} - -func (db *FSDB) Set(key []byte, value []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.SetNoLock(key, value) -} - -func (db *FSDB) SetSync(key []byte, value []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.SetNoLock(key, value) -} - -// NOTE: Implements atomicSetDeleter. -func (db *FSDB) SetNoLock(key []byte, value []byte) { - key = escapeKey(key) - value = nonNilBytes(value) - path := db.nameToPath(key) - err := write(path, value) - if err != nil { - panic(errors.Wrapf(err, "Setting key %s (0x%X)", string(key), key)) - } -} - -func (db *FSDB) Delete(key []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.DeleteNoLock(key) -} - -func (db *FSDB) DeleteSync(key []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.DeleteNoLock(key) -} - -// NOTE: Implements atomicSetDeleter. -func (db *FSDB) DeleteNoLock(key []byte) { - key = escapeKey(key) - path := db.nameToPath(key) - err := remove(path) - if os.IsNotExist(err) { - return - } else if err != nil { - panic(errors.Wrapf(err, "Removing key %s (0x%X)", string(key), key)) - } -} - -func (db *FSDB) Close() { - // Nothing to do. -} - -func (db *FSDB) Print() { - db.mtx.Lock() - defer db.mtx.Unlock() - - panic("FSDB.Print not yet implemented") -} - -func (db *FSDB) Stats() map[string]string { - db.mtx.Lock() - defer db.mtx.Unlock() - - panic("FSDB.Stats not yet implemented") -} - -func (db *FSDB) NewBatch() Batch { - db.mtx.Lock() - defer db.mtx.Unlock() - - // Not sure we would ever want to try... - // It doesn't seem easy for general filesystems. - panic("FSDB.NewBatch not yet implemented") -} - -func (db *FSDB) Mutex() *sync.Mutex { - return &(db.mtx) -} - -func (db *FSDB) Iterator(start, end []byte) Iterator { - return db.MakeIterator(start, end, false) -} - -func (db *FSDB) MakeIterator(start, end []byte, isReversed bool) Iterator { - db.mtx.Lock() - defer db.mtx.Unlock() - - // We need a copy of all of the keys. - // Not the best, but probably not a bottleneck depending. - keys, err := list(db.dir, start, end) - if err != nil { - panic(errors.Wrapf(err, "Listing keys in %s", db.dir)) - } - if isReversed { - sort.Sort(sort.Reverse(sort.StringSlice(keys))) - } else { - sort.Strings(keys) - } - return newMemDBIterator(db, keys, start, end) -} - -func (db *FSDB) ReverseIterator(start, end []byte) Iterator { - return db.MakeIterator(start, end, true) -} - -func (db *FSDB) nameToPath(name []byte) string { - n := url.PathEscape(string(name)) - return filepath.Join(db.dir, n) -} - -// Read some bytes to a file. -// CONTRACT: returns os errors directly without wrapping. -func read(path string) ([]byte, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - - d, err := ioutil.ReadAll(f) - if err != nil { - return nil, err - } - return d, nil -} - -// Write some bytes from a file. -// CONTRACT: returns os errors directly without wrapping. -func write(path string, d []byte) error { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, keyPerm) - if err != nil { - return err - } - defer f.Close() - // fInfo, err := f.Stat() - // if err != nil { - // return err - // } - // if fInfo.Mode() != keyPerm { - // return tmerrors.NewErrPermissionsChanged(f.Name(), keyPerm, fInfo.Mode()) - // } - _, err = f.Write(d) - if err != nil { - return err - } - err = f.Sync() - return err -} - -// Remove a file. -// CONTRACT: returns os errors directly without wrapping. -func remove(path string) error { - return os.Remove(path) -} - -// List keys in a directory, stripping of escape sequences and dir portions. -// CONTRACT: returns os errors directly without wrapping. -func list(dirPath string, start, end []byte) ([]string, error) { - dir, err := os.Open(dirPath) - if err != nil { - return nil, err - } - defer dir.Close() - - names, err := dir.Readdirnames(0) - if err != nil { - return nil, err - } - var keys []string - for _, name := range names { - n, err := url.PathUnescape(name) - if err != nil { - return nil, fmt.Errorf("Failed to unescape %s while listing", name) - } - key := unescapeKey([]byte(n)) - if IsKeyInDomain(key, start, end) { - keys = append(keys, string(key)) - } - } - return keys, nil -} - -// To support empty or nil keys, while the file system doesn't allow empty -// filenames. -func escapeKey(key []byte) []byte { - return []byte("k_" + string(key)) -} -func unescapeKey(escKey []byte) []byte { - if len(escKey) < 2 { - panic(fmt.Sprintf("Invalid esc key: %x", escKey)) - } - if string(escKey[:2]) != "k_" { - panic(fmt.Sprintf("Invalid esc key: %x", escKey)) - } - return escKey[2:] -} diff --git a/libs/db/go_level_db.go b/libs/db/go_level_db.go deleted file mode 100644 index 8c20ccdde..000000000 --- a/libs/db/go_level_db.go +++ /dev/null @@ -1,333 +0,0 @@ -package db - -import ( - "bytes" - "fmt" - "path/filepath" - - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/errors" - "github.com/syndtr/goleveldb/leveldb/iterator" - "github.com/syndtr/goleveldb/leveldb/opt" -) - -func init() { - dbCreator := func(name string, dir string) (DB, error) { - return NewGoLevelDB(name, dir) - } - registerDBCreator(GoLevelDBBackend, dbCreator, false) -} - -var _ DB = (*GoLevelDB)(nil) - -type GoLevelDB struct { - db *leveldb.DB -} - -func NewGoLevelDB(name string, dir string) (*GoLevelDB, error) { - return NewGoLevelDBWithOpts(name, dir, nil) -} - -func NewGoLevelDBWithOpts(name string, dir string, o *opt.Options) (*GoLevelDB, error) { - dbPath := filepath.Join(dir, name+".db") - db, err := leveldb.OpenFile(dbPath, o) - if err != nil { - return nil, err - } - database := &GoLevelDB{ - db: db, - } - return database, nil -} - -// Implements DB. -func (db *GoLevelDB) Get(key []byte) []byte { - key = nonNilBytes(key) - res, err := db.db.Get(key, nil) - if err != nil { - if err == errors.ErrNotFound { - return nil - } - panic(err) - } - return res -} - -// Implements DB. -func (db *GoLevelDB) Has(key []byte) bool { - return db.Get(key) != nil -} - -// Implements DB. -func (db *GoLevelDB) Set(key []byte, value []byte) { - key = nonNilBytes(key) - value = nonNilBytes(value) - err := db.db.Put(key, value, nil) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *GoLevelDB) SetSync(key []byte, value []byte) { - key = nonNilBytes(key) - value = nonNilBytes(value) - err := db.db.Put(key, value, &opt.WriteOptions{Sync: true}) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *GoLevelDB) Delete(key []byte) { - key = nonNilBytes(key) - err := db.db.Delete(key, nil) - if err != nil { - panic(err) - } -} - -// Implements DB. -func (db *GoLevelDB) DeleteSync(key []byte) { - key = nonNilBytes(key) - err := db.db.Delete(key, &opt.WriteOptions{Sync: true}) - if err != nil { - panic(err) - } -} - -func (db *GoLevelDB) DB() *leveldb.DB { - return db.db -} - -// Implements DB. -func (db *GoLevelDB) Close() { - db.db.Close() -} - -// Implements DB. -func (db *GoLevelDB) Print() { - str, _ := db.db.GetProperty("leveldb.stats") - fmt.Printf("%v\n", str) - - itr := db.db.NewIterator(nil, nil) - for itr.Next() { - key := itr.Key() - value := itr.Value() - fmt.Printf("[%X]:\t[%X]\n", key, value) - } -} - -// Implements DB. -func (db *GoLevelDB) Stats() map[string]string { - keys := []string{ - "leveldb.num-files-at-level{n}", - "leveldb.stats", - "leveldb.sstables", - "leveldb.blockpool", - "leveldb.cachedblock", - "leveldb.openedtables", - "leveldb.alivesnaps", - "leveldb.aliveiters", - } - - stats := make(map[string]string) - for _, key := range keys { - str, err := db.db.GetProperty(key) - if err == nil { - stats[key] = str - } - } - return stats -} - -//---------------------------------------- -// Batch - -// Implements DB. -func (db *GoLevelDB) NewBatch() Batch { - batch := new(leveldb.Batch) - return &goLevelDBBatch{db, batch} -} - -type goLevelDBBatch struct { - db *GoLevelDB - batch *leveldb.Batch -} - -// Implements Batch. -func (mBatch *goLevelDBBatch) Set(key, value []byte) { - mBatch.batch.Put(key, value) -} - -// Implements Batch. -func (mBatch *goLevelDBBatch) Delete(key []byte) { - mBatch.batch.Delete(key) -} - -// Implements Batch. -func (mBatch *goLevelDBBatch) Write() { - err := mBatch.db.db.Write(mBatch.batch, &opt.WriteOptions{Sync: false}) - if err != nil { - panic(err) - } -} - -// Implements Batch. -func (mBatch *goLevelDBBatch) WriteSync() { - err := mBatch.db.db.Write(mBatch.batch, &opt.WriteOptions{Sync: true}) - if err != nil { - panic(err) - } -} - -// Implements Batch. -// Close is no-op for goLevelDBBatch. -func (mBatch *goLevelDBBatch) Close() {} - -//---------------------------------------- -// Iterator -// NOTE This is almost identical to db/c_level_db.Iterator -// Before creating a third version, refactor. - -// Implements DB. -func (db *GoLevelDB) Iterator(start, end []byte) Iterator { - itr := db.db.NewIterator(nil, nil) - return newGoLevelDBIterator(itr, start, end, false) -} - -// Implements DB. -func (db *GoLevelDB) ReverseIterator(start, end []byte) Iterator { - itr := db.db.NewIterator(nil, nil) - return newGoLevelDBIterator(itr, start, end, true) -} - -type goLevelDBIterator struct { - source iterator.Iterator - start []byte - end []byte - isReverse bool - isInvalid bool -} - -var _ Iterator = (*goLevelDBIterator)(nil) - -func newGoLevelDBIterator(source iterator.Iterator, start, end []byte, isReverse bool) *goLevelDBIterator { - if isReverse { - if end == nil { - source.Last() - } else { - valid := source.Seek(end) - if valid { - eoakey := source.Key() // end or after key - if bytes.Compare(end, eoakey) <= 0 { - source.Prev() - } - } else { - source.Last() - } - } - } else { - if start == nil { - source.First() - } else { - source.Seek(start) - } - } - return &goLevelDBIterator{ - source: source, - start: start, - end: end, - isReverse: isReverse, - isInvalid: false, - } -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Domain() ([]byte, []byte) { - return itr.start, itr.end -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Valid() bool { - - // Once invalid, forever invalid. - if itr.isInvalid { - return false - } - - // Panic on DB error. No way to recover. - itr.assertNoError() - - // If source is invalid, invalid. - if !itr.source.Valid() { - itr.isInvalid = true - return false - } - - // If key is end or past it, invalid. - var start = itr.start - var end = itr.end - var key = itr.source.Key() - - if itr.isReverse { - if start != nil && bytes.Compare(key, start) < 0 { - itr.isInvalid = true - return false - } - } else { - if end != nil && bytes.Compare(end, key) <= 0 { - itr.isInvalid = true - return false - } - } - - // Valid - return true -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Key() []byte { - // Key returns a copy of the current key. - // See https://github.com/syndtr/goleveldb/blob/52c212e6c196a1404ea59592d3f1c227c9f034b2/leveldb/iterator/iter.go#L88 - itr.assertNoError() - itr.assertIsValid() - return cp(itr.source.Key()) -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Value() []byte { - // Value returns a copy of the current value. - // See https://github.com/syndtr/goleveldb/blob/52c212e6c196a1404ea59592d3f1c227c9f034b2/leveldb/iterator/iter.go#L88 - itr.assertNoError() - itr.assertIsValid() - return cp(itr.source.Value()) -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Next() { - itr.assertNoError() - itr.assertIsValid() - if itr.isReverse { - itr.source.Prev() - } else { - itr.source.Next() - } -} - -// Implements Iterator. -func (itr *goLevelDBIterator) Close() { - itr.source.Release() -} - -func (itr *goLevelDBIterator) assertNoError() { - if err := itr.source.Error(); err != nil { - panic(err) - } -} - -func (itr goLevelDBIterator) assertIsValid() { - if !itr.Valid() { - panic("goLevelDBIterator is invalid") - } -} diff --git a/libs/db/go_level_db_test.go b/libs/db/go_level_db_test.go deleted file mode 100644 index f781a2b3d..000000000 --- a/libs/db/go_level_db_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package db - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - "github.com/syndtr/goleveldb/leveldb/opt" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -func TestGoLevelDBNewGoLevelDB(t *testing.T) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - defer cleanupDBDir("", name) - - // Test we can't open the db twice for writing - wr1, err := NewGoLevelDB(name, "") - require.Nil(t, err) - _, err = NewGoLevelDB(name, "") - require.NotNil(t, err) - wr1.Close() // Close the db to release the lock - - // Test we can open the db twice for reading only - ro1, err := NewGoLevelDBWithOpts(name, "", &opt.Options{ReadOnly: true}) - defer ro1.Close() - require.Nil(t, err) - ro2, err := NewGoLevelDBWithOpts(name, "", &opt.Options{ReadOnly: true}) - defer ro2.Close() - require.Nil(t, err) -} - -func BenchmarkGoLevelDBRandomReadsWrites(b *testing.B) { - name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - db, err := NewGoLevelDB(name, "") - if err != nil { - b.Fatal(err) - } - defer func() { - db.Close() - cleanupDBDir("", name) - }() - - benchmarkRandomReadsWrites(b, db) -} diff --git a/libs/db/mem_batch.go b/libs/db/mem_batch.go deleted file mode 100644 index 2ce765786..000000000 --- a/libs/db/mem_batch.go +++ /dev/null @@ -1,74 +0,0 @@ -package db - -import "sync" - -type atomicSetDeleter interface { - Mutex() *sync.Mutex - SetNoLock(key, value []byte) - SetNoLockSync(key, value []byte) - DeleteNoLock(key []byte) - DeleteNoLockSync(key []byte) -} - -type memBatch struct { - db atomicSetDeleter - ops []operation -} - -type opType int - -const ( - opTypeSet opType = 1 - opTypeDelete opType = 2 -) - -type operation struct { - opType - key []byte - value []byte -} - -func (mBatch *memBatch) Set(key, value []byte) { - mBatch.ops = append(mBatch.ops, operation{opTypeSet, key, value}) -} - -func (mBatch *memBatch) Delete(key []byte) { - mBatch.ops = append(mBatch.ops, operation{opTypeDelete, key, nil}) -} - -func (mBatch *memBatch) Write() { - mBatch.write(false) -} - -func (mBatch *memBatch) WriteSync() { - mBatch.write(true) -} - -func (mBatch *memBatch) Close() { - mBatch.ops = nil -} - -func (mBatch *memBatch) write(doSync bool) { - if mtx := mBatch.db.Mutex(); mtx != nil { - mtx.Lock() - defer mtx.Unlock() - } - - for i, op := range mBatch.ops { - if doSync && i == (len(mBatch.ops)-1) { - switch op.opType { - case opTypeSet: - mBatch.db.SetNoLockSync(op.key, op.value) - case opTypeDelete: - mBatch.db.DeleteNoLockSync(op.key) - } - break // we're done. - } - switch op.opType { - case opTypeSet: - mBatch.db.SetNoLock(op.key, op.value) - case opTypeDelete: - mBatch.db.DeleteNoLock(op.key) - } - } -} diff --git a/libs/db/mem_db.go b/libs/db/mem_db.go deleted file mode 100644 index fc567577d..000000000 --- a/libs/db/mem_db.go +++ /dev/null @@ -1,255 +0,0 @@ -package db - -import ( - "fmt" - "sort" - "sync" -) - -func init() { - registerDBCreator(MemDBBackend, func(name, dir string) (DB, error) { - return NewMemDB(), nil - }, false) -} - -var _ DB = (*MemDB)(nil) - -type MemDB struct { - mtx sync.Mutex - db map[string][]byte -} - -func NewMemDB() *MemDB { - database := &MemDB{ - db: make(map[string][]byte), - } - return database -} - -// Implements atomicSetDeleter. -func (db *MemDB) Mutex() *sync.Mutex { - return &(db.mtx) -} - -// Implements DB. -func (db *MemDB) Get(key []byte) []byte { - db.mtx.Lock() - defer db.mtx.Unlock() - key = nonNilBytes(key) - - value := db.db[string(key)] - return value -} - -// Implements DB. -func (db *MemDB) Has(key []byte) bool { - db.mtx.Lock() - defer db.mtx.Unlock() - key = nonNilBytes(key) - - _, ok := db.db[string(key)] - return ok -} - -// Implements DB. -func (db *MemDB) Set(key []byte, value []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.SetNoLock(key, value) -} - -// Implements DB. -func (db *MemDB) SetSync(key []byte, value []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.SetNoLock(key, value) -} - -// Implements atomicSetDeleter. -func (db *MemDB) SetNoLock(key []byte, value []byte) { - db.SetNoLockSync(key, value) -} - -// Implements atomicSetDeleter. -func (db *MemDB) SetNoLockSync(key []byte, value []byte) { - key = nonNilBytes(key) - value = nonNilBytes(value) - - db.db[string(key)] = value -} - -// Implements DB. -func (db *MemDB) Delete(key []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.DeleteNoLock(key) -} - -// Implements DB. -func (db *MemDB) DeleteSync(key []byte) { - db.mtx.Lock() - defer db.mtx.Unlock() - - db.DeleteNoLock(key) -} - -// Implements atomicSetDeleter. -func (db *MemDB) DeleteNoLock(key []byte) { - db.DeleteNoLockSync(key) -} - -// Implements atomicSetDeleter. -func (db *MemDB) DeleteNoLockSync(key []byte) { - key = nonNilBytes(key) - - delete(db.db, string(key)) -} - -// Implements DB. -func (db *MemDB) Close() { - // Close is a noop since for an in-memory - // database, we don't have a destination - // to flush contents to nor do we want - // any data loss on invoking Close() - // See the discussion in https://github.com/tendermint/tendermint/libs/pull/56 -} - -// Implements DB. -func (db *MemDB) Print() { - db.mtx.Lock() - defer db.mtx.Unlock() - - for key, value := range db.db { - fmt.Printf("[%X]:\t[%X]\n", []byte(key), value) - } -} - -// Implements DB. -func (db *MemDB) Stats() map[string]string { - db.mtx.Lock() - defer db.mtx.Unlock() - - stats := make(map[string]string) - stats["database.type"] = "memDB" - stats["database.size"] = fmt.Sprintf("%d", len(db.db)) - return stats -} - -// Implements DB. -func (db *MemDB) NewBatch() Batch { - db.mtx.Lock() - defer db.mtx.Unlock() - - return &memBatch{db, nil} -} - -//---------------------------------------- -// Iterator - -// Implements DB. -func (db *MemDB) Iterator(start, end []byte) Iterator { - db.mtx.Lock() - defer db.mtx.Unlock() - - keys := db.getSortedKeys(start, end, false) - return newMemDBIterator(db, keys, start, end) -} - -// Implements DB. -func (db *MemDB) ReverseIterator(start, end []byte) Iterator { - db.mtx.Lock() - defer db.mtx.Unlock() - - keys := db.getSortedKeys(start, end, true) - return newMemDBIterator(db, keys, start, end) -} - -// We need a copy of all of the keys. -// Not the best, but probably not a bottleneck depending. -type memDBIterator struct { - db DB - cur int - keys []string - start []byte - end []byte -} - -var _ Iterator = (*memDBIterator)(nil) - -// Keys is expected to be in reverse order for reverse iterators. -func newMemDBIterator(db DB, keys []string, start, end []byte) *memDBIterator { - return &memDBIterator{ - db: db, - cur: 0, - keys: keys, - start: start, - end: end, - } -} - -// Implements Iterator. -func (itr *memDBIterator) Domain() ([]byte, []byte) { - return itr.start, itr.end -} - -// Implements Iterator. -func (itr *memDBIterator) Valid() bool { - return 0 <= itr.cur && itr.cur < len(itr.keys) -} - -// Implements Iterator. -func (itr *memDBIterator) Next() { - itr.assertIsValid() - itr.cur++ -} - -// Implements Iterator. -func (itr *memDBIterator) Key() []byte { - itr.assertIsValid() - return []byte(itr.keys[itr.cur]) -} - -// Implements Iterator. -func (itr *memDBIterator) Value() []byte { - itr.assertIsValid() - key := []byte(itr.keys[itr.cur]) - return itr.db.Get(key) -} - -// Implements Iterator. -func (itr *memDBIterator) Close() { - itr.keys = nil - itr.db = nil -} - -func (itr *memDBIterator) assertIsValid() { - if !itr.Valid() { - panic("memDBIterator is invalid") - } -} - -//---------------------------------------- -// Misc. - -func (db *MemDB) getSortedKeys(start, end []byte, reverse bool) []string { - keys := []string{} - for key := range db.db { - inDomain := IsKeyInDomain([]byte(key), start, end) - if inDomain { - keys = append(keys, key) - } - } - sort.Strings(keys) - if reverse { - nkeys := len(keys) - for i := 0; i < nkeys/2; i++ { - temp := keys[i] - keys[i] = keys[nkeys-i-1] - keys[nkeys-i-1] = temp - } - } - return keys -} diff --git a/libs/db/prefix_db.go b/libs/db/prefix_db.go deleted file mode 100644 index 0dd06ef9d..000000000 --- a/libs/db/prefix_db.go +++ /dev/null @@ -1,336 +0,0 @@ -package db - -import ( - "bytes" - "fmt" - "sync" -) - -// IteratePrefix is a convenience function for iterating over a key domain -// restricted by prefix. -func IteratePrefix(db DB, prefix []byte) Iterator { - var start, end []byte - if len(prefix) == 0 { - start = nil - end = nil - } else { - start = cp(prefix) - end = cpIncr(prefix) - } - return db.Iterator(start, end) -} - -/* -TODO: Make test, maybe rename. -// Like IteratePrefix but the iterator strips the prefix from the keys. -func IteratePrefixStripped(db DB, prefix []byte) Iterator { - start, end := ... - return newPrefixIterator(prefix, start, end, IteratePrefix(db, prefix)) -} -*/ - -//---------------------------------------- -// prefixDB - -type prefixDB struct { - mtx sync.Mutex - prefix []byte - db DB -} - -// NewPrefixDB lets you namespace multiple DBs within a single DB. -func NewPrefixDB(db DB, prefix []byte) *prefixDB { - return &prefixDB{ - prefix: prefix, - db: db, - } -} - -// Implements atomicSetDeleter. -func (pdb *prefixDB) Mutex() *sync.Mutex { - return &(pdb.mtx) -} - -// Implements DB. -func (pdb *prefixDB) Get(key []byte) []byte { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pkey := pdb.prefixed(key) - value := pdb.db.Get(pkey) - return value -} - -// Implements DB. -func (pdb *prefixDB) Has(key []byte) bool { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - return pdb.db.Has(pdb.prefixed(key)) -} - -// Implements DB. -func (pdb *prefixDB) Set(key []byte, value []byte) { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pkey := pdb.prefixed(key) - pdb.db.Set(pkey, value) -} - -// Implements DB. -func (pdb *prefixDB) SetSync(key []byte, value []byte) { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pdb.db.SetSync(pdb.prefixed(key), value) -} - -// Implements DB. -func (pdb *prefixDB) Delete(key []byte) { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pdb.db.Delete(pdb.prefixed(key)) -} - -// Implements DB. -func (pdb *prefixDB) DeleteSync(key []byte) { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pdb.db.DeleteSync(pdb.prefixed(key)) -} - -// Implements DB. -func (pdb *prefixDB) Iterator(start, end []byte) Iterator { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - var pstart, pend []byte - pstart = append(cp(pdb.prefix), start...) - if end == nil { - pend = cpIncr(pdb.prefix) - } else { - pend = append(cp(pdb.prefix), end...) - } - return newPrefixIterator( - pdb.prefix, - start, - end, - pdb.db.Iterator( - pstart, - pend, - ), - ) -} - -// Implements DB. -func (pdb *prefixDB) ReverseIterator(start, end []byte) Iterator { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - var pstart, pend []byte - pstart = append(cp(pdb.prefix), start...) - if end == nil { - pend = cpIncr(pdb.prefix) - } else { - pend = append(cp(pdb.prefix), end...) - } - ritr := pdb.db.ReverseIterator(pstart, pend) - return newPrefixIterator( - pdb.prefix, - start, - end, - ritr, - ) -} - -// Implements DB. -// Panics if the underlying DB is not an -// atomicSetDeleter. -func (pdb *prefixDB) NewBatch() Batch { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - return newPrefixBatch(pdb.prefix, pdb.db.NewBatch()) -} - -/* NOTE: Uncomment to use memBatch instead of prefixBatch -// Implements atomicSetDeleter. -func (pdb *prefixDB) SetNoLock(key []byte, value []byte) { - pdb.db.(atomicSetDeleter).SetNoLock(pdb.prefixed(key), value) -} - -// Implements atomicSetDeleter. -func (pdb *prefixDB) SetNoLockSync(key []byte, value []byte) { - pdb.db.(atomicSetDeleter).SetNoLockSync(pdb.prefixed(key), value) -} - -// Implements atomicSetDeleter. -func (pdb *prefixDB) DeleteNoLock(key []byte) { - pdb.db.(atomicSetDeleter).DeleteNoLock(pdb.prefixed(key)) -} - -// Implements atomicSetDeleter. -func (pdb *prefixDB) DeleteNoLockSync(key []byte) { - pdb.db.(atomicSetDeleter).DeleteNoLockSync(pdb.prefixed(key)) -} -*/ - -// Implements DB. -func (pdb *prefixDB) Close() { - pdb.mtx.Lock() - defer pdb.mtx.Unlock() - - pdb.db.Close() -} - -// Implements DB. -func (pdb *prefixDB) Print() { - fmt.Printf("prefix: %X\n", pdb.prefix) - - itr := pdb.Iterator(nil, nil) - defer itr.Close() - for ; itr.Valid(); itr.Next() { - key := itr.Key() - value := itr.Value() - fmt.Printf("[%X]:\t[%X]\n", key, value) - } -} - -// Implements DB. -func (pdb *prefixDB) Stats() map[string]string { - stats := make(map[string]string) - stats["prefixdb.prefix.string"] = string(pdb.prefix) - stats["prefixdb.prefix.hex"] = fmt.Sprintf("%X", pdb.prefix) - source := pdb.db.Stats() - for key, value := range source { - stats["prefixdb.source."+key] = value - } - return stats -} - -func (pdb *prefixDB) prefixed(key []byte) []byte { - return append(cp(pdb.prefix), key...) -} - -//---------------------------------------- -// prefixBatch - -type prefixBatch struct { - prefix []byte - source Batch -} - -func newPrefixBatch(prefix []byte, source Batch) prefixBatch { - return prefixBatch{ - prefix: prefix, - source: source, - } -} - -func (pb prefixBatch) Set(key, value []byte) { - pkey := append(cp(pb.prefix), key...) - pb.source.Set(pkey, value) -} - -func (pb prefixBatch) Delete(key []byte) { - pkey := append(cp(pb.prefix), key...) - pb.source.Delete(pkey) -} - -func (pb prefixBatch) Write() { - pb.source.Write() -} - -func (pb prefixBatch) WriteSync() { - pb.source.WriteSync() -} - -func (pb prefixBatch) Close() { - pb.source.Close() -} - -//---------------------------------------- -// prefixIterator - -var _ Iterator = (*prefixIterator)(nil) - -// Strips prefix while iterating from Iterator. -type prefixIterator struct { - prefix []byte - start []byte - end []byte - source Iterator - valid bool -} - -func newPrefixIterator(prefix, start, end []byte, source Iterator) *prefixIterator { - if !source.Valid() || !bytes.HasPrefix(source.Key(), prefix) { - return &prefixIterator{ - prefix: prefix, - start: start, - end: end, - source: source, - valid: false, - } - } else { - return &prefixIterator{ - prefix: prefix, - start: start, - end: end, - source: source, - valid: true, - } - } -} - -func (itr *prefixIterator) Domain() (start []byte, end []byte) { - return itr.start, itr.end -} - -func (itr *prefixIterator) Valid() bool { - return itr.valid && itr.source.Valid() -} - -func (itr *prefixIterator) Next() { - if !itr.valid { - panic("prefixIterator invalid, cannot call Next()") - } - itr.source.Next() - if !itr.source.Valid() || !bytes.HasPrefix(itr.source.Key(), itr.prefix) { - itr.valid = false - return - } -} - -func (itr *prefixIterator) Key() (key []byte) { - if !itr.valid { - panic("prefixIterator invalid, cannot call Key()") - } - return stripPrefix(itr.source.Key(), itr.prefix) -} - -func (itr *prefixIterator) Value() (value []byte) { - if !itr.valid { - panic("prefixIterator invalid, cannot call Value()") - } - return itr.source.Value() -} - -func (itr *prefixIterator) Close() { - itr.source.Close() -} - -//---------------------------------------- - -func stripPrefix(key []byte, prefix []byte) (stripped []byte) { - if len(key) < len(prefix) { - panic("should not happen") - } - if !bytes.Equal(key[:len(prefix)], prefix) { - panic("should not happne") - } - return key[len(prefix):] -} diff --git a/libs/db/prefix_db_test.go b/libs/db/prefix_db_test.go deleted file mode 100644 index e3e37c7d1..000000000 --- a/libs/db/prefix_db_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package db - -import "testing" - -func mockDBWithStuff() DB { - db := NewMemDB() - // Under "key" prefix - db.Set(bz("key"), bz("value")) - db.Set(bz("key1"), bz("value1")) - db.Set(bz("key2"), bz("value2")) - db.Set(bz("key3"), bz("value3")) - db.Set(bz("something"), bz("else")) - db.Set(bz(""), bz("")) - db.Set(bz("k"), bz("val")) - db.Set(bz("ke"), bz("valu")) - db.Set(bz("kee"), bz("valuu")) - return db -} - -func TestPrefixDBSimple(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - checkValue(t, pdb, bz("key"), nil) - checkValue(t, pdb, bz(""), bz("value")) - checkValue(t, pdb, bz("key1"), nil) - checkValue(t, pdb, bz("1"), bz("value1")) - checkValue(t, pdb, bz("key2"), nil) - checkValue(t, pdb, bz("2"), bz("value2")) - checkValue(t, pdb, bz("key3"), nil) - checkValue(t, pdb, bz("3"), bz("value3")) - checkValue(t, pdb, bz("something"), nil) - checkValue(t, pdb, bz("k"), nil) - checkValue(t, pdb, bz("ke"), nil) - checkValue(t, pdb, bz("kee"), nil) -} - -func TestPrefixDBIterator1(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.Iterator(nil, nil) - checkDomain(t, itr, nil, nil) - checkItem(t, itr, bz(""), bz("value")) - checkNext(t, itr, true) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, true) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBIterator2(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.Iterator(nil, bz("")) - checkDomain(t, itr, nil, bz("")) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBIterator3(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.Iterator(bz(""), nil) - checkDomain(t, itr, bz(""), nil) - checkItem(t, itr, bz(""), bz("value")) - checkNext(t, itr, true) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, true) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBIterator4(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.Iterator(bz(""), bz("")) - checkDomain(t, itr, bz(""), bz("")) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator1(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(nil, nil) - checkDomain(t, itr, nil, nil) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, true) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, true) - checkItem(t, itr, bz(""), bz("value")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator2(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(bz(""), nil) - checkDomain(t, itr, bz(""), nil) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, true) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, true) - checkItem(t, itr, bz(""), bz("value")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator3(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(nil, bz("")) - checkDomain(t, itr, nil, bz("")) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator4(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(bz(""), bz("")) - checkDomain(t, itr, bz(""), bz("")) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator5(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(bz("1"), nil) - checkDomain(t, itr, bz("1"), nil) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, true) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator6(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(bz("2"), nil) - checkDomain(t, itr, bz("2"), nil) - checkItem(t, itr, bz("3"), bz("value3")) - checkNext(t, itr, true) - checkItem(t, itr, bz("2"), bz("value2")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} - -func TestPrefixDBReverseIterator7(t *testing.T) { - db := mockDBWithStuff() - pdb := NewPrefixDB(db, bz("key")) - - itr := pdb.ReverseIterator(nil, bz("2")) - checkDomain(t, itr, nil, bz("2")) - checkItem(t, itr, bz("1"), bz("value1")) - checkNext(t, itr, true) - checkItem(t, itr, bz(""), bz("value")) - checkNext(t, itr, false) - checkInvalid(t, itr) - itr.Close() -} diff --git a/libs/db/remotedb/doc.go b/libs/db/remotedb/doc.go deleted file mode 100644 index 93d9c8a29..000000000 --- a/libs/db/remotedb/doc.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -remotedb is a package for connecting to distributed Tendermint db.DB -instances. The purpose is to detach difficult deployments such as -CLevelDB that requires gcc or perhaps for databases that require -custom configurations such as extra disk space. It also eases -the burden and cost of deployment of dependencies for databases -to be used by Tendermint developers. Most importantly it is built -over the high performant gRPC transport. - -remotedb's RemoteDB implements db.DB so can be used normally -like other databases. One just has to explicitly connect to the -remote database with a client setup such as: - - client, err := remotedb.NewRemoteDB(addr, cert) - // Make sure to invoke InitRemote! - if err := client.InitRemote(&remotedb.Init{Name: "test-remote-db", Type: "leveldb"}); err != nil { - log.Fatalf("Failed to initialize the remote db") - } - - client.Set(key1, value) - gv1 := client.SetSync(k2, v2) - - client.Delete(k1) - gv2 := client.Get(k1) - - for itr := client.Iterator(k1, k9); itr.Valid(); itr.Next() { - ik, iv := itr.Key(), itr.Value() - ds, de := itr.Domain() - } - - stats := client.Stats() - - if !client.Has(dk1) { - client.SetSync(dk1, dv1) - } -*/ -package remotedb diff --git a/libs/db/remotedb/grpcdb/client.go b/libs/db/remotedb/grpcdb/client.go deleted file mode 100644 index b3c69ff23..000000000 --- a/libs/db/remotedb/grpcdb/client.go +++ /dev/null @@ -1,22 +0,0 @@ -package grpcdb - -import ( - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - - protodb "github.com/tendermint/tendermint/libs/db/remotedb/proto" -) - -// NewClient creates a gRPC client connected to the bound gRPC server at serverAddr. -// Use kind to set the level of security to either Secure or Insecure. -func NewClient(serverAddr, serverCert string) (protodb.DBClient, error) { - creds, err := credentials.NewClientTLSFromFile(serverCert, "") - if err != nil { - return nil, err - } - cc, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(creds)) - if err != nil { - return nil, err - } - return protodb.NewDBClient(cc), nil -} diff --git a/libs/db/remotedb/grpcdb/doc.go b/libs/db/remotedb/grpcdb/doc.go deleted file mode 100644 index 0d8e380ce..000000000 --- a/libs/db/remotedb/grpcdb/doc.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -grpcdb is the distribution of Tendermint's db.DB instances using -the gRPC transport to decouple local db.DB usages from applications, -to using them over a network in a highly performant manner. - -grpcdb allows users to initialize a database's server like -they would locally and invoke the respective methods of db.DB. - -Most users shouldn't use this package, but should instead use -remotedb. Only the lower level users and database server deployers -should use it, for functionality such as: - - ln, err := net.Listen("tcp", "0.0.0.0:0") - srv := grpcdb.NewServer() - defer srv.Stop() - go func() { - if err := srv.Serve(ln); err != nil { - t.Fatalf("BindServer: %v", err) - } - }() - -or - addr := ":8998" - cert := "server.crt" - key := "server.key" - go func() { - if err := grpcdb.ListenAndServe(addr, cert, key); err != nil { - log.Fatalf("BindServer: %v", err) - } - }() -*/ -package grpcdb diff --git a/libs/db/remotedb/grpcdb/example_test.go b/libs/db/remotedb/grpcdb/example_test.go deleted file mode 100644 index eba0d6914..000000000 --- a/libs/db/remotedb/grpcdb/example_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package grpcdb_test - -import ( - "bytes" - "context" - "log" - - grpcdb "github.com/tendermint/tendermint/libs/db/remotedb/grpcdb" - protodb "github.com/tendermint/tendermint/libs/db/remotedb/proto" -) - -func Example() { - addr := ":8998" - cert := "server.crt" - key := "server.key" - go func() { - if err := grpcdb.ListenAndServe(addr, cert, key); err != nil { - log.Fatalf("BindServer: %v", err) - } - }() - - client, err := grpcdb.NewClient(addr, cert) - if err != nil { - log.Fatalf("Failed to create grpcDB client: %v", err) - } - - ctx := context.Background() - // 1. Initialize the DB - in := &protodb.Init{ - Type: "leveldb", - Name: "grpc-uno-test", - Dir: ".", - } - if _, err := client.Init(ctx, in); err != nil { - log.Fatalf("Init error: %v", err) - } - - // 2. Now it can be used! - query1 := &protodb.Entity{Key: []byte("Project"), Value: []byte("Tmlibs-on-gRPC")} - if _, err := client.SetSync(ctx, query1); err != nil { - log.Fatalf("SetSync err: %v", err) - } - - query2 := &protodb.Entity{Key: []byte("Project")} - read, err := client.Get(ctx, query2) - if err != nil { - log.Fatalf("Get err: %v", err) - } - if g, w := read.Value, []byte("Tmlibs-on-gRPC"); !bytes.Equal(g, w) { - log.Fatalf("got= (%q ==> % X)\nwant=(%q ==> % X)", g, g, w, w) - } -} diff --git a/libs/db/remotedb/grpcdb/server.go b/libs/db/remotedb/grpcdb/server.go deleted file mode 100644 index a032292b3..000000000 --- a/libs/db/remotedb/grpcdb/server.go +++ /dev/null @@ -1,200 +0,0 @@ -package grpcdb - -import ( - "context" - "net" - "sync" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - - "github.com/tendermint/tendermint/libs/db" - protodb "github.com/tendermint/tendermint/libs/db/remotedb/proto" -) - -// ListenAndServe is a blocking function that sets up a gRPC based -// server at the address supplied, with the gRPC options passed in. -// Normally in usage, invoke it in a goroutine like you would for http.ListenAndServe. -func ListenAndServe(addr, cert, key string, opts ...grpc.ServerOption) error { - ln, err := net.Listen("tcp", addr) - if err != nil { - return err - } - srv, err := NewServer(cert, key, opts...) - if err != nil { - return err - } - return srv.Serve(ln) -} - -func NewServer(cert, key string, opts ...grpc.ServerOption) (*grpc.Server, error) { - creds, err := credentials.NewServerTLSFromFile(cert, key) - if err != nil { - return nil, err - } - opts = append(opts, grpc.Creds(creds)) - srv := grpc.NewServer(opts...) - protodb.RegisterDBServer(srv, new(server)) - return srv, nil -} - -type server struct { - mu sync.Mutex - db db.DB -} - -var _ protodb.DBServer = (*server)(nil) - -// Init initializes the server's database. Only one type of database -// can be initialized per server. -// -// Dir is the directory on the file system in which the DB will be stored(if backed by disk) (TODO: remove) -// -// Name is representative filesystem entry's basepath -// -// Type can be either one of: -// * cleveldb (if built with gcc enabled) -// * fsdb -// * memdB -// * leveldb -// See https://godoc.org/github.com/tendermint/tendermint/libs/db#DBBackendType -func (s *server) Init(ctx context.Context, in *protodb.Init) (*protodb.Entity, error) { - s.mu.Lock() - defer s.mu.Unlock() - - s.db = db.NewDB(in.Name, db.DBBackendType(in.Type), in.Dir) - return &protodb.Entity{CreatedAt: time.Now().Unix()}, nil -} - -func (s *server) Delete(ctx context.Context, in *protodb.Entity) (*protodb.Nothing, error) { - s.db.Delete(in.Key) - return nothing, nil -} - -var nothing = new(protodb.Nothing) - -func (s *server) DeleteSync(ctx context.Context, in *protodb.Entity) (*protodb.Nothing, error) { - s.db.DeleteSync(in.Key) - return nothing, nil -} - -func (s *server) Get(ctx context.Context, in *protodb.Entity) (*protodb.Entity, error) { - value := s.db.Get(in.Key) - return &protodb.Entity{Value: value}, nil -} - -func (s *server) GetStream(ds protodb.DB_GetStreamServer) error { - // Receive routine - responsesChan := make(chan *protodb.Entity) - go func() { - defer close(responsesChan) - ctx := context.Background() - for { - in, err := ds.Recv() - if err != nil { - responsesChan <- &protodb.Entity{Err: err.Error()} - return - } - out, err := s.Get(ctx, in) - if err != nil { - if out == nil { - out = new(protodb.Entity) - out.Key = in.Key - } - out.Err = err.Error() - responsesChan <- out - return - } - - // Otherwise continue on - responsesChan <- out - } - }() - - // Send routine, block until we return - for out := range responsesChan { - if err := ds.Send(out); err != nil { - return err - } - } - return nil -} - -func (s *server) Has(ctx context.Context, in *protodb.Entity) (*protodb.Entity, error) { - exists := s.db.Has(in.Key) - return &protodb.Entity{Exists: exists}, nil -} - -func (s *server) Set(ctx context.Context, in *protodb.Entity) (*protodb.Nothing, error) { - s.db.Set(in.Key, in.Value) - return nothing, nil -} - -func (s *server) SetSync(ctx context.Context, in *protodb.Entity) (*protodb.Nothing, error) { - s.db.SetSync(in.Key, in.Value) - return nothing, nil -} - -func (s *server) Iterator(query *protodb.Entity, dis protodb.DB_IteratorServer) error { - it := s.db.Iterator(query.Start, query.End) - defer it.Close() - return s.handleIterator(it, dis.Send) -} - -func (s *server) handleIterator(it db.Iterator, sendFunc func(*protodb.Iterator) error) error { - for it.Valid() { - start, end := it.Domain() - out := &protodb.Iterator{ - Domain: &protodb.Domain{Start: start, End: end}, - Valid: it.Valid(), - Key: it.Key(), - Value: it.Value(), - } - if err := sendFunc(out); err != nil { - return err - } - - // Finally move the iterator forward - it.Next() - } - return nil -} - -func (s *server) ReverseIterator(query *protodb.Entity, dis protodb.DB_ReverseIteratorServer) error { - it := s.db.ReverseIterator(query.Start, query.End) - defer it.Close() - return s.handleIterator(it, dis.Send) -} - -func (s *server) Stats(context.Context, *protodb.Nothing) (*protodb.Stats, error) { - stats := s.db.Stats() - return &protodb.Stats{Data: stats, TimeAt: time.Now().Unix()}, nil -} - -func (s *server) BatchWrite(c context.Context, b *protodb.Batch) (*protodb.Nothing, error) { - return s.batchWrite(c, b, false) -} - -func (s *server) BatchWriteSync(c context.Context, b *protodb.Batch) (*protodb.Nothing, error) { - return s.batchWrite(c, b, true) -} - -func (s *server) batchWrite(c context.Context, b *protodb.Batch, sync bool) (*protodb.Nothing, error) { - bat := s.db.NewBatch() - defer bat.Close() - for _, op := range b.Ops { - switch op.Type { - case protodb.Operation_SET: - bat.Set(op.Entity.Key, op.Entity.Value) - case protodb.Operation_DELETE: - bat.Delete(op.Entity.Key) - } - } - if sync { - bat.WriteSync() - } else { - bat.Write() - } - return nothing, nil -} diff --git a/libs/db/remotedb/proto/defs.pb.go b/libs/db/remotedb/proto/defs.pb.go deleted file mode 100644 index 4d9f0b272..000000000 --- a/libs/db/remotedb/proto/defs.pb.go +++ /dev/null @@ -1,914 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: defs.proto - -/* -Package protodb is a generated protocol buffer package. - -It is generated from these files: - defs.proto - -It has these top-level messages: - Batch - Operation - Entity - Nothing - Domain - Iterator - Stats - Init -*/ -package protodb - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -import ( - context "golang.org/x/net/context" - grpc "google.golang.org/grpc" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Operation_Type int32 - -const ( - Operation_SET Operation_Type = 0 - Operation_DELETE Operation_Type = 1 -) - -var Operation_Type_name = map[int32]string{ - 0: "SET", - 1: "DELETE", -} -var Operation_Type_value = map[string]int32{ - "SET": 0, - "DELETE": 1, -} - -func (x Operation_Type) String() string { - return proto.EnumName(Operation_Type_name, int32(x)) -} -func (Operation_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1, 0} } - -type Batch struct { - Ops []*Operation `protobuf:"bytes,1,rep,name=ops" json:"ops,omitempty"` -} - -func (m *Batch) Reset() { *m = Batch{} } -func (m *Batch) String() string { return proto.CompactTextString(m) } -func (*Batch) ProtoMessage() {} -func (*Batch) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *Batch) GetOps() []*Operation { - if m != nil { - return m.Ops - } - return nil -} - -type Operation struct { - Entity *Entity `protobuf:"bytes,1,opt,name=entity" json:"entity,omitempty"` - Type Operation_Type `protobuf:"varint,2,opt,name=type,enum=protodb.Operation_Type" json:"type,omitempty"` -} - -func (m *Operation) Reset() { *m = Operation{} } -func (m *Operation) String() string { return proto.CompactTextString(m) } -func (*Operation) ProtoMessage() {} -func (*Operation) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } - -func (m *Operation) GetEntity() *Entity { - if m != nil { - return m.Entity - } - return nil -} - -func (m *Operation) GetType() Operation_Type { - if m != nil { - return m.Type - } - return Operation_SET -} - -type Entity struct { - Id int32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"` - Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` - Exists bool `protobuf:"varint,4,opt,name=exists" json:"exists,omitempty"` - Start []byte `protobuf:"bytes,5,opt,name=start,proto3" json:"start,omitempty"` - End []byte `protobuf:"bytes,6,opt,name=end,proto3" json:"end,omitempty"` - Err string `protobuf:"bytes,7,opt,name=err" json:"err,omitempty"` - CreatedAt int64 `protobuf:"varint,8,opt,name=created_at,json=createdAt" json:"created_at,omitempty"` -} - -func (m *Entity) Reset() { *m = Entity{} } -func (m *Entity) String() string { return proto.CompactTextString(m) } -func (*Entity) ProtoMessage() {} -func (*Entity) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } - -func (m *Entity) GetId() int32 { - if m != nil { - return m.Id - } - return 0 -} - -func (m *Entity) GetKey() []byte { - if m != nil { - return m.Key - } - return nil -} - -func (m *Entity) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -func (m *Entity) GetExists() bool { - if m != nil { - return m.Exists - } - return false -} - -func (m *Entity) GetStart() []byte { - if m != nil { - return m.Start - } - return nil -} - -func (m *Entity) GetEnd() []byte { - if m != nil { - return m.End - } - return nil -} - -func (m *Entity) GetErr() string { - if m != nil { - return m.Err - } - return "" -} - -func (m *Entity) GetCreatedAt() int64 { - if m != nil { - return m.CreatedAt - } - return 0 -} - -type Nothing struct { -} - -func (m *Nothing) Reset() { *m = Nothing{} } -func (m *Nothing) String() string { return proto.CompactTextString(m) } -func (*Nothing) ProtoMessage() {} -func (*Nothing) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } - -type Domain struct { - Start []byte `protobuf:"bytes,1,opt,name=start,proto3" json:"start,omitempty"` - End []byte `protobuf:"bytes,2,opt,name=end,proto3" json:"end,omitempty"` -} - -func (m *Domain) Reset() { *m = Domain{} } -func (m *Domain) String() string { return proto.CompactTextString(m) } -func (*Domain) ProtoMessage() {} -func (*Domain) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } - -func (m *Domain) GetStart() []byte { - if m != nil { - return m.Start - } - return nil -} - -func (m *Domain) GetEnd() []byte { - if m != nil { - return m.End - } - return nil -} - -type Iterator struct { - Domain *Domain `protobuf:"bytes,1,opt,name=domain" json:"domain,omitempty"` - Valid bool `protobuf:"varint,2,opt,name=valid" json:"valid,omitempty"` - Key []byte `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` -} - -func (m *Iterator) Reset() { *m = Iterator{} } -func (m *Iterator) String() string { return proto.CompactTextString(m) } -func (*Iterator) ProtoMessage() {} -func (*Iterator) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } - -func (m *Iterator) GetDomain() *Domain { - if m != nil { - return m.Domain - } - return nil -} - -func (m *Iterator) GetValid() bool { - if m != nil { - return m.Valid - } - return false -} - -func (m *Iterator) GetKey() []byte { - if m != nil { - return m.Key - } - return nil -} - -func (m *Iterator) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -type Stats struct { - Data map[string]string `protobuf:"bytes,1,rep,name=data" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - TimeAt int64 `protobuf:"varint,2,opt,name=time_at,json=timeAt" json:"time_at,omitempty"` -} - -func (m *Stats) Reset() { *m = Stats{} } -func (m *Stats) String() string { return proto.CompactTextString(m) } -func (*Stats) ProtoMessage() {} -func (*Stats) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } - -func (m *Stats) GetData() map[string]string { - if m != nil { - return m.Data - } - return nil -} - -func (m *Stats) GetTimeAt() int64 { - if m != nil { - return m.TimeAt - } - return 0 -} - -type Init struct { - Type string `protobuf:"bytes,1,opt,name=Type" json:"Type,omitempty"` - Name string `protobuf:"bytes,2,opt,name=Name" json:"Name,omitempty"` - Dir string `protobuf:"bytes,3,opt,name=Dir" json:"Dir,omitempty"` -} - -func (m *Init) Reset() { *m = Init{} } -func (m *Init) String() string { return proto.CompactTextString(m) } -func (*Init) ProtoMessage() {} -func (*Init) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } - -func (m *Init) GetType() string { - if m != nil { - return m.Type - } - return "" -} - -func (m *Init) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Init) GetDir() string { - if m != nil { - return m.Dir - } - return "" -} - -func init() { - proto.RegisterType((*Batch)(nil), "protodb.Batch") - proto.RegisterType((*Operation)(nil), "protodb.Operation") - proto.RegisterType((*Entity)(nil), "protodb.Entity") - proto.RegisterType((*Nothing)(nil), "protodb.Nothing") - proto.RegisterType((*Domain)(nil), "protodb.Domain") - proto.RegisterType((*Iterator)(nil), "protodb.Iterator") - proto.RegisterType((*Stats)(nil), "protodb.Stats") - proto.RegisterType((*Init)(nil), "protodb.Init") - proto.RegisterEnum("protodb.Operation_Type", Operation_Type_name, Operation_Type_value) -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// Client API for DB service - -type DBClient interface { - Init(ctx context.Context, in *Init, opts ...grpc.CallOption) (*Entity, error) - Get(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Entity, error) - GetStream(ctx context.Context, opts ...grpc.CallOption) (DB_GetStreamClient, error) - Has(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Entity, error) - Set(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) - SetSync(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) - Delete(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) - DeleteSync(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) - Iterator(ctx context.Context, in *Entity, opts ...grpc.CallOption) (DB_IteratorClient, error) - ReverseIterator(ctx context.Context, in *Entity, opts ...grpc.CallOption) (DB_ReverseIteratorClient, error) - // rpc print(Nothing) returns (Entity) {} - Stats(ctx context.Context, in *Nothing, opts ...grpc.CallOption) (*Stats, error) - BatchWrite(ctx context.Context, in *Batch, opts ...grpc.CallOption) (*Nothing, error) - BatchWriteSync(ctx context.Context, in *Batch, opts ...grpc.CallOption) (*Nothing, error) -} - -type dBClient struct { - cc *grpc.ClientConn -} - -func NewDBClient(cc *grpc.ClientConn) DBClient { - return &dBClient{cc} -} - -func (c *dBClient) Init(ctx context.Context, in *Init, opts ...grpc.CallOption) (*Entity, error) { - out := new(Entity) - err := grpc.Invoke(ctx, "/protodb.DB/init", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) Get(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Entity, error) { - out := new(Entity) - err := grpc.Invoke(ctx, "/protodb.DB/get", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) GetStream(ctx context.Context, opts ...grpc.CallOption) (DB_GetStreamClient, error) { - stream, err := grpc.NewClientStream(ctx, &_DB_serviceDesc.Streams[0], c.cc, "/protodb.DB/getStream", opts...) - if err != nil { - return nil, err - } - x := &dBGetStreamClient{stream} - return x, nil -} - -type DB_GetStreamClient interface { - Send(*Entity) error - Recv() (*Entity, error) - grpc.ClientStream -} - -type dBGetStreamClient struct { - grpc.ClientStream -} - -func (x *dBGetStreamClient) Send(m *Entity) error { - return x.ClientStream.SendMsg(m) -} - -func (x *dBGetStreamClient) Recv() (*Entity, error) { - m := new(Entity) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *dBClient) Has(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Entity, error) { - out := new(Entity) - err := grpc.Invoke(ctx, "/protodb.DB/has", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) Set(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/set", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) SetSync(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/setSync", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) Delete(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/delete", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) DeleteSync(ctx context.Context, in *Entity, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/deleteSync", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) Iterator(ctx context.Context, in *Entity, opts ...grpc.CallOption) (DB_IteratorClient, error) { - stream, err := grpc.NewClientStream(ctx, &_DB_serviceDesc.Streams[1], c.cc, "/protodb.DB/iterator", opts...) - if err != nil { - return nil, err - } - x := &dBIteratorClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type DB_IteratorClient interface { - Recv() (*Iterator, error) - grpc.ClientStream -} - -type dBIteratorClient struct { - grpc.ClientStream -} - -func (x *dBIteratorClient) Recv() (*Iterator, error) { - m := new(Iterator) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *dBClient) ReverseIterator(ctx context.Context, in *Entity, opts ...grpc.CallOption) (DB_ReverseIteratorClient, error) { - stream, err := grpc.NewClientStream(ctx, &_DB_serviceDesc.Streams[2], c.cc, "/protodb.DB/reverseIterator", opts...) - if err != nil { - return nil, err - } - x := &dBReverseIteratorClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type DB_ReverseIteratorClient interface { - Recv() (*Iterator, error) - grpc.ClientStream -} - -type dBReverseIteratorClient struct { - grpc.ClientStream -} - -func (x *dBReverseIteratorClient) Recv() (*Iterator, error) { - m := new(Iterator) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *dBClient) Stats(ctx context.Context, in *Nothing, opts ...grpc.CallOption) (*Stats, error) { - out := new(Stats) - err := grpc.Invoke(ctx, "/protodb.DB/stats", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) BatchWrite(ctx context.Context, in *Batch, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/batchWrite", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *dBClient) BatchWriteSync(ctx context.Context, in *Batch, opts ...grpc.CallOption) (*Nothing, error) { - out := new(Nothing) - err := grpc.Invoke(ctx, "/protodb.DB/batchWriteSync", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// Server API for DB service - -type DBServer interface { - Init(context.Context, *Init) (*Entity, error) - Get(context.Context, *Entity) (*Entity, error) - GetStream(DB_GetStreamServer) error - Has(context.Context, *Entity) (*Entity, error) - Set(context.Context, *Entity) (*Nothing, error) - SetSync(context.Context, *Entity) (*Nothing, error) - Delete(context.Context, *Entity) (*Nothing, error) - DeleteSync(context.Context, *Entity) (*Nothing, error) - Iterator(*Entity, DB_IteratorServer) error - ReverseIterator(*Entity, DB_ReverseIteratorServer) error - // rpc print(Nothing) returns (Entity) {} - Stats(context.Context, *Nothing) (*Stats, error) - BatchWrite(context.Context, *Batch) (*Nothing, error) - BatchWriteSync(context.Context, *Batch) (*Nothing, error) -} - -func RegisterDBServer(s *grpc.Server, srv DBServer) { - s.RegisterService(&_DB_serviceDesc, srv) -} - -func _DB_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Init) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Init(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Init", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Init(ctx, req.(*Init)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Get(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Get", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Get(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_GetStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(DBServer).GetStream(&dBGetStreamServer{stream}) -} - -type DB_GetStreamServer interface { - Send(*Entity) error - Recv() (*Entity, error) - grpc.ServerStream -} - -type dBGetStreamServer struct { - grpc.ServerStream -} - -func (x *dBGetStreamServer) Send(m *Entity) error { - return x.ServerStream.SendMsg(m) -} - -func (x *dBGetStreamServer) Recv() (*Entity, error) { - m := new(Entity) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func _DB_Has_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Has(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Has", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Has(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Set(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Set", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Set(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_SetSync_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).SetSync(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/SetSync", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).SetSync(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Delete(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Delete", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Delete(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_DeleteSync_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Entity) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).DeleteSync(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/DeleteSync", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).DeleteSync(ctx, req.(*Entity)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_Iterator_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(Entity) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(DBServer).Iterator(m, &dBIteratorServer{stream}) -} - -type DB_IteratorServer interface { - Send(*Iterator) error - grpc.ServerStream -} - -type dBIteratorServer struct { - grpc.ServerStream -} - -func (x *dBIteratorServer) Send(m *Iterator) error { - return x.ServerStream.SendMsg(m) -} - -func _DB_ReverseIterator_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(Entity) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(DBServer).ReverseIterator(m, &dBReverseIteratorServer{stream}) -} - -type DB_ReverseIteratorServer interface { - Send(*Iterator) error - grpc.ServerStream -} - -type dBReverseIteratorServer struct { - grpc.ServerStream -} - -func (x *dBReverseIteratorServer) Send(m *Iterator) error { - return x.ServerStream.SendMsg(m) -} - -func _DB_Stats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Nothing) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).Stats(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/Stats", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).Stats(ctx, req.(*Nothing)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_BatchWrite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Batch) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).BatchWrite(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/BatchWrite", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).BatchWrite(ctx, req.(*Batch)) - } - return interceptor(ctx, in, info, handler) -} - -func _DB_BatchWriteSync_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Batch) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DBServer).BatchWriteSync(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/protodb.DB/BatchWriteSync", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DBServer).BatchWriteSync(ctx, req.(*Batch)) - } - return interceptor(ctx, in, info, handler) -} - -var _DB_serviceDesc = grpc.ServiceDesc{ - ServiceName: "protodb.DB", - HandlerType: (*DBServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "init", - Handler: _DB_Init_Handler, - }, - { - MethodName: "get", - Handler: _DB_Get_Handler, - }, - { - MethodName: "has", - Handler: _DB_Has_Handler, - }, - { - MethodName: "set", - Handler: _DB_Set_Handler, - }, - { - MethodName: "setSync", - Handler: _DB_SetSync_Handler, - }, - { - MethodName: "delete", - Handler: _DB_Delete_Handler, - }, - { - MethodName: "deleteSync", - Handler: _DB_DeleteSync_Handler, - }, - { - MethodName: "stats", - Handler: _DB_Stats_Handler, - }, - { - MethodName: "batchWrite", - Handler: _DB_BatchWrite_Handler, - }, - { - MethodName: "batchWriteSync", - Handler: _DB_BatchWriteSync_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "getStream", - Handler: _DB_GetStream_Handler, - ServerStreams: true, - ClientStreams: true, - }, - { - StreamName: "iterator", - Handler: _DB_Iterator_Handler, - ServerStreams: true, - }, - { - StreamName: "reverseIterator", - Handler: _DB_ReverseIterator_Handler, - ServerStreams: true, - }, - }, - Metadata: "defs.proto", -} - -func init() { proto.RegisterFile("defs.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 606 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x4f, 0x6f, 0xd3, 0x4e, - 0x10, 0xcd, 0xda, 0x8e, 0x13, 0x4f, 0x7f, 0xbf, 0x34, 0x8c, 0x10, 0xb5, 0x8a, 0x90, 0x22, 0x0b, - 0x09, 0x43, 0x69, 0x14, 0x52, 0x24, 0xfe, 0x9c, 0x68, 0x95, 0x1c, 0x2a, 0xa1, 0x22, 0x39, 0x95, - 0x38, 0xa2, 0x6d, 0x3d, 0x34, 0x2b, 0x1a, 0x3b, 0xac, 0x87, 0x8a, 0x5c, 0xb8, 0xf2, 0x79, 0xf8, - 0x7c, 0x5c, 0xd0, 0xae, 0x1d, 0x87, 0x36, 0x39, 0x84, 0x53, 0x76, 0x66, 0xde, 0x7b, 0xb3, 0xf3, - 0x32, 0x5e, 0x80, 0x94, 0x3e, 0x17, 0xfd, 0xb9, 0xce, 0x39, 0xc7, 0x96, 0xfd, 0x49, 0x2f, 0xa2, - 0x43, 0x68, 0x9e, 0x48, 0xbe, 0x9c, 0xe2, 0x63, 0x70, 0xf3, 0x79, 0x11, 0x8a, 0x9e, 0x1b, 0xef, - 0x0c, 0xb1, 0x5f, 0xd5, 0xfb, 0x1f, 0xe6, 0xa4, 0x25, 0xab, 0x3c, 0x4b, 0x4c, 0x39, 0xfa, 0x01, - 0x41, 0x9d, 0xc1, 0x27, 0xe0, 0x53, 0xc6, 0x8a, 0x17, 0xa1, 0xe8, 0x89, 0x78, 0x67, 0xb8, 0x5b, - 0xb3, 0xc6, 0x36, 0x9d, 0x54, 0x65, 0x3c, 0x00, 0x8f, 0x17, 0x73, 0x0a, 0x9d, 0x9e, 0x88, 0x3b, - 0xc3, 0xbd, 0x75, 0xf1, 0xfe, 0xf9, 0x62, 0x4e, 0x89, 0x05, 0x45, 0x0f, 0xc1, 0x33, 0x11, 0xb6, - 0xc0, 0x9d, 0x8c, 0xcf, 0xbb, 0x0d, 0x04, 0xf0, 0x47, 0xe3, 0xf7, 0xe3, 0xf3, 0x71, 0x57, 0x44, - 0xbf, 0x04, 0xf8, 0xa5, 0x38, 0x76, 0xc0, 0x51, 0xa9, 0xed, 0xdc, 0x4c, 0x1c, 0x95, 0x62, 0x17, - 0xdc, 0x2f, 0xb4, 0xb0, 0x3d, 0xfe, 0x4b, 0xcc, 0x11, 0xef, 0x43, 0xf3, 0x46, 0x5e, 0x7f, 0xa3, - 0xd0, 0xb5, 0xb9, 0x32, 0xc0, 0x07, 0xe0, 0xd3, 0x77, 0x55, 0x70, 0x11, 0x7a, 0x3d, 0x11, 0xb7, - 0x93, 0x2a, 0x32, 0xe8, 0x82, 0xa5, 0xe6, 0xb0, 0x59, 0xa2, 0x6d, 0x60, 0x54, 0x29, 0x4b, 0x43, - 0xbf, 0x54, 0xa5, 0xcc, 0xf6, 0x21, 0xad, 0xc3, 0x56, 0x4f, 0xc4, 0x41, 0x62, 0x8e, 0xf8, 0x08, - 0xe0, 0x52, 0x93, 0x64, 0x4a, 0x3f, 0x49, 0x0e, 0xdb, 0x3d, 0x11, 0xbb, 0x49, 0x50, 0x65, 0x8e, - 0x39, 0x0a, 0xa0, 0x75, 0x96, 0xf3, 0x54, 0x65, 0x57, 0xd1, 0x00, 0xfc, 0x51, 0x3e, 0x93, 0x2a, - 0x5b, 0x75, 0x13, 0x1b, 0xba, 0x39, 0x75, 0xb7, 0xe8, 0x2b, 0xb4, 0x4f, 0xd9, 0xb8, 0x94, 0x6b, - 0xe3, 0x77, 0x6a, 0xd9, 0x6b, 0x7e, 0x97, 0xa2, 0x49, 0x55, 0xae, 0x06, 0x57, 0xa5, 0x50, 0x3b, - 0x29, 0x83, 0xa5, 0x41, 0xee, 0x06, 0x83, 0xbc, 0xbf, 0x0c, 0x8a, 0x7e, 0x0a, 0x68, 0x4e, 0x58, - 0x72, 0x81, 0xcf, 0xc1, 0x4b, 0x25, 0xcb, 0x6a, 0x29, 0xc2, 0xba, 0x9d, 0xad, 0xf6, 0x47, 0x92, - 0xe5, 0x38, 0x63, 0xbd, 0x48, 0x2c, 0x0a, 0xf7, 0xa0, 0xc5, 0x6a, 0x46, 0xc6, 0x03, 0xc7, 0x7a, - 0xe0, 0x9b, 0xf0, 0x98, 0xf7, 0x5f, 0x41, 0x50, 0x63, 0x97, 0xb7, 0x10, 0xa5, 0x7d, 0xb7, 0x6e, - 0xe1, 0xd8, 0x5c, 0x19, 0xbc, 0x75, 0x5e, 0x8b, 0xe8, 0x1d, 0x78, 0xa7, 0x99, 0x62, 0xc4, 0x72, - 0x25, 0x2a, 0x52, 0xb9, 0x1e, 0x08, 0xde, 0x99, 0x9c, 0x2d, 0x49, 0xf6, 0x6c, 0xb4, 0x47, 0x4a, - 0xdb, 0x09, 0x83, 0xc4, 0x1c, 0x87, 0xbf, 0x3d, 0x70, 0x46, 0x27, 0x18, 0x83, 0xa7, 0x8c, 0xd0, - 0xff, 0xf5, 0x08, 0x46, 0x77, 0xff, 0xee, 0xc2, 0x46, 0x0d, 0x7c, 0x0a, 0xee, 0x15, 0x31, 0xde, - 0xad, 0x6c, 0x82, 0x1e, 0x41, 0x70, 0x45, 0x3c, 0x61, 0x4d, 0x72, 0xb6, 0x0d, 0x21, 0x16, 0x03, - 0x61, 0xf4, 0xa7, 0xb2, 0xd8, 0x4a, 0xff, 0x19, 0xb8, 0xc5, 0xa6, 0xab, 0x74, 0xeb, 0xc4, 0x72, - 0xad, 0x1a, 0xd8, 0x87, 0x56, 0x41, 0x3c, 0x59, 0x64, 0x97, 0xdb, 0xe1, 0x0f, 0xc1, 0x4f, 0xe9, - 0x9a, 0x98, 0xb6, 0x83, 0xbf, 0x30, 0x8f, 0x87, 0x81, 0x6f, 0xdf, 0x61, 0x08, 0x6d, 0xb5, 0x5c, - 0xdc, 0x35, 0xc2, 0xbd, 0xd5, 0xff, 0x50, 0x61, 0xa2, 0xc6, 0x40, 0xe0, 0x1b, 0xd8, 0xd5, 0x74, - 0x43, 0xba, 0xa0, 0xd3, 0x7f, 0xa5, 0x1e, 0xd8, 0xef, 0x89, 0x0b, 0x5c, 0xbb, 0xcb, 0x7e, 0xe7, - 0xf6, 0xde, 0x46, 0x0d, 0x1c, 0x00, 0x5c, 0x98, 0x47, 0xef, 0xa3, 0x56, 0x4c, 0xb8, 0xaa, 0xdb, - 0x97, 0x70, 0xe3, 0x34, 0x2f, 0xa1, 0xb3, 0x62, 0x58, 0x13, 0xb6, 0x60, 0x5d, 0xf8, 0x36, 0x75, - 0xf4, 0x27, 0x00, 0x00, 0xff, 0xff, 0x95, 0xf4, 0xe3, 0x82, 0x7a, 0x05, 0x00, 0x00, -} diff --git a/libs/db/remotedb/proto/defs.proto b/libs/db/remotedb/proto/defs.proto deleted file mode 100644 index 70471f234..000000000 --- a/libs/db/remotedb/proto/defs.proto +++ /dev/null @@ -1,71 +0,0 @@ -syntax = "proto3"; - -package protodb; - -message Batch { - repeated Operation ops = 1; -} - -message Operation { - Entity entity = 1; - enum Type { - SET = 0; - DELETE = 1; - } - Type type = 2; -} - -message Entity { - int32 id = 1; - bytes key = 2; - bytes value = 3; - bool exists = 4; - bytes start = 5; - bytes end = 6; - string err = 7; - int64 created_at = 8; -} - -message Nothing { -} - -message Domain { - bytes start = 1; - bytes end = 2; -} - -message Iterator { - Domain domain = 1; - bool valid = 2; - bytes key = 3; - bytes value = 4; -} - -message Stats { - map data = 1; - int64 time_at = 2; -} - -message Init { - string Type = 1; - string Name = 2; - string Dir = 3; -} - -service DB { - rpc init(Init) returns (Entity) {} - rpc get(Entity) returns (Entity) {} - rpc getStream(stream Entity) returns (stream Entity) {} - - rpc has(Entity) returns (Entity) {} - rpc set(Entity) returns (Nothing) {} - rpc setSync(Entity) returns (Nothing) {} - rpc delete(Entity) returns (Nothing) {} - rpc deleteSync(Entity) returns (Nothing) {} - rpc iterator(Entity) returns (stream Iterator) {} - rpc reverseIterator(Entity) returns (stream Iterator) {} - // rpc print(Nothing) returns (Entity) {} - rpc stats(Nothing) returns (Stats) {} - rpc batchWrite(Batch) returns (Nothing) {} - rpc batchWriteSync(Batch) returns (Nothing) {} -} diff --git a/libs/db/remotedb/remotedb.go b/libs/db/remotedb/remotedb.go deleted file mode 100644 index c70d54b9e..000000000 --- a/libs/db/remotedb/remotedb.go +++ /dev/null @@ -1,266 +0,0 @@ -package remotedb - -import ( - "context" - "fmt" - - "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/db/remotedb/grpcdb" - protodb "github.com/tendermint/tendermint/libs/db/remotedb/proto" -) - -type RemoteDB struct { - ctx context.Context - dc protodb.DBClient -} - -func NewRemoteDB(serverAddr string, serverKey string) (*RemoteDB, error) { - return newRemoteDB(grpcdb.NewClient(serverAddr, serverKey)) -} - -func newRemoteDB(gdc protodb.DBClient, err error) (*RemoteDB, error) { - if err != nil { - return nil, err - } - return &RemoteDB{dc: gdc, ctx: context.Background()}, nil -} - -type Init struct { - Dir string - Name string - Type string -} - -func (rd *RemoteDB) InitRemote(in *Init) error { - _, err := rd.dc.Init(rd.ctx, &protodb.Init{Dir: in.Dir, Type: in.Type, Name: in.Name}) - return err -} - -var _ db.DB = (*RemoteDB)(nil) - -// Close is a noop currently -func (rd *RemoteDB) Close() { -} - -func (rd *RemoteDB) Delete(key []byte) { - if _, err := rd.dc.Delete(rd.ctx, &protodb.Entity{Key: key}); err != nil { - panic(fmt.Sprintf("RemoteDB.Delete: %v", err)) - } -} - -func (rd *RemoteDB) DeleteSync(key []byte) { - if _, err := rd.dc.DeleteSync(rd.ctx, &protodb.Entity{Key: key}); err != nil { - panic(fmt.Sprintf("RemoteDB.DeleteSync: %v", err)) - } -} - -func (rd *RemoteDB) Set(key, value []byte) { - if _, err := rd.dc.Set(rd.ctx, &protodb.Entity{Key: key, Value: value}); err != nil { - panic(fmt.Sprintf("RemoteDB.Set: %v", err)) - } -} - -func (rd *RemoteDB) SetSync(key, value []byte) { - if _, err := rd.dc.SetSync(rd.ctx, &protodb.Entity{Key: key, Value: value}); err != nil { - panic(fmt.Sprintf("RemoteDB.SetSync: %v", err)) - } -} - -func (rd *RemoteDB) Get(key []byte) []byte { - res, err := rd.dc.Get(rd.ctx, &protodb.Entity{Key: key}) - if err != nil { - panic(fmt.Sprintf("RemoteDB.Get error: %v", err)) - } - return res.Value -} - -func (rd *RemoteDB) Has(key []byte) bool { - res, err := rd.dc.Has(rd.ctx, &protodb.Entity{Key: key}) - if err != nil { - panic(fmt.Sprintf("RemoteDB.Has error: %v", err)) - } - return res.Exists -} - -func (rd *RemoteDB) ReverseIterator(start, end []byte) db.Iterator { - dic, err := rd.dc.ReverseIterator(rd.ctx, &protodb.Entity{Start: start, End: end}) - if err != nil { - panic(fmt.Sprintf("RemoteDB.Iterator error: %v", err)) - } - return makeReverseIterator(dic) -} - -func (rd *RemoteDB) NewBatch() db.Batch { - return &batch{ - db: rd, - ops: nil, - } -} - -// TODO: Implement Print when db.DB implements a method -// to print to a string and not db.Print to stdout. -func (rd *RemoteDB) Print() { - panic("Unimplemented") -} - -func (rd *RemoteDB) Stats() map[string]string { - stats, err := rd.dc.Stats(rd.ctx, &protodb.Nothing{}) - if err != nil { - panic(fmt.Sprintf("RemoteDB.Stats error: %v", err)) - } - if stats == nil { - return nil - } - return stats.Data -} - -func (rd *RemoteDB) Iterator(start, end []byte) db.Iterator { - dic, err := rd.dc.Iterator(rd.ctx, &protodb.Entity{Start: start, End: end}) - if err != nil { - panic(fmt.Sprintf("RemoteDB.Iterator error: %v", err)) - } - return makeIterator(dic) -} - -func makeIterator(dic protodb.DB_IteratorClient) db.Iterator { - return &iterator{dic: dic} -} - -func makeReverseIterator(dric protodb.DB_ReverseIteratorClient) db.Iterator { - return &reverseIterator{dric: dric} -} - -type reverseIterator struct { - dric protodb.DB_ReverseIteratorClient - cur *protodb.Iterator -} - -var _ db.Iterator = (*iterator)(nil) - -func (rItr *reverseIterator) Valid() bool { - return rItr.cur != nil && rItr.cur.Valid -} - -func (rItr *reverseIterator) Domain() (start, end []byte) { - if rItr.cur == nil || rItr.cur.Domain == nil { - return nil, nil - } - return rItr.cur.Domain.Start, rItr.cur.Domain.End -} - -// Next advances the current reverseIterator -func (rItr *reverseIterator) Next() { - var err error - rItr.cur, err = rItr.dric.Recv() - if err != nil { - panic(fmt.Sprintf("RemoteDB.ReverseIterator.Next error: %v", err)) - } -} - -func (rItr *reverseIterator) Key() []byte { - if rItr.cur == nil { - return nil - } - return rItr.cur.Key -} - -func (rItr *reverseIterator) Value() []byte { - if rItr.cur == nil { - return nil - } - return rItr.cur.Value -} - -func (rItr *reverseIterator) Close() { -} - -// iterator implements the db.Iterator by retrieving -// streamed iterators from the remote backend as -// needed. It is NOT safe for concurrent usage, -// matching the behavior of other iterators. -type iterator struct { - dic protodb.DB_IteratorClient - cur *protodb.Iterator -} - -var _ db.Iterator = (*iterator)(nil) - -func (itr *iterator) Valid() bool { - return itr.cur != nil && itr.cur.Valid -} - -func (itr *iterator) Domain() (start, end []byte) { - if itr.cur == nil || itr.cur.Domain == nil { - return nil, nil - } - return itr.cur.Domain.Start, itr.cur.Domain.End -} - -// Next advances the current iterator -func (itr *iterator) Next() { - var err error - itr.cur, err = itr.dic.Recv() - if err != nil { - panic(fmt.Sprintf("RemoteDB.Iterator.Next error: %v", err)) - } -} - -func (itr *iterator) Key() []byte { - if itr.cur == nil { - return nil - } - return itr.cur.Key -} - -func (itr *iterator) Value() []byte { - if itr.cur == nil { - return nil - } - return itr.cur.Value -} - -func (itr *iterator) Close() { - err := itr.dic.CloseSend() - if err != nil { - panic(fmt.Sprintf("Error closing iterator: %v", err)) - } -} - -type batch struct { - db *RemoteDB - ops []*protodb.Operation -} - -var _ db.Batch = (*batch)(nil) - -func (bat *batch) Set(key, value []byte) { - op := &protodb.Operation{ - Entity: &protodb.Entity{Key: key, Value: value}, - Type: protodb.Operation_SET, - } - bat.ops = append(bat.ops, op) -} - -func (bat *batch) Delete(key []byte) { - op := &protodb.Operation{ - Entity: &protodb.Entity{Key: key}, - Type: protodb.Operation_DELETE, - } - bat.ops = append(bat.ops, op) -} - -func (bat *batch) Write() { - if _, err := bat.db.dc.BatchWrite(bat.db.ctx, &protodb.Batch{Ops: bat.ops}); err != nil { - panic(fmt.Sprintf("RemoteDB.BatchWrite: %v", err)) - } -} - -func (bat *batch) WriteSync() { - if _, err := bat.db.dc.BatchWriteSync(bat.db.ctx, &protodb.Batch{Ops: bat.ops}); err != nil { - panic(fmt.Sprintf("RemoteDB.BatchWriteSync: %v", err)) - } -} - -func (bat *batch) Close() { - bat.ops = nil -} diff --git a/libs/db/remotedb/remotedb_test.go b/libs/db/remotedb/remotedb_test.go deleted file mode 100644 index 43a022461..000000000 --- a/libs/db/remotedb/remotedb_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package remotedb_test - -import ( - "net" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/libs/db/remotedb" - "github.com/tendermint/tendermint/libs/db/remotedb/grpcdb" -) - -func TestRemoteDB(t *testing.T) { - cert := "test.crt" - key := "test.key" - ln, err := net.Listen("tcp", "localhost:0") - require.Nil(t, err, "expecting a port to have been assigned on which we can listen") - srv, err := grpcdb.NewServer(cert, key) - require.Nil(t, err) - defer srv.Stop() - go func() { - if err := srv.Serve(ln); err != nil { - t.Fatalf("BindServer: %v", err) - } - }() - - client, err := remotedb.NewRemoteDB(ln.Addr().String(), cert) - require.Nil(t, err, "expecting a successful client creation") - dbName := "test-remote-db" - require.Nil(t, client.InitRemote(&remotedb.Init{Name: dbName, Type: "goleveldb"})) - defer func() { - err := os.RemoveAll(dbName + ".db") - if err != nil { - panic(err) - } - }() - - k1 := []byte("key-1") - v1 := client.Get(k1) - require.Equal(t, 0, len(v1), "expecting no key1 to have been stored, got %X (%s)", v1, v1) - vv1 := []byte("value-1") - client.Set(k1, vv1) - gv1 := client.Get(k1) - require.Equal(t, gv1, vv1) - - // Simple iteration - itr := client.Iterator(nil, nil) - itr.Next() - require.Equal(t, itr.Key(), []byte("key-1")) - require.Equal(t, itr.Value(), []byte("value-1")) - require.Panics(t, itr.Next) - itr.Close() - - // Set some more keys - k2 := []byte("key-2") - v2 := []byte("value-2") - client.SetSync(k2, v2) - has := client.Has(k2) - require.True(t, has) - gv2 := client.Get(k2) - require.Equal(t, gv2, v2) - - // More iteration - itr = client.Iterator(nil, nil) - itr.Next() - require.Equal(t, itr.Key(), []byte("key-1")) - require.Equal(t, itr.Value(), []byte("value-1")) - itr.Next() - require.Equal(t, itr.Key(), []byte("key-2")) - require.Equal(t, itr.Value(), []byte("value-2")) - require.Panics(t, itr.Next) - itr.Close() - - // Deletion - client.Delete(k1) - client.DeleteSync(k2) - gv1 = client.Get(k1) - gv2 = client.Get(k2) - require.Equal(t, len(gv2), 0, "after deletion, not expecting the key to exist anymore") - require.Equal(t, len(gv1), 0, "after deletion, not expecting the key to exist anymore") - - // Batch tests - set - k3 := []byte("key-3") - k4 := []byte("key-4") - k5 := []byte("key-5") - v3 := []byte("value-3") - v4 := []byte("value-4") - v5 := []byte("value-5") - bat := client.NewBatch() - bat.Set(k3, v3) - bat.Set(k4, v4) - rv3 := client.Get(k3) - require.Equal(t, 0, len(rv3), "expecting no k3 to have been stored") - rv4 := client.Get(k4) - require.Equal(t, 0, len(rv4), "expecting no k4 to have been stored") - bat.Write() - rv3 = client.Get(k3) - require.Equal(t, rv3, v3, "expecting k3 to have been stored") - rv4 = client.Get(k4) - require.Equal(t, rv4, v4, "expecting k4 to have been stored") - - // Batch tests - deletion - bat = client.NewBatch() - bat.Delete(k4) - bat.Delete(k3) - bat.WriteSync() - rv3 = client.Get(k3) - require.Equal(t, 0, len(rv3), "expecting k3 to have been deleted") - rv4 = client.Get(k4) - require.Equal(t, 0, len(rv4), "expecting k4 to have been deleted") - - // Batch tests - set and delete - bat = client.NewBatch() - bat.Set(k4, v4) - bat.Set(k5, v5) - bat.Delete(k4) - bat.WriteSync() - rv4 = client.Get(k4) - require.Equal(t, 0, len(rv4), "expecting k4 to have been deleted") - rv5 := client.Get(k5) - require.Equal(t, rv5, v5, "expecting k5 to have been stored") -} diff --git a/libs/db/remotedb/test.crt b/libs/db/remotedb/test.crt deleted file mode 100644 index 1090e73d7..000000000 --- a/libs/db/remotedb/test.crt +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEOjCCAiKgAwIBAgIQYO+jRR0Sbs+WzU/hj2aoxzANBgkqhkiG9w0BAQsFADAZ -MRcwFQYDVQQDEw50ZW5kZXJtaW50LmNvbTAeFw0xOTA2MDIxMTAyMDdaFw0yMDEy -MDIxMTAyMDRaMBMxETAPBgNVBAMTCHJlbW90ZWRiMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAt7YkYMJ5X5X3MT1tWG1KFO3uyZl962fInl+43xVESydp -qYYHYei7b3T8c/3Ww6f3aKkkCHrvPtqHZjU6o+wp/AQMNlyUoyRN89+6Oj67u2C7 -iZjzAJ+Pk87jMaStubvmZ9J+uk4op4rv5Rt4ns/Kg70RaMvqYR8tGqPcy3o8fWS+ -hCbuwAS8b65yp+AgbnThDEBUnieN3OFLfDV//45qw2OlqlM/gHOVT2JMRbl14Y7x -tW3/Xe+lsB7B3+OC6NQ2Nu7DEA1X+TBNyItIGnQH6DwK2ZBRtyQEk26FAWVj8fHd -A5I4+RcGWXz4T6gJmDZN7+47WHO0ProjARbUV0GIuQIDAQABo4GDMIGAMA4GA1Ud -DwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O -BBYEFOA8wzCYhoZmy0WHgnv/0efijUMKMB8GA1UdIwQYMBaAFNSTPe743aIx7rIp -vn5HV3gJ4z1hMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAKZf -EVo0i9nMZv6ZJjbmAlMfo5FH41/oBYC8pyGAnJKl42raXKJAbl45h80iGn3vNggf -7HJjN+znAHDFYjIwK2IV2WhHPyxK6uk+FA5uBR/aAPcw+zhRfXUMYdhNHr6KBlZZ -bvD7Iq4UALg+XFQz/fQkIi7QvTBwkYyPNA2+a/TGf6myMp26hoz73DQXklqm6Zle -myPs1Vp9bTgOv/3l64BMUV37FZ2TyiisBkV1qPEoDxT7Fbi8G1K8gMDLd0wu0jvX -nz96nk30TDnZewV1fhkMJVKKGiLbaIgHcu1lWsWJZ0tdc+MF7R9bLBO5T0cTDgNy -V8/51g+Cxu5SSHKjFkT0vBBONhjPmRqzJpxOQfHjiv8mmHwwiaNNy2VkJHj5GHer -64r67fQTSqAifzgwAbXYK+ObUbx4PnHvSYSF5dbcR1Oj6UTVtGAgdmN2Y03AIc1B -CiaojcMVuMRz/SvmPWl34GBvvT5/h9VCpHEB3vV6bQxJb5U1fLyo4GABA2Ic3DHr -kV5p7CZI06UNbyQyFtnEb5XoXywRa4Df7FzDIv3HL13MtyXrYrJqC1eAbn+3jGdh -bQa510mWYAlQQmzHSf/SLKott4QKR3SmhOGqGKNvquAYJ9XLdYdsPmKKGH6iGUD8 -n7yEi0KMD/BHsPQNNLatsR2SxqGDeLhbLR0w2hig ------END CERTIFICATE----- diff --git a/libs/db/remotedb/test.key b/libs/db/remotedb/test.key deleted file mode 100644 index b30bf809a..000000000 --- a/libs/db/remotedb/test.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAt7YkYMJ5X5X3MT1tWG1KFO3uyZl962fInl+43xVESydpqYYH -Yei7b3T8c/3Ww6f3aKkkCHrvPtqHZjU6o+wp/AQMNlyUoyRN89+6Oj67u2C7iZjz -AJ+Pk87jMaStubvmZ9J+uk4op4rv5Rt4ns/Kg70RaMvqYR8tGqPcy3o8fWS+hCbu -wAS8b65yp+AgbnThDEBUnieN3OFLfDV//45qw2OlqlM/gHOVT2JMRbl14Y7xtW3/ -Xe+lsB7B3+OC6NQ2Nu7DEA1X+TBNyItIGnQH6DwK2ZBRtyQEk26FAWVj8fHdA5I4 -+RcGWXz4T6gJmDZN7+47WHO0ProjARbUV0GIuQIDAQABAoIBAQCEVFAZ3puc7aIU -NuIXqwmMz+KMFuMr+SL6aYr6LhB2bhpfQSr6LLEu1L6wMm1LnCbLneJVtW+1/6U+ -SyNFRmzrmmLNmZx7c0AvZb14DQ4fJ8uOjryje0vptUHT1YJJ4n5R1L7yJjCElsC8 -cDBPfO+sOzlaGmBmuxU7NkNp0k/WJc1Wnn5WFCKKk8BCH1AUKvn/vwbRV4zl/Be7 -ApywPUouV+GJlTAG5KLb15CWKSqFNJxUJ6K7NnmfDoy7muUUv8MtrTn59XTH4qK7 -p/3A8tdNpR/RpEJ8+y3kS9CDZBVnsk0j0ptT//jdt1vSsylXxrf7vjLnyguRZZ5H -Vwe2POotAoGBAOY1UaFjtIz2G5qromaUtrPb5EPWRU8fiLtUXUDKG8KqNAqsGbDz -Stw1mVFyyuaFMReO18djCvcja1xxF3TZbdpV1k7RfcpEZXiFzBAPgeEGdA3Tc3V2 -byuJQthWamCBxF/7OGUmH/E/kH0pv5g9+eIitK/CUC2YUhCnubhchGAXAoGBAMxL -O7mnPqDJ2PqxVip/lL6VnchtF1bx1aDNr83rVTf+BEsOgCIFoDEBIVKDnhXlaJu7 -8JN4la/esytq4j3nM1cl6mjvw2ixYmwQtKiDuNiyb88hhQ+nxVsbIpYxtbhsj+u5 -hOrMN6jKd0GVWsYpdNvY/dXZG1MXhbWwExjRAY+vAoGBAKBu3jHUU5q9VWWIYciN -sXpNL5qbNHg86MRsugSSFaCnj1c0sz7ffvdSn0Pk9USL5Defw/9fpd+wHn0xD4DO -msFDevQ5CSoyWmkRDbLPq9sP7UdJariczkMQCLbOGpqhNSMS6C2N0UsG2oJv2ueV -oZUYTMYEbG4qLl8PFN5IE7UHAoGAGwEq4OyZm7lyxBii8jUxHUw7sh2xgx2uhnYJ -8idUeXVLbfx5tYWW2kNy+yxIvk432LYsI+JBryC6AFg9lb81CyUI6lwfMXyZLP28 -U7Ytvf9ARloA88PSk6tvk/j4M2uuTpOUXVEnXll9EB9FA4LBXro9O4JaWU53rz+a -FqKyGSMCgYEAuYCGC+Fz7lIa0aE4tT9mwczQequxGYsL41KR/4pDO3t9QsnzunLY -fvCFhteBOstwTBBdfBaKIwSp3zI2QtA4K0Jx9SAJ9q0ft2ciB9ukUFBhC9+TqzXg -gSz3XpRtI8PhwAxZgCnov+NPQV8IxvD4ZgnnEiRBHrYnSEsaMLoVnkw= ------END RSA PRIVATE KEY----- diff --git a/libs/db/types.go b/libs/db/types.go deleted file mode 100644 index 30f8afd18..000000000 --- a/libs/db/types.go +++ /dev/null @@ -1,136 +0,0 @@ -package db - -// DBs are goroutine safe. -type DB interface { - - // Get returns nil iff key doesn't exist. - // A nil key is interpreted as an empty byteslice. - // CONTRACT: key, value readonly []byte - Get([]byte) []byte - - // Has checks if a key exists. - // A nil key is interpreted as an empty byteslice. - // CONTRACT: key, value readonly []byte - Has(key []byte) bool - - // Set sets the key. - // A nil key is interpreted as an empty byteslice. - // CONTRACT: key, value readonly []byte - Set([]byte, []byte) - SetSync([]byte, []byte) - - // Delete deletes the key. - // A nil key is interpreted as an empty byteslice. - // CONTRACT: key readonly []byte - Delete([]byte) - DeleteSync([]byte) - - // Iterate over a domain of keys in ascending order. End is exclusive. - // Start must be less than end, or the Iterator is invalid. - // A nil start is interpreted as an empty byteslice. - // If end is nil, iterates up to the last item (inclusive). - // CONTRACT: No writes may happen within a domain while an iterator exists over it. - // CONTRACT: start, end readonly []byte - Iterator(start, end []byte) Iterator - - // Iterate over a domain of keys in descending order. End is exclusive. - // Start must be less than end, or the Iterator is invalid. - // If start is nil, iterates up to the first/least item (inclusive). - // If end is nil, iterates from the last/greatest item (inclusive). - // CONTRACT: No writes may happen within a domain while an iterator exists over it. - // CONTRACT: start, end readonly []byte - ReverseIterator(start, end []byte) Iterator - - // Closes the connection. - Close() - - // Creates a batch for atomic updates. - NewBatch() Batch - - // For debugging - Print() - - // Stats returns a map of property values for all keys and the size of the cache. - Stats() map[string]string -} - -//---------------------------------------- -// Batch - -// Batch Close must be called when the program no longer needs the object. -type Batch interface { - SetDeleter - Write() - WriteSync() - Close() -} - -type SetDeleter interface { - Set(key, value []byte) // CONTRACT: key, value readonly []byte - Delete(key []byte) // CONTRACT: key readonly []byte -} - -//---------------------------------------- -// Iterator - -/* - Usage: - - var itr Iterator = ... - defer itr.Close() - - for ; itr.Valid(); itr.Next() { - k, v := itr.Key(); itr.Value() - // ... - } -*/ -type Iterator interface { - - // The start & end (exclusive) limits to iterate over. - // If end < start, then the Iterator goes in reverse order. - // - // A domain of ([]byte{12, 13}, []byte{12, 14}) will iterate - // over anything with the prefix []byte{12, 13}. - // - // The smallest key is the empty byte array []byte{} - see BeginningKey(). - // The largest key is the nil byte array []byte(nil) - see EndingKey(). - // CONTRACT: start, end readonly []byte - Domain() (start []byte, end []byte) - - // Valid returns whether the current position is valid. - // Once invalid, an Iterator is forever invalid. - Valid() bool - - // Next moves the iterator to the next sequential key in the database, as - // defined by order of iteration. - // - // If Valid returns false, this method will panic. - Next() - - // Key returns the key of the cursor. - // If Valid returns false, this method will panic. - // CONTRACT: key readonly []byte - Key() (key []byte) - - // Value returns the value of the cursor. - // If Valid returns false, this method will panic. - // CONTRACT: value readonly []byte - Value() (value []byte) - - // Close releases the Iterator. - Close() -} - -// For testing convenience. -func bz(s string) []byte { - return []byte(s) -} - -// We defensively turn nil keys or values into []byte{} for -// most operations. -func nonNilBytes(bz []byte) []byte { - if bz == nil { - return []byte{} - } - return bz -} diff --git a/libs/db/util.go b/libs/db/util.go deleted file mode 100644 index e927c3548..000000000 --- a/libs/db/util.go +++ /dev/null @@ -1,45 +0,0 @@ -package db - -import ( - "bytes" -) - -func cp(bz []byte) (ret []byte) { - ret = make([]byte, len(bz)) - copy(ret, bz) - return ret -} - -// Returns a slice of the same length (big endian) -// except incremented by one. -// Returns nil on overflow (e.g. if bz bytes are all 0xFF) -// CONTRACT: len(bz) > 0 -func cpIncr(bz []byte) (ret []byte) { - if len(bz) == 0 { - panic("cpIncr expects non-zero bz length") - } - ret = cp(bz) - for i := len(bz) - 1; i >= 0; i-- { - if ret[i] < byte(0xFF) { - ret[i]++ - return - } - ret[i] = byte(0x00) - if i == 0 { - // Overflow - return nil - } - } - return nil -} - -// See DB interface documentation for more information. -func IsKeyInDomain(key, start, end []byte) bool { - if bytes.Compare(key, start) < 0 { - return false - } - if end != nil && bytes.Compare(end, key) <= 0 { - return false - } - return true -} diff --git a/libs/db/util_test.go b/libs/db/util_test.go deleted file mode 100644 index 39a02160c..000000000 --- a/libs/db/util_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package db - -import ( - "fmt" - "os" - "testing" -) - -// Empty iterator for empty db. -func TestPrefixIteratorNoMatchNil(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - itr := IteratePrefix(db, []byte("2")) - - checkInvalid(t, itr) - }) - } -} - -// Empty iterator for db populated after iterator created. -func TestPrefixIteratorNoMatch1(t *testing.T) { - for backend := range backends { - if backend == BoltDBBackend { - t.Log("bolt does not support concurrent writes while iterating") - continue - } - - t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - itr := IteratePrefix(db, []byte("2")) - db.SetSync(bz("1"), bz("value_1")) - - checkInvalid(t, itr) - }) - } -} - -// Empty iterator for prefix starting after db entry. -func TestPrefixIteratorNoMatch2(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - db.SetSync(bz("3"), bz("value_3")) - itr := IteratePrefix(db, []byte("4")) - - checkInvalid(t, itr) - }) - } -} - -// Iterator with single val for db with single val, starting from that val. -func TestPrefixIteratorMatch1(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - db.SetSync(bz("2"), bz("value_2")) - itr := IteratePrefix(db, bz("2")) - - checkValid(t, itr, true) - checkItem(t, itr, bz("2"), bz("value_2")) - checkNext(t, itr, false) - - // Once invalid... - checkInvalid(t, itr) - }) - } -} - -// Iterator with prefix iterates over everything with same prefix. -func TestPrefixIteratorMatches1N(t *testing.T) { - for backend := range backends { - t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { - db, dir := newTempDB(t, backend) - defer os.RemoveAll(dir) - - // prefixed - db.SetSync(bz("a/1"), bz("value_1")) - db.SetSync(bz("a/3"), bz("value_3")) - - // not - db.SetSync(bz("b/3"), bz("value_3")) - db.SetSync(bz("a-3"), bz("value_3")) - db.SetSync(bz("a.3"), bz("value_3")) - db.SetSync(bz("abcdefg"), bz("value_3")) - itr := IteratePrefix(db, bz("a/")) - - checkValid(t, itr, true) - checkItem(t, itr, bz("a/1"), bz("value_1")) - checkNext(t, itr, true) - checkItem(t, itr, bz("a/3"), bz("value_3")) - - // Bad! - checkNext(t, itr, false) - - //Once invalid... - checkInvalid(t, itr) - }) - } -} diff --git a/libs/fail/fail.go b/libs/fail/fail.go index d7912af5c..0c9220622 100644 --- a/libs/fail/fail.go +++ b/libs/fail/fail.go @@ -2,36 +2,30 @@ package fail import ( "fmt" - "math/rand" "os" "strconv" ) -var callIndexToFail int - -func init() { +func envSet() int { callIndexToFailS := os.Getenv("FAIL_TEST_INDEX") if callIndexToFailS == "" { - callIndexToFail = -1 + return -1 } else { var err error - callIndexToFail, err = strconv.Atoi(callIndexToFailS) + callIndexToFail, err := strconv.Atoi(callIndexToFailS) if err != nil { - callIndexToFail = -1 + return -1 } + return callIndexToFail } } // Fail when FAIL_TEST_INDEX == callIndex -var ( - callIndex int //indexes Fail calls - - callRandIndex int // indexes a run of FailRand calls - callRandIndexToFail = -1 // the callRandIndex to fail on in FailRand -) +var callIndex int //indexes Fail calls func Fail() { + callIndexToFail := envSet() if callIndexToFail < 0 { return } @@ -43,33 +37,6 @@ func Fail() { callIndex += 1 } -// FailRand should be called n successive times. -// It will fail on a random one of those calls -// n must be greater than 0 -func FailRand(n int) { - if callIndexToFail < 0 { - return - } - - if callRandIndexToFail < 0 { - // first call in the loop, pick a random index to fail at - callRandIndexToFail = rand.Intn(n) - callRandIndex = 0 - } - - if callIndex == callIndexToFail { - if callRandIndex == callRandIndexToFail { - Exit() - } - } - - callRandIndex += 1 - - if callRandIndex == n { - callIndex += 1 - } -} - func Exit() { fmt.Printf("*** fail-test %d ***\n", callIndex) os.Exit(1) diff --git a/libs/log/tmfmt_logger.go b/libs/log/tmfmt_logger.go index d841263ea..d57f9558e 100644 --- a/libs/log/tmfmt_logger.go +++ b/libs/log/tmfmt_logger.go @@ -60,9 +60,10 @@ func (l tmfmtLogger) Log(keyvals ...interface{}) error { for i := 0; i < len(keyvals)-1; i += 2 { // Extract level - if keyvals[i] == kitlevel.Key() { + switch keyvals[i] { + case kitlevel.Key(): excludeIndexes = append(excludeIndexes, i) - switch keyvals[i+1].(type) { + switch keyvals[i+1].(type) { // nolint:gocritic case string: lvl = keyvals[i+1].(string) case kitlevel.Value: @@ -71,11 +72,11 @@ func (l tmfmtLogger) Log(keyvals ...interface{}) error { panic(fmt.Sprintf("level value of unknown type %T", keyvals[i+1])) } // and message - } else if keyvals[i] == msgKey { + case msgKey: excludeIndexes = append(excludeIndexes, i) msg = keyvals[i+1].(string) // and module (could be multiple keyvals; if such case last keyvalue wins) - } else if keyvals[i] == moduleKey { + case moduleKey: excludeIndexes = append(excludeIndexes, i) module = keyvals[i+1].(string) } diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go index d5f61dc07..5a2baa14f 100644 --- a/libs/pubsub/pubsub_test.go +++ b/libs/pubsub/pubsub_test.go @@ -273,11 +273,11 @@ func TestResubscribe(t *testing.T) { defer s.Stop() ctx := context.Background() - subscription, err := s.Subscribe(ctx, clientID, query.Empty{}) + _, err := s.Subscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) err = s.Unsubscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) - subscription, err = s.Subscribe(ctx, clientID, query.Empty{}) + subscription, err := s.Subscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) err = s.Publish(ctx, "Cable") diff --git a/libs/test/assert.go b/libs/test/assert.go deleted file mode 100644 index a6ffed0ce..000000000 --- a/libs/test/assert.go +++ /dev/null @@ -1,14 +0,0 @@ -package test - -import ( - "testing" -) - -func AssertPanics(t *testing.T, msg string, f func()) { - defer func() { - if err := recover(); err == nil { - t.Errorf("Should have panic'd, but didn't: %v", msg) - } - }() - f() -} diff --git a/libs/version/version.go b/libs/version/version.go deleted file mode 100644 index 6e73a937d..000000000 --- a/libs/version/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package version - -const Version = "0.9.0" diff --git a/lite/base_verifier.go b/lite/base_verifier.go index 21fa2ce52..96dc4d254 100644 --- a/lite/base_verifier.go +++ b/lite/base_verifier.go @@ -3,6 +3,8 @@ package lite import ( "bytes" + "github.com/pkg/errors" + cmn "github.com/tendermint/tendermint/libs/common" lerr "github.com/tendermint/tendermint/lite/errors" "github.com/tendermint/tendermint/types" @@ -66,7 +68,7 @@ func (bv *BaseVerifier) Verify(signedHeader types.SignedHeader) error { // Do basic sanity checks. err := signedHeader.ValidateBasic(bv.chainID) if err != nil { - return cmn.ErrorWrap(err, "in verify") + return errors.Wrap(err, "in verify") } // Check commit signatures. @@ -74,7 +76,7 @@ func (bv *BaseVerifier) Verify(signedHeader types.SignedHeader) error { bv.chainID, signedHeader.Commit.BlockID, signedHeader.Height, signedHeader.Commit) if err != nil { - return cmn.ErrorWrap(err, "in verify") + return errors.Wrap(err, "in verify") } return nil diff --git a/lite/dbprovider.go b/lite/dbprovider.go index 4e76e3657..bbde7ef8c 100644 --- a/lite/dbprovider.go +++ b/lite/dbprovider.go @@ -7,10 +7,10 @@ import ( amino "github.com/tendermint/go-amino" cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" - dbm "github.com/tendermint/tendermint/libs/db" log "github.com/tendermint/tendermint/libs/log" lerr "github.com/tendermint/tendermint/lite/errors" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) var _ PersistentProvider = (*DBProvider)(nil) diff --git a/lite/errors/errors.go b/lite/errors/errors.go index ce479342c..005d97a29 100644 --- a/lite/errors/errors.go +++ b/lite/errors/errors.go @@ -3,12 +3,9 @@ package errors import ( "fmt" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) -//---------------------------------------- -// Error types - type errCommitNotFound struct{} func (e errCommitNotFound) Error() string { @@ -47,88 +44,55 @@ func (e errCommitExpired) Error() string { return "commit is too old to be trusted" } -//---------------------------------------- -// Methods for above error types - -//----------------- -// ErrCommitNotFound - // ErrCommitNotFound indicates that a the requested commit was not found. func ErrCommitNotFound() error { - return cmn.ErrorWrap(errCommitNotFound{}, "") + return errors.Wrap(errCommitNotFound{}, "") } func IsErrCommitNotFound(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errCommitNotFound) - return ok - } - return false + _, ok := errors.Cause(err).(errCommitNotFound) + return ok } -//----------------- -// ErrUnexpectedValidators - // ErrUnexpectedValidators indicates a validator set mismatch. func ErrUnexpectedValidators(got, want []byte) error { - return cmn.ErrorWrap(errUnexpectedValidators{ + return errors.Wrap(errUnexpectedValidators{ got: got, want: want, }, "") } func IsErrUnexpectedValidators(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errUnexpectedValidators) - return ok - } - return false + _, ok := errors.Cause(err).(errUnexpectedValidators) + return ok } -//----------------- -// ErrUnknownValidators - // ErrUnknownValidators indicates that some validator set was missing or unknown. func ErrUnknownValidators(chainID string, height int64) error { - return cmn.ErrorWrap(errUnknownValidators{chainID, height}, "") + return errors.Wrap(errUnknownValidators{chainID, height}, "") } func IsErrUnknownValidators(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errUnknownValidators) - return ok - } - return false + _, ok := errors.Cause(err).(errUnknownValidators) + return ok } -//----------------- -// ErrEmptyTree - func ErrEmptyTree() error { - return cmn.ErrorWrap(errEmptyTree{}, "") + return errors.Wrap(errEmptyTree{}, "") } func IsErrEmptyTree(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errEmptyTree) - return ok - } - return false + _, ok := errors.Cause(err).(errEmptyTree) + return ok } -//----------------- -// ErrCommitExpired - func ErrCommitExpired() error { - return cmn.ErrorWrap(errCommitExpired{}, "") + return errors.Wrap(errCommitExpired{}, "") } func IsErrCommitExpired(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errCommitExpired) - return ok - } - return false + _, ok := errors.Cause(err).(errCommitExpired) + return ok } type errValidatorChange struct { @@ -140,15 +104,10 @@ func (e errValidatorChange) Error() string { } func ErrValidatorChange(change float64) error { - return cmn.ErrorWrap(errValidatorChange{ - change: change, - }, "") + return errors.Wrap(errValidatorChange{change: change}, "") } func IsErrValidatorChange(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errValidatorChange) - return ok - } - return false + _, ok := errors.Cause(err).(errValidatorChange) + return ok } diff --git a/lite/provider_test.go b/lite/provider_test.go index d578b5c4a..11aad25a1 100644 --- a/lite/provider_test.go +++ b/lite/provider_test.go @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - dbm "github.com/tendermint/tendermint/libs/db" log "github.com/tendermint/tendermint/libs/log" lerr "github.com/tendermint/tendermint/lite/errors" pks "github.com/tendermint/tendermint/lite/internal/privkeys" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) // missingProvider doesn't store anything, always a miss. diff --git a/lite/proxy/errors.go b/lite/proxy/errors.go index 6a7c2354c..41923659f 100644 --- a/lite/proxy/errors.go +++ b/lite/proxy/errors.go @@ -1,7 +1,7 @@ package proxy import ( - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) type errNoData struct{} @@ -12,13 +12,10 @@ func (e errNoData) Error() string { // IsErrNoData checks whether an error is due to a query returning empty data func IsErrNoData(err error) bool { - if err_, ok := err.(cmn.Error); ok { - _, ok := err_.Data().(errNoData) - return ok - } - return false + _, ok := errors.Cause(err).(errNoData) + return ok } func ErrNoData() error { - return cmn.ErrorWrap(errNoData{}, "") + return errors.Wrap(errNoData{}, "") } diff --git a/lite/proxy/query.go b/lite/proxy/query.go index fd10e0bb6..2e0b93229 100644 --- a/lite/proxy/query.go +++ b/lite/proxy/query.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" "github.com/tendermint/tendermint/crypto/merkle" + cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/lite" lerr "github.com/tendermint/tendermint/lite/errors" rpcclient "github.com/tendermint/tendermint/rpc/client" @@ -83,7 +84,7 @@ func GetWithProofOptions(prt *merkle.ProofRuntime, path string, key []byte, opts kp = kp.AppendKey(resp.Key, merkle.KeyEncodingURL) err = prt.VerifyValue(resp.Proof, signedHeader.AppHash, kp.String(), resp.Value) if err != nil { - return nil, cmn.ErrorWrap(err, "Couldn't verify value proof") + return nil, errors.Wrap(err, "Couldn't verify value proof") } return &ctypes.ResultABCIQuery{Response: resp}, nil } else { @@ -92,7 +93,7 @@ func GetWithProofOptions(prt *merkle.ProofRuntime, path string, key []byte, opts // XXX How do we encode the key into a string... err = prt.VerifyAbsence(resp.Proof, signedHeader.AppHash, string(resp.Key)) if err != nil { - return nil, cmn.ErrorWrap(err, "Couldn't verify absence proof") + return nil, errors.Wrap(err, "Couldn't verify absence proof") } return &ctypes.ResultABCIQuery{Response: resp}, nil } diff --git a/lite/proxy/query_test.go b/lite/proxy/query_test.go index db2b6e46c..d92a486ea 100644 --- a/lite/proxy/query_test.go +++ b/lite/proxy/query_test.go @@ -143,13 +143,13 @@ func TestTxProofs(t *testing.T) { // First let's make sure a bogus transaction hash returns a valid non-existence proof. key := types.Tx([]byte("bogus")).Hash() - res, err := cl.Tx(key, true) + _, err = cl.Tx(key, true) require.NotNil(err) require.Contains(err.Error(), "not found") // Now let's check with the real tx root hash. key = types.Tx(tx).Hash() - res, err = cl.Tx(key, true) + res, err := cl.Tx(key, true) require.NoError(err, "%#v", err) require.NotNil(res) keyHash := merkle.SimpleHashFromByteSlices([][]byte{key}) diff --git a/lite/verifying/provider.go b/lite/verifying/provider.go index e53e3ab91..9a4d2c6b1 100644 --- a/lite/verifying/provider.go +++ b/lite/verifying/provider.go @@ -12,12 +12,12 @@ import ( "time" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" log "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/lite" lclient "github.com/tendermint/tendermint/lite/client" lerr "github.com/tendermint/tendermint/lite/errors" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) const ( diff --git a/lite/verifying/provider_test.go b/lite/verifying/provider_test.go index 7b910663c..c0bcbcc2c 100644 --- a/lite/verifying/provider_test.go +++ b/lite/verifying/provider_test.go @@ -8,11 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - dbm "github.com/tendermint/tendermint/libs/db" log "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/lite" pks "github.com/tendermint/tendermint/lite/internal/privkeys" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) func TestProviderValidPath(t *testing.T) { diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 4042e9b4b..8df4e6708 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -220,9 +220,10 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t var ( memSize = mem.Size() txsBytes = mem.TxsBytes() + txSize = len(tx) ) if memSize >= mem.config.Size || - int64(len(tx))+txsBytes > mem.config.MaxTxsBytes { + int64(txSize)+txsBytes > mem.config.MaxTxsBytes { return ErrMempoolIsFull{ memSize, mem.config.Size, txsBytes, mem.config.MaxTxsBytes} @@ -231,8 +232,8 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t // The size of the corresponding amino-encoded TxMessage // can't be larger than the maxMsgSize, otherwise we can't // relay it to peers. - if len(tx) > maxTxSize { - return ErrTxTooLarge + if txSize > mem.config.MaxTxBytes { + return ErrTxTooLarge{mem.config.MaxTxBytes, txSize} } if mem.preCheck != nil { @@ -249,11 +250,11 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t // so we only record the sender for txs still in the mempool. if e, ok := mem.txsMap.Load(txKey(tx)); ok { memTx := e.(*clist.CElement).Value.(*mempoolTx) - if _, loaded := memTx.senders.LoadOrStore(txInfo.SenderID, true); loaded { - // TODO: consider punishing peer for dups, - // its non-trivial since invalid txs can become valid, - // but they can spam the same tx with little cost to them atm. - } + memTx.senders.LoadOrStore(txInfo.SenderID, true) + // TODO: consider punishing peer for dups, + // its non-trivial since invalid txs can become valid, + // but they can spam the same tx with little cost to them atm. + } return ErrTxInCache diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 90d0ed1ae..666d5530a 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -244,7 +244,7 @@ func TestTxsAvailable(t *testing.T) { ensureNoFire(t, mempool.TxsAvailable(), timeoutMS) // now call update with all the txs. it should not fire as there are no txs left - committedTxs = append(txs, moreTxs...) + committedTxs = append(txs, moreTxs...) //nolint: gocritic if err := mempool.Update(2, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { t.Error(err) } @@ -426,6 +426,9 @@ func TestMempoolMaxMsgSize(t *testing.T) { mempl, cleanup := newMempoolWithApp(cc) defer cleanup() + maxTxSize := mempl.config.MaxTxBytes + maxMsgSize := calcMaxMsgSize(maxTxSize) + testCases := []struct { len int err bool @@ -462,7 +465,7 @@ func TestMempoolMaxMsgSize(t *testing.T) { require.NoError(t, err, caseString) } else { require.True(t, len(encoded) > maxMsgSize, caseString) - require.Equal(t, err, ErrTxTooLarge, caseString) + require.Equal(t, err, ErrTxTooLarge{maxTxSize, testCase.len}, caseString) } } diff --git a/mempool/wire.go b/mempool/codec.go similarity index 100% rename from mempool/wire.go rename to mempool/codec.go diff --git a/mempool/errors.go b/mempool/errors.go index ac2a9b3c2..c5140bdf0 100644 --- a/mempool/errors.go +++ b/mempool/errors.go @@ -9,11 +9,18 @@ import ( var ( // ErrTxInCache is returned to the client if we saw tx earlier ErrTxInCache = errors.New("Tx already exists in cache") - - // ErrTxTooLarge means the tx is too big to be sent in a message to other peers - ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize) ) +// ErrTxTooLarge means the tx is too big to be sent in a message to other peers +type ErrTxTooLarge struct { + max int + actual int +} + +func (e ErrTxTooLarge) Error() string { + return fmt.Sprintf("Tx too large. Max size is %d, but got %d", e.max, e.actual) +} + // ErrMempoolIsFull means Tendermint & an application can't handle that much load type ErrMempoolIsFull struct { numTxs int diff --git a/mempool/reactor.go b/mempool/reactor.go index 65ccd7dfd..349ed7ea5 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -19,8 +19,7 @@ import ( const ( MempoolChannel = byte(0x30) - maxMsgSize = 1048576 // 1MB TODO make it configurable - maxTxSize = maxMsgSize - 8 // account for amino overhead of TxMessage + aminoOverheadForTxMessage = 8 peerCatchupSleepIntervalMS = 100 // If peer is behind, sleep this amount @@ -156,7 +155,7 @@ func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { // Receive implements Reactor. // It adds any received transactions to the mempool. func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := decodeMsg(msgBytes) + msg, err := memR.decodeMsg(msgBytes) if err != nil { memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) memR.Switch.StopPeerForError(src, err) @@ -263,9 +262,10 @@ func RegisterMempoolMessages(cdc *amino.Codec) { cdc.RegisterConcrete(&TxMessage{}, "tendermint/mempool/TxMessage", nil) } -func decodeMsg(bz []byte) (msg MempoolMessage, err error) { - if len(bz) > maxMsgSize { - return msg, fmt.Errorf("Msg exceeds max size (%d > %d)", len(bz), maxMsgSize) +func (memR *Reactor) decodeMsg(bz []byte) (msg MempoolMessage, err error) { + maxMsgSize := calcMaxMsgSize(memR.config.MaxTxBytes) + if l := len(bz); l > maxMsgSize { + return msg, ErrTxTooLarge{maxMsgSize, l} } err = cdc.UnmarshalBinaryBare(bz, &msg) return @@ -282,3 +282,9 @@ type TxMessage struct { func (m *TxMessage) String() string { return fmt.Sprintf("[TxMessage %v]", m.Tx) } + +// calcMaxMsgSize returns the max size of TxMessage +// account for amino overhead of TxMessage +func calcMaxMsgSize(maxTxSize int) int { + return maxTxSize + aminoOverheadForTxMessage +} diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index 94c0d1900..dff4c0d68 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -42,10 +42,10 @@ func mempoolLogger() log.Logger { } // connect N mempool reactors through N switches -func makeAndConnectReactors(config *cfg.Config, N int) []*Reactor { - reactors := make([]*Reactor, N) +func makeAndConnectReactors(config *cfg.Config, n int) []*Reactor { + reactors := make([]*Reactor, n) logger := mempoolLogger() - for i := 0; i < N; i++ { + for i := 0; i < n; i++ { app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool, cleanup := newMempoolWithApp(cc) @@ -55,7 +55,7 @@ func makeAndConnectReactors(config *cfg.Config, N int) []*Reactor { reactors[i].SetLogger(logger.With("validator", i)) } - p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { + p2p.MakeConnectedSwitches(config.P2P, n, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("MEMPOOL", reactors[i]) return s diff --git a/node/wire.go b/node/codec.go similarity index 100% rename from node/wire.go rename to node/codec.go diff --git a/node/doc.go b/node/doc.go new file mode 100644 index 000000000..08f3fa258 --- /dev/null +++ b/node/doc.go @@ -0,0 +1,40 @@ +/* +Package node is the main entry point, where the Node struct, which +represents a full node, is defined. + +Adding new p2p.Reactor(s) + +To add a new p2p.Reactor, use the CustomReactors option: + + node, err := NewNode( + config, + privVal, + nodeKey, + clientCreator, + genesisDocProvider, + dbProvider, + metricsProvider, + logger, + CustomReactors(map[string]p2p.Reactor{"CUSTOM": customReactor}), + ) + +Replacing existing p2p.Reactor(s) + +To replace the built-in p2p.Reactor, use the CustomReactors option: + + node, err := NewNode( + config, + privVal, + nodeKey, + clientCreator, + genesisDocProvider, + dbProvider, + metricsProvider, + logger, + CustomReactors(map[string]p2p.Reactor{"BLOCKCHAIN": customBlockchainReactor}), + ) + +The list of existing reactors can be found in CustomReactors documentation. + +*/ +package node diff --git a/node/node.go b/node/node.go index 9beb0669f..5c98ea5bf 100644 --- a/node/node.go +++ b/node/node.go @@ -18,15 +18,14 @@ import ( amino "github.com/tendermint/go-amino" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/blockchain" - bc "github.com/tendermint/tendermint/blockchain" + bcv0 "github.com/tendermint/tendermint/blockchain/v0" + bcv1 "github.com/tendermint/tendermint/blockchain/v1" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/consensus" cs "github.com/tendermint/tendermint/consensus" - "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/evidence" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" mempl "github.com/tendermint/tendermint/mempool" @@ -42,15 +41,13 @@ import ( "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/state/txindex/kv" "github.com/tendermint/tendermint/state/txindex/null" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" + dbm "github.com/tendermint/tm-db" ) -// CustomReactorNamePrefix is a prefix for all custom reactors to prevent -// clashes with built-in reactors. -const CustomReactorNamePrefix = "CUSTOM_" - //------------------------------------------------------------------------------ // DBContext specifies config information for loading a new DB. @@ -143,11 +140,26 @@ func DefaultMetricsProvider(config *cfg.InstrumentationConfig) MetricsProvider { // Option sets a parameter for the node. type Option func(*Node) -// CustomReactors allows you to add custom reactors to the node's Switch. +// CustomReactors allows you to add custom reactors (name -> p2p.Reactor) to +// the node's Switch. +// +// WARNING: using any name from the below list of the existing reactors will +// result in replacing it with the custom one. +// +// - MEMPOOL +// - BLOCKCHAIN +// - CONSENSUS +// - EVIDENCE +// - PEX func CustomReactors(reactors map[string]p2p.Reactor) Option { return func(n *Node) { for name, reactor := range reactors { - n.sw.AddReactor(CustomReactorNamePrefix+name, reactor) + if existingReactor := n.sw.Reactor(name); existingReactor != nil { + n.sw.Logger.Info("Replacing existing reactor with a custom one", + "name", name, "existing", existingReactor, "custom", reactor) + n.sw.RemoveReactor(name, existingReactor) + } + n.sw.AddReactor(name, reactor) } } } @@ -175,9 +187,9 @@ type Node struct { // services eventBus *types.EventBus // pub/sub for services stateDB dbm.DB - blockStore *bc.BlockStore // store the blockchain to disk - bcReactor *bc.BlockchainReactor // for fast-syncing - mempoolReactor *mempl.Reactor // for gossipping transactions + blockStore *store.BlockStore // store the blockchain to disk + bcReactor p2p.Reactor // for fast-syncing + mempoolReactor *mempl.Reactor // for gossipping transactions mempool mempl.Mempool consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus @@ -190,13 +202,13 @@ type Node struct { prometheusSrv *http.Server } -func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *bc.BlockStore, stateDB dbm.DB, err error) { +func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { var blockStoreDB dbm.DB blockStoreDB, err = dbProvider(&DBContext{"blockstore", config}) if err != nil { return } - blockStore = bc.NewBlockStore(blockStoreDB) + blockStore = store.NewBlockStore(blockStoreDB) stateDB, err = dbProvider(&DBContext{"state", config}) if err != nil { @@ -234,11 +246,12 @@ func createAndStartIndexerService(config *cfg.Config, dbProvider DBProvider, if err != nil { return nil, nil, err } - if config.TxIndex.IndexTags != "" { + switch { + case config.TxIndex.IndexTags != "": txIndexer = kv.NewTxIndex(store, kv.IndexTags(splitAndTrimEmpty(config.TxIndex.IndexTags, ",", " "))) - } else if config.TxIndex.IndexAllTags { + case config.TxIndex.IndexAllTags: txIndexer = kv.NewTxIndex(store, kv.IndexAllTags()) - } else { + default: txIndexer = kv.NewTxIndex(store) } default: @@ -265,9 +278,7 @@ func doHandshake(stateDB dbm.DB, state sm.State, blockStore sm.BlockStore, return nil } -func logNodeStartupInfo(state sm.State, privValidator types.PrivValidator, logger, - consensusLogger log.Logger) { - +func logNodeStartupInfo(state sm.State, pubKey crypto.PubKey, logger, consensusLogger log.Logger) { // Log the version info. logger.Info("Version info", "software", version.TMCoreSemVer, @@ -283,7 +294,6 @@ func logNodeStartupInfo(state sm.State, privValidator types.PrivValidator, logge ) } - pubKey := privValidator.GetPubKey() addr := pubKey.Address() // Log whether this node is a validator or an observer if state.Validators.HasAddress(addr) { @@ -337,6 +347,26 @@ func createEvidenceReactor(config *cfg.Config, dbProvider DBProvider, return evidenceReactor, evidencePool, nil } +func createBlockchainReactor(config *cfg.Config, + state sm.State, + blockExec *sm.BlockExecutor, + blockStore *store.BlockStore, + fastSync bool, + logger log.Logger) (bcReactor p2p.Reactor, err error) { + + switch config.FastSync.Version { + case "v0": + bcReactor = bcv0.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + case "v1": + bcReactor = bcv1.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + default: + return nil, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) + } + + bcReactor.SetLogger(logger.With("module", "blockchain")) + return bcReactor, nil +} + func createConsensusReactor(config *cfg.Config, state sm.State, blockExec *sm.BlockExecutor, @@ -431,7 +461,7 @@ func createSwitch(config *cfg.Config, p2pMetrics *p2p.Metrics, peerFilters []p2p.PeerFilterFunc, mempoolReactor *mempl.Reactor, - bcReactor *blockchain.BlockchainReactor, + bcReactor p2p.Reactor, consensusReactor *consensus.ConsensusReactor, evidenceReactor *evidence.EvidenceReactor, nodeInfo p2p.NodeInfo, @@ -568,11 +598,17 @@ func NewNode(config *cfg.Config, } } - logNodeStartupInfo(state, privValidator, logger, consensusLogger) + pubKey := privValidator.GetPubKey() + if pubKey == nil { + // TODO: GetPubKey should return errors - https://github.com/tendermint/tendermint/issues/3602 + return nil, errors.New("could not retrieve public key from private validator") + } + + logNodeStartupInfo(state, pubKey, logger, consensusLogger) // Decide whether to fast-sync or not // We don't fast-sync when the only validator is us. - fastSync := config.FastSync && !onlyValidatorIsUs(state, privValidator) + fastSync := config.FastSyncMode && !onlyValidatorIsUs(state, privValidator) csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) @@ -596,8 +632,10 @@ func NewNode(config *cfg.Config, ) // Make BlockchainReactor - bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - bcReactor.SetLogger(logger.With("module", "blockchain")) + bcReactor, err := createBlockchainReactor(config, state, blockExec, blockStore, fastSync, logger) + if err != nil { + return nil, errors.Wrap(err, "could not create blockchain reactor") + } // Make ConsensusReactor consensusReactor, consensusState := createConsensusReactor( @@ -820,6 +858,17 @@ func (n *Node) startRPC() ([]net.Listener, error) { rpccore.AddUnsafeRoutes() } + config := rpcserver.DefaultConfig() + config.MaxBodyBytes = n.config.RPC.MaxBodyBytes + config.MaxHeaderBytes = n.config.RPC.MaxHeaderBytes + config.MaxOpenConnections = n.config.RPC.MaxOpenConnections + // If necessary adjust global WriteTimeout to ensure it's greater than + // TimeoutBroadcastTxCommit. + // See https://github.com/tendermint/tendermint/issues/3435 + if config.WriteTimeout <= n.config.RPC.TimeoutBroadcastTxCommit { + config.WriteTimeout = n.config.RPC.TimeoutBroadcastTxCommit + 1*time.Second + } + // we may expose the rpc over both a unix and tcp socket listeners := make([]net.Listener, len(listenAddrs)) for i, listenAddr := range listenAddrs { @@ -832,20 +881,12 @@ func (n *Node) startRPC() ([]net.Listener, error) { if err != nil && err != tmpubsub.ErrSubscriptionNotFound { wmLogger.Error("Failed to unsubscribe addr from events", "addr", remoteAddr, "err", err) } - })) + }), + rpcserver.ReadLimit(config.MaxBodyBytes), + ) wm.SetLogger(wmLogger) mux.HandleFunc("/websocket", wm.WebsocketHandler) rpcserver.RegisterRPCFuncs(mux, rpccore.Routes, coreCodec, rpcLogger) - - config := rpcserver.DefaultConfig() - config.MaxOpenConnections = n.config.RPC.MaxOpenConnections - // If necessary adjust global WriteTimeout to ensure it's greater than - // TimeoutBroadcastTxCommit. - // See https://github.com/tendermint/tendermint/issues/3435 - if config.WriteTimeout <= n.config.RPC.TimeoutBroadcastTxCommit { - config.WriteTimeout = n.config.RPC.TimeoutBroadcastTxCommit + 1*time.Second - } - listener, err := rpcserver.Listen( listenAddr, config, @@ -927,7 +968,7 @@ func (n *Node) Switch() *p2p.Switch { } // BlockStore returns the Node's BlockStore. -func (n *Node) BlockStore() *bc.BlockStore { +func (n *Node) BlockStore() *store.BlockStore { return n.blockStore } @@ -1015,6 +1056,17 @@ func makeNodeInfo( if _, ok := txIndexer.(*null.TxIndex); ok { txIndexerStatus = "off" } + + var bcChannel byte + switch config.FastSync.Version { + case "v0": + bcChannel = bcv0.BlockchainChannel + case "v1": + bcChannel = bcv1.BlockchainChannel + default: + return nil, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) + } + nodeInfo := p2p.DefaultNodeInfo{ ProtocolVersion: p2p.NewProtocolVersion( version.P2PProtocol, // global @@ -1025,7 +1077,7 @@ func makeNodeInfo( Network: genDoc.ChainID, Version: version.TMCoreSemVer, Channels: []byte{ - bc.BlockchainChannel, + bcChannel, cs.StateChannel, cs.DataChannel, cs.VoteChannel, cs.VoteSetBitsChannel, mempl.MempoolChannel, evidence.EvidenceChannel, @@ -1109,29 +1161,13 @@ func createAndStartPrivValidatorSocketClient( listenAddr string, logger log.Logger, ) (types.PrivValidator, error) { - var listener net.Listener - - protocol, address := cmn.ProtocolAndAddress(listenAddr) - ln, err := net.Listen(protocol, address) + pve, err := privval.NewSignerListener(listenAddr, logger) if err != nil { - return nil, err - } - switch protocol { - case "unix": - listener = privval.NewUnixListener(ln) - case "tcp": - // TODO: persist this key so external signer - // can actually authenticate us - listener = privval.NewTCPListener(ln, ed25519.GenPrivKey()) - default: - return nil, fmt.Errorf( - "wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", - protocol, - ) + return nil, errors.Wrap(err, "failed to start private validator") } - pvsc := privval.NewSignerValidatorEndpoint(logger.With("module", "privval"), listener) - if err := pvsc.Start(); err != nil { + pvsc, err := privval.NewSignerClient(pve) + if err != nil { return nil, errors.Wrap(err, "failed to start private validator") } diff --git a/node/node_test.go b/node/node_test.go index 841a04686..6cdaceffb 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -17,7 +17,6 @@ import ( "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/evidence" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" @@ -28,6 +27,7 @@ import ( "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" + dbm "github.com/tendermint/tm-db" ) func TestNodeStartStop(t *testing.T) { @@ -136,25 +136,29 @@ func TestNodeSetPrivValTCP(t *testing.T) { config.BaseConfig.PrivValidatorListenAddr = addr dialer := privval.DialTCPFn(addr, 100*time.Millisecond, ed25519.GenPrivKey()) - pvsc := privval.NewSignerServiceEndpoint( + dialerEndpoint := privval.NewSignerDialerEndpoint( log.TestingLogger(), - config.ChainID(), - types.NewMockPV(), dialer, ) - privval.SignerServiceEndpointTimeoutReadWrite(100 * time.Millisecond)(pvsc) + privval.SignerDialerEndpointTimeoutReadWrite(100 * time.Millisecond)(dialerEndpoint) + + signerServer := privval.NewSignerServer( + dialerEndpoint, + config.ChainID(), + types.NewMockPV(), + ) go func() { - err := pvsc.Start() + err := signerServer.Start() if err != nil { panic(err) } }() - defer pvsc.Stop() + defer signerServer.Stop() n, err := DefaultNewNode(config, log.TestingLogger()) require.NoError(t, err) - assert.IsType(t, &privval.SignerValidatorEndpoint{}, n.PrivValidator()) + assert.IsType(t, &privval.SignerClient{}, n.PrivValidator()) } // address without a protocol must result in error @@ -178,13 +182,17 @@ func TestNodeSetPrivValIPC(t *testing.T) { config.BaseConfig.PrivValidatorListenAddr = "unix://" + tmpfile dialer := privval.DialUnixFn(tmpfile) - pvsc := privval.NewSignerServiceEndpoint( + dialerEndpoint := privval.NewSignerDialerEndpoint( log.TestingLogger(), - config.ChainID(), - types.NewMockPV(), dialer, ) - privval.SignerServiceEndpointTimeoutReadWrite(100 * time.Millisecond)(pvsc) + privval.SignerDialerEndpointTimeoutReadWrite(100 * time.Millisecond)(dialerEndpoint) + + pvsc := privval.NewSignerServer( + dialerEndpoint, + config.ChainID(), + types.NewMockPV(), + ) go func() { err := pvsc.Start() @@ -194,8 +202,7 @@ func TestNodeSetPrivValIPC(t *testing.T) { n, err := DefaultNewNode(config, log.TestingLogger()) require.NoError(t, err) - assert.IsType(t, &privval.SignerValidatorEndpoint{}, n.PrivValidator()) - + assert.IsType(t, &privval.SignerClient{}, n.PrivValidator()) } // testFreeAddr claims a free port so we don't block on listener being ready. @@ -288,6 +295,7 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { defer os.RemoveAll(config.RootDir) cr := p2pmock.NewReactor() + customBlockchainReactor := p2pmock.NewReactor() nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) require.NoError(t, err) @@ -300,7 +308,7 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), log.TestingLogger(), - CustomReactors(map[string]p2p.Reactor{"FOO": cr}), + CustomReactors(map[string]p2p.Reactor{"FOO": cr, "BLOCKCHAIN": customBlockchainReactor}), ) require.NoError(t, err) @@ -309,6 +317,10 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { defer n.Stop() assert.True(t, cr.IsRunning()) + assert.Equal(t, cr, n.Switch().Reactor("FOO")) + + assert.True(t, customBlockchainReactor.IsRunning()) + assert.Equal(t, customBlockchainReactor, n.Switch().Reactor("BLOCKCHAIN")) } func state(nVals int, height int64) (sm.State, dbm.DB) { diff --git a/p2p/wire.go b/p2p/codec.go similarity index 100% rename from p2p/wire.go rename to p2p/codec.go diff --git a/p2p/conn/wire.go b/p2p/conn/codec.go similarity index 100% rename from p2p/conn/wire.go rename to p2p/conn/codec.go diff --git a/p2p/conn/connection.go b/p2p/conn/connection.go index ee29fc85c..68066a7c7 100644 --- a/p2p/conn/connection.go +++ b/p2p/conn/connection.go @@ -2,7 +2,8 @@ package conn import ( "bufio" - "errors" + "runtime/debug" + "fmt" "io" "math" @@ -12,6 +13,8 @@ import ( "sync/atomic" "time" + "github.com/pkg/errors" + amino "github.com/tendermint/go-amino" cmn "github.com/tendermint/tendermint/libs/common" flow "github.com/tendermint/tendermint/libs/flowrate" @@ -90,6 +93,9 @@ type MConnection struct { quitSendRoutine chan struct{} doneSendRoutine chan struct{} + // Closing quitRecvRouting will cause the recvRouting to eventually quit. + quitRecvRoutine chan struct{} + // used to ensure FlushStop and OnStop // are safe to call concurrently. stopMtx sync.Mutex @@ -206,6 +212,7 @@ func (c *MConnection) OnStart() error { c.chStatsTimer = time.NewTicker(updateStats) c.quitSendRoutine = make(chan struct{}) c.doneSendRoutine = make(chan struct{}) + c.quitRecvRoutine = make(chan struct{}) go c.sendRoutine() go c.recvRoutine() return nil @@ -220,7 +227,14 @@ func (c *MConnection) stopServices() (alreadyStopped bool) { select { case <-c.quitSendRoutine: - // already quit via FlushStop or OnStop + // already quit + return true + default: + } + + select { + case <-c.quitRecvRoutine: + // already quit return true default: } @@ -230,6 +244,8 @@ func (c *MConnection) stopServices() (alreadyStopped bool) { c.pingTimer.Stop() c.chStatsTimer.Stop() + // inform the recvRouting that we are shutting down + close(c.quitRecvRoutine) close(c.quitSendRoutine) return false } @@ -250,8 +266,6 @@ func (c *MConnection) FlushStop() { <-c.doneSendRoutine // Send and flush all pending msgs. - // By now, IsRunning == false, - // so any concurrent attempts to send will fail. // Since sendRoutine has exited, we can call this // safely eof := c.sendSomePacketMsgs() @@ -302,8 +316,8 @@ func (c *MConnection) flush() { // Catch panics, usually caused by remote disconnects. func (c *MConnection) _recover() { if r := recover(); r != nil { - err := cmn.ErrorWrap(r, "recovered panic in MConnection") - c.stopForError(err) + c.Logger.Error("MConnection panicked", "err", r, "stack", string(debug.Stack())) + c.stopForError(errors.Errorf("recovered from panic: %v", r)) } } @@ -550,9 +564,22 @@ FOR_LOOP: var err error _n, err = cdc.UnmarshalBinaryLengthPrefixedReader(c.bufConnReader, &packet, int64(c._maxPacketMsgSize)) c.recvMonitor.Update(int(_n)) + if err != nil { + // stopServices was invoked and we are shutting down + // receiving is excpected to fail since we will close the connection + select { + case <-c.quitRecvRoutine: + break FOR_LOOP + default: + } + if c.IsRunning() { - c.Logger.Error("Connection failed @ recvRoutine (reading byte)", "conn", c, "err", err) + if err == io.EOF { + c.Logger.Info("Connection is closed @ recvRoutine (likely by the other side)", "conn", c) + } else { + c.Logger.Error("Connection failed @ recvRoutine (reading byte)", "conn", c, "err", err) + } c.stopForError(err) } break FOR_LOOP diff --git a/p2p/conn/connection_test.go b/p2p/conn/connection_test.go index 283b00ebe..91e3e2099 100644 --- a/p2p/conn/connection_test.go +++ b/p2p/conn/connection_test.go @@ -57,7 +57,8 @@ func TestMConnectionSendFlushStop(t *testing.T) { msgB := make([]byte, aminoMsgLength) _, err := server.Read(msgB) if err != nil { - t.Fatal(err) + t.Error(err) + return } errCh <- err }() diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index 7f76ac800..a4489f475 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -2,6 +2,7 @@ package conn import ( "bytes" + "crypto/cipher" crand "crypto/rand" "crypto/sha256" "crypto/subtle" @@ -17,6 +18,7 @@ import ( "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" + pool "github.com/libp2p/go-buffer-pool" "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" "golang.org/x/crypto/hkdf" @@ -47,10 +49,11 @@ var ( type SecretConnection struct { // immutable - recvSecret *[aeadKeySize]byte - sendSecret *[aeadKeySize]byte - remPubKey crypto.PubKey - conn io.ReadWriteCloser + recvAead cipher.AEAD + sendAead cipher.AEAD + + remPubKey crypto.PubKey + conn io.ReadWriteCloser // net.Conn must be thread safe: // https://golang.org/pkg/net/#Conn. @@ -102,14 +105,22 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* // generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast) + sendAead, err := chacha20poly1305.New(sendSecret[:]) + if err != nil { + return nil, errors.New("Invalid send SecretConnection Key") + } + recvAead, err := chacha20poly1305.New(recvSecret[:]) + if err != nil { + return nil, errors.New("Invalid receive SecretConnection Key") + } // Construct SecretConnection. sc := &SecretConnection{ conn: conn, recvBuffer: nil, recvNonce: new([aeadNonceSize]byte), sendNonce: new([aeadNonceSize]byte), - recvSecret: recvSecret, - sendSecret: sendSecret, + recvAead: recvAead, + sendAead: sendAead, } // Sign the challenge bytes for authentication. @@ -143,35 +154,39 @@ func (sc *SecretConnection) Write(data []byte) (n int, err error) { defer sc.sendMtx.Unlock() for 0 < len(data) { - var frame = make([]byte, totalFrameSize) - var chunk []byte - if dataMaxSize < len(data) { - chunk = data[:dataMaxSize] - data = data[dataMaxSize:] - } else { - chunk = data - data = nil - } - chunkLength := len(chunk) - binary.LittleEndian.PutUint32(frame, uint32(chunkLength)) - copy(frame[dataLenSize:], chunk) + if err := func() error { + var sealedFrame = pool.Get(aeadSizeOverhead + totalFrameSize) + var frame = pool.Get(totalFrameSize) + defer func() { + pool.Put(sealedFrame) + pool.Put(frame) + }() + var chunk []byte + if dataMaxSize < len(data) { + chunk = data[:dataMaxSize] + data = data[dataMaxSize:] + } else { + chunk = data + data = nil + } + chunkLength := len(chunk) + binary.LittleEndian.PutUint32(frame, uint32(chunkLength)) + copy(frame[dataLenSize:], chunk) - aead, err := chacha20poly1305.New(sc.sendSecret[:]) - if err != nil { - return n, errors.New("Invalid SecretConnection Key") - } + // encrypt the frame + sc.sendAead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil) + incrNonce(sc.sendNonce) + // end encryption - // encrypt the frame - var sealedFrame = make([]byte, aeadSizeOverhead+totalFrameSize) - aead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil) - incrNonce(sc.sendNonce) - // end encryption - - _, err = sc.conn.Write(sealedFrame) - if err != nil { + _, err = sc.conn.Write(sealedFrame) + if err != nil { + return err + } + n += len(chunk) + return nil + }(); err != nil { return n, err } - n += len(chunk) } return } @@ -189,21 +204,18 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) { } // read off the conn - sealedFrame := make([]byte, totalFrameSize+aeadSizeOverhead) + var sealedFrame = pool.Get(aeadSizeOverhead + totalFrameSize) + defer pool.Put(sealedFrame) _, err = io.ReadFull(sc.conn, sealedFrame) if err != nil { return } - aead, err := chacha20poly1305.New(sc.recvSecret[:]) - if err != nil { - return n, errors.New("Invalid SecretConnection Key") - } - // decrypt the frame. // reads and updates the sc.recvNonce - var frame = make([]byte, totalFrameSize) - _, err = aead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) + var frame = pool.Get(totalFrameSize) + defer pool.Put(frame) + _, err = sc.recvAead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) if err != nil { return n, errors.New("Failed to decrypt SecretConnection") } @@ -218,7 +230,10 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) { } var chunk = frame[dataLenSize : dataLenSize+chunkLength] n = copy(data, chunk) - sc.recvBuffer = chunk[n:] + if n < len(chunk) { + sc.recvBuffer = make([]byte, len(chunk)-n) + copy(sc.recvBuffer, chunk[n:]) + } return } diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index 7e264e913..9ab9695a3 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -192,7 +192,8 @@ func writeLots(t *testing.T, wg *sync.WaitGroup, conn net.Conn, txt string, n in for i := 0; i < n; i++ { _, err := conn.Write([]byte(txt)) if err != nil { - t.Fatalf("Failed to write to fooSecConn: %v", err) + t.Errorf("Failed to write to fooSecConn: %v", err) + return } } } @@ -383,10 +384,23 @@ func createGoldenTestVectors(t *testing.T) string { return data } -func BenchmarkSecretConnection(b *testing.B) { +func BenchmarkWriteSecretConnection(b *testing.B) { b.StopTimer() + b.ReportAllocs() fooSecConn, barSecConn := makeSecretConnPair(b) - fooWriteText := cmn.RandStr(dataMaxSize) + randomMsgSizes := []int{ + dataMaxSize / 10, + dataMaxSize / 3, + dataMaxSize / 2, + dataMaxSize, + dataMaxSize * 3 / 2, + dataMaxSize * 2, + dataMaxSize * 7 / 2, + } + fooWriteBytes := make([][]byte, 0, len(randomMsgSizes)) + for _, size := range randomMsgSizes { + fooWriteBytes = append(fooWriteBytes, cmn.RandBytes(size)) + } // Consume reads from bar's reader go func() { readBuffer := make([]byte, dataMaxSize) @@ -395,16 +409,19 @@ func BenchmarkSecretConnection(b *testing.B) { if err == io.EOF { return } else if err != nil { - b.Fatalf("Failed to read from barSecConn: %v", err) + b.Errorf("Failed to read from barSecConn: %v", err) + return } } }() b.StartTimer() for i := 0; i < b.N; i++ { - _, err := fooSecConn.Write([]byte(fooWriteText)) + idx := cmn.RandIntn(len(fooWriteBytes)) + _, err := fooSecConn.Write(fooWriteBytes[idx]) if err != nil { - b.Fatalf("Failed to write to fooSecConn: %v", err) + b.Errorf("Failed to write to fooSecConn: %v", err) + return } } b.StopTimer() @@ -414,3 +431,45 @@ func BenchmarkSecretConnection(b *testing.B) { } //barSecConn.Close() race condition } + +func BenchmarkReadSecretConnection(b *testing.B) { + b.StopTimer() + b.ReportAllocs() + fooSecConn, barSecConn := makeSecretConnPair(b) + randomMsgSizes := []int{ + dataMaxSize / 10, + dataMaxSize / 3, + dataMaxSize / 2, + dataMaxSize, + dataMaxSize * 3 / 2, + dataMaxSize * 2, + dataMaxSize * 7 / 2, + } + fooWriteBytes := make([][]byte, 0, len(randomMsgSizes)) + for _, size := range randomMsgSizes { + fooWriteBytes = append(fooWriteBytes, cmn.RandBytes(size)) + } + go func() { + for i := 0; i < b.N; i++ { + idx := cmn.RandIntn(len(fooWriteBytes)) + _, err := fooSecConn.Write(fooWriteBytes[idx]) + if err != nil { + b.Errorf("Failed to write to fooSecConn: %v, %v,%v", err, i, b.N) + return + } + } + }() + + b.StartTimer() + for i := 0; i < b.N; i++ { + readBuffer := make([]byte, dataMaxSize) + _, err := barSecConn.Read(readBuffer) + + if err == io.EOF { + return + } else if err != nil { + b.Fatalf("Failed to read from barSecConn: %v", err) + } + } + b.StopTimer() +} diff --git a/p2p/fuzz.go b/p2p/fuzz.go index 80e4fed6a..135155d86 100644 --- a/p2p/fuzz.go +++ b/p2p/fuzz.go @@ -117,14 +117,15 @@ func (fc *FuzzedConnection) fuzz() bool { case config.FuzzModeDrop: // randomly drop the r/w, drop the conn, or sleep r := cmn.RandFloat64() - if r <= fc.config.ProbDropRW { + switch { + case r <= fc.config.ProbDropRW: return true - } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn { + case r < fc.config.ProbDropRW+fc.config.ProbDropConn: // XXX: can't this fail because machine precision? // XXX: do we need an error? fc.Close() // nolint: errcheck, gas return true - } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep { + case r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep: time.Sleep(fc.randomDuration()) } case config.FuzzModeDelay: diff --git a/p2p/netaddress.go b/p2p/netaddress.go index d11504525..04a2b843f 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "errors" + "github.com/pkg/errors" ) // NetAddress defines information about a peer on the network @@ -40,7 +40,7 @@ func IDAddressString(id ID, protocolHostPort string) string { // NewNetAddress returns a new NetAddress using the provided TCP // address. When testing, other net.Addr (except TCP) will result in // using 0.0.0.0:0. When normal run, other net.Addr (except TCP) will -// panic. +// panic. Panics if ID is invalid. // TODO: socks proxies? func NewNetAddress(id ID, addr net.Addr) *NetAddress { tcpAddr, ok := addr.(*net.TCPAddr) @@ -53,6 +53,11 @@ func NewNetAddress(id ID, addr net.Addr) *NetAddress { return netAddr } } + + if err := validateID(id); err != nil { + panic(fmt.Sprintf("Invalid ID %v: %v (addr: %v)", id, err, addr)) + } + ip := tcpAddr.IP port := uint16(tcpAddr.Port) na := NewNetAddressIPPort(ip, port) @@ -72,18 +77,11 @@ func NewNetAddressString(addr string) (*NetAddress, error) { } // get ID - idStr := spl[0] - idBytes, err := hex.DecodeString(idStr) - if err != nil { + if err := validateID(ID(spl[0])); err != nil { return nil, ErrNetAddressInvalid{addrWithoutProtocol, err} } - if len(idBytes) != IDByteLength { - return nil, ErrNetAddressInvalid{ - addrWithoutProtocol, - fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength)} - } var id ID - id, addrWithoutProtocol = ID(idStr), spl[1] + id, addrWithoutProtocol = ID(spl[0]), spl[1] // get host and port host, portStr, err := net.SplitHostPort(addrWithoutProtocol) @@ -207,22 +205,28 @@ func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) { // Routable returns true if the address is routable. func (na *NetAddress) Routable() bool { + if err := na.Valid(); err != nil { + return false + } // TODO(oga) bitcoind doesn't include RFC3849 here, but should we? - return na.Valid() && !(na.RFC1918() || na.RFC3927() || na.RFC4862() || + return !(na.RFC1918() || na.RFC3927() || na.RFC4862() || na.RFC4193() || na.RFC4843() || na.Local()) } // For IPv4 these are either a 0 or all bits set address. For IPv6 a zero // address or one that matches the RFC3849 documentation address format. -func (na *NetAddress) Valid() bool { - if string(na.ID) != "" { - data, err := hex.DecodeString(string(na.ID)) - if err != nil || len(data) != IDByteLength { - return false - } +func (na *NetAddress) Valid() error { + if err := validateID(na.ID); err != nil { + return errors.Wrap(err, "invalid ID") } - return na.IP != nil && !(na.IP.IsUnspecified() || na.RFC3849() || - na.IP.Equal(net.IPv4bcast)) + + if na.IP == nil { + return errors.New("no IP") + } + if na.IP.IsUnspecified() || na.RFC3849() || na.IP.Equal(net.IPv4bcast) { + return errors.New("invalid IP") + } + return nil } // HasID returns true if the address has an ID. @@ -246,36 +250,39 @@ func (na *NetAddress) ReachabilityTo(o *NetAddress) int { Ipv4 Ipv6_strong ) - if !na.Routable() { + switch { + case !na.Routable(): return Unreachable - } else if na.RFC4380() { - if !o.Routable() { + case na.RFC4380(): + switch { + case !o.Routable(): return Default - } else if o.RFC4380() { + case o.RFC4380(): return Teredo - } else if o.IP.To4() != nil { + case o.IP.To4() != nil: return Ipv4 - } else { // ipv6 + default: // ipv6 return Ipv6_weak } - } else if na.IP.To4() != nil { + case na.IP.To4() != nil: if o.Routable() && o.IP.To4() != nil { return Ipv4 } return Default - } else /* ipv6 */ { + default: /* ipv6 */ var tunnelled bool // Is our v6 is tunnelled? if o.RFC3964() || o.RFC6052() || o.RFC6145() { tunnelled = true } - if !o.Routable() { + switch { + case !o.Routable(): return Default - } else if o.RFC4380() { + case o.RFC4380(): return Teredo - } else if o.IP.To4() != nil { + case o.IP.To4() != nil: return Ipv4 - } else if tunnelled { + case tunnelled: // only prioritise ipv6 if we aren't tunnelling it. return Ipv6_weak } @@ -329,3 +336,17 @@ func removeProtocolIfDefined(addr string) string { return addr } + +func validateID(id ID) error { + if len(id) == 0 { + return errors.New("no ID") + } + idBytes, err := hex.DecodeString(string(id)) + if err != nil { + return err + } + if len(idBytes) != IDByteLength { + return fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength) + } + return nil +} diff --git a/p2p/netaddress_test.go b/p2p/netaddress_test.go index 7afcab131..e7d82cd77 100644 --- a/p2p/netaddress_test.go +++ b/p2p/netaddress_test.go @@ -11,9 +11,13 @@ import ( func TestNewNetAddress(t *testing.T) { tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") require.Nil(t, err) - addr := NewNetAddress("", tcpAddr) - assert.Equal(t, "127.0.0.1:8080", addr.String()) + assert.Panics(t, func() { + NewNetAddress("", tcpAddr) + }) + + addr := NewNetAddress("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", tcpAddr) + assert.Equal(t, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", addr.String()) assert.NotPanics(t, func() { NewNetAddress("", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) @@ -106,7 +110,12 @@ func TestNetAddressProperties(t *testing.T) { addr, err := NewNetAddressString(tc.addr) require.Nil(t, err) - assert.Equal(t, tc.valid, addr.Valid()) + err = addr.Valid() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } assert.Equal(t, tc.local, addr.Local()) assert.Equal(t, tc.routable, addr.Routable()) } diff --git a/p2p/node_info_test.go b/p2p/node_info_test.go index 19567d2bf..e90f397cf 100644 --- a/p2p/node_info_test.go +++ b/p2p/node_info_test.go @@ -19,7 +19,7 @@ func TestNodeInfoValidate(t *testing.T) { channels[i] = byte(i) } dupChannels := make([]byte, 5) - copy(dupChannels[:], channels[:5]) + copy(dupChannels, channels[:5]) dupChannels = append(dupChannels, testCh) nonAscii := "¢§µ" @@ -31,7 +31,7 @@ func TestNodeInfoValidate(t *testing.T) { malleateNodeInfo func(*DefaultNodeInfo) expectErr bool }{ - {"Too Many Channels", func(ni *DefaultNodeInfo) { ni.Channels = append(channels, byte(maxNumChannels)) }, true}, + {"Too Many Channels", func(ni *DefaultNodeInfo) { ni.Channels = append(channels, byte(maxNumChannels)) }, true}, // nolint: gocritic {"Duplicate Channel", func(ni *DefaultNodeInfo) { ni.Channels = dupChannels }, true}, {"Good Channels", func(ni *DefaultNodeInfo) { ni.Channels = ni.Channels[:5] }, false}, diff --git a/p2p/peer_test.go b/p2p/peer_test.go index bf61beb4f..37a3009c0 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -113,13 +114,13 @@ func testOutboundPeerConn( var pc peerConn conn, err := testDial(addr, config) if err != nil { - return pc, cmn.ErrorWrap(err, "Error creating peer") + return pc, errors.Wrap(err, "Error creating peer") } pc, err = testPeerConn(conn, config, true, persistent, ourNodePrivKey, addr) if err != nil { if cerr := conn.Close(); cerr != nil { - return pc, cmn.ErrorWrap(err, cerr.Error()) + return pc, errors.Wrap(err, cerr.Error()) } return pc, err } @@ -127,7 +128,7 @@ func testOutboundPeerConn( // ensure dialed ID matches connection ID if addr.ID != pc.ID() { if cerr := conn.Close(); cerr != nil { - return pc, cmn.ErrorWrap(err, cerr.Error()) + return pc, errors.Wrap(err, cerr.Error()) } return pc, ErrSwitchAuthenticationFailure{addr, pc.ID()} } diff --git a/p2p/pex/addrbook.go b/p2p/pex/addrbook.go index 85dd05248..27bcef9e8 100644 --- a/p2p/pex/addrbook.go +++ b/p2p/pex/addrbook.go @@ -178,11 +178,11 @@ func (a *addrBook) OurAddress(addr *p2p.NetAddress) bool { return ok } -func (a *addrBook) AddPrivateIDs(IDs []string) { +func (a *addrBook) AddPrivateIDs(ids []string) { a.mtx.Lock() defer a.mtx.Unlock() - for _, id := range IDs { + for _, id := range ids { a.privateIDs[p2p.ID(id)] = struct{}{} } } @@ -586,8 +586,8 @@ func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { return ErrAddrBookNilAddr{addr, src} } - if !addr.HasID() { - return ErrAddrBookInvalidAddrNoID{addr} + if err := addr.Valid(); err != nil { + return ErrAddrBookInvalidAddr{Addr: addr, AddrErr: err} } if _, ok := a.privateIDs[addr.ID]; ok { @@ -607,10 +607,6 @@ func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { return ErrAddrBookNonRoutable{addr} } - if !addr.Valid() { - return ErrAddrBookInvalidAddr{addr} - } - ka := a.addrLookup[addr.ID] if ka != nil { // If its already old and the addr is the same, ignore it. @@ -647,7 +643,7 @@ func (a *addrBook) randomPickAddresses(bucketType byte, num int) []*p2p.NetAddre } total := 0 for _, bucket := range buckets { - total = total + len(bucket) + total += len(bucket) } addresses := make([]*knownAddress, 0, total) for _, bucket := range buckets { diff --git a/p2p/pex/wire.go b/p2p/pex/codec.go similarity index 100% rename from p2p/pex/wire.go rename to p2p/pex/codec.go diff --git a/p2p/pex/errors.go b/p2p/pex/errors.go index 543056af5..911389a9e 100644 --- a/p2p/pex/errors.go +++ b/p2p/pex/errors.go @@ -56,17 +56,10 @@ func (err ErrAddrBookNilAddr) Error() string { } type ErrAddrBookInvalidAddr struct { - Addr *p2p.NetAddress + Addr *p2p.NetAddress + AddrErr error } func (err ErrAddrBookInvalidAddr) Error() string { - return fmt.Sprintf("Cannot add invalid address %v", err.Addr) -} - -type ErrAddrBookInvalidAddrNoID struct { - Addr *p2p.NetAddress -} - -func (err ErrAddrBookInvalidAddrNoID) Error() string { - return fmt.Sprintf("Cannot add address with no ID %v", err.Addr) + return fmt.Sprintf("Cannot add invalid address %v: %v", err.Addr, err.AddrErr) } diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 20862d323..55cde5a35 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -350,22 +350,8 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { } for _, netAddr := range addrs { - // Validate netAddr. Disconnect from a peer if it sends us invalid data. - if netAddr == nil { - return errors.New("nil address in pexAddrsMessage") - } - // TODO: extract validating logic from NewNetAddressString - // and put it in netAddr#Valid (#2722) - na, err := p2p.NewNetAddressString(netAddr.String()) - if err != nil { - return fmt.Errorf("%s address in pexAddrsMessage is invalid: %v", - netAddr.String(), - err, - ) - } - // NOTE: we check netAddr validity and routability in book#AddAddress. - err = r.book.AddAddress(na, srcAddr) + err = r.book.AddAddress(netAddr, srcAddr) if err != nil { r.logErrAddrBook(err) // XXX: should we be strict about incoming data and disconnect from a @@ -608,7 +594,10 @@ func (r *PEXReactor) dialSeeds() { } r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) } - r.Switch.Logger.Error("Couldn't connect to any seeds") + // do not write error message if there were no seeds specified in config + if len(r.seedAddrs) > 0 { + r.Switch.Logger.Error("Couldn't connect to any seeds") + } } // AttemptsToDial returns the number of attempts to dial specific address. It diff --git a/p2p/switch.go b/p2p/switch.go index 7e681d67c..4898b80c9 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -152,11 +152,9 @@ func WithMetrics(metrics *Metrics) SwitchOption { // AddReactor adds the given reactor to the switch. // NOTE: Not goroutine safe. func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { - // Validate the reactor. - // No two reactors can share the same channel. - reactorChannels := reactor.GetChannels() - for _, chDesc := range reactorChannels { + for _, chDesc := range reactor.GetChannels() { chID := chDesc.ID + // No two reactors can share the same channel. if sw.reactorsByCh[chID] != nil { panic(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) } @@ -168,6 +166,23 @@ func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { return reactor } +// RemoveReactor removes the given Reactor from the Switch. +// NOTE: Not goroutine safe. +func (sw *Switch) RemoveReactor(name string, reactor Reactor) { + for _, chDesc := range reactor.GetChannels() { + // remove channel description + for i := 0; i < len(sw.chDescs); i++ { + if chDesc.ID == sw.chDescs[i].ID { + sw.chDescs = append(sw.chDescs[:i], sw.chDescs[i+1:]...) + break + } + } + delete(sw.reactorsByCh, chDesc.ID) + } + delete(sw.reactors, name) + reactor.SetSwitch(nil) +} + // Reactors returns a map of reactors registered on the switch. // NOTE: Not goroutine safe. func (sw *Switch) Reactors() map[string]Reactor { @@ -207,7 +222,7 @@ func (sw *Switch) OnStart() error { for _, reactor := range sw.reactors { err := reactor.Start() if err != nil { - return cmn.ErrorWrap(err, "failed to start %v", reactor) + return errors.Wrapf(err, "failed to start %v", reactor) } } @@ -664,8 +679,7 @@ func (sw *Switch) addOutboundPeerWithConfig( metrics: sw.metrics, }) if err != nil { - switch e := err.(type) { - case ErrRejected: + if e, ok := err.(ErrRejected); ok { if e.IsSelf() { // Remove the given address from the address book and add to our addresses // to avoid dialing in the future. diff --git a/p2p/switch_test.go b/p2p/switch_test.go index aa5ca78bf..0879acc2d 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - stdprometheus "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -348,7 +348,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { } func TestSwitchStopPeerForError(t *testing.T) { - s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) + s := httptest.NewServer(promhttp.Handler()) defer s.Close() scrapeMetrics := func() string { diff --git a/p2p/test_util.go b/p2p/test_util.go index fa175aeb4..a14073f99 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -5,6 +5,8 @@ import ( "net" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" @@ -233,7 +235,7 @@ func testPeerConn( // Encrypt connection conn, err = upgradeSecretConn(conn, cfg.HandshakeTimeout, ourNodePrivKey) if err != nil { - return pc, cmn.ErrorWrap(err, "Error creating peer") + return pc, errors.Wrap(err, "Error creating peer") } // Only the information we already have diff --git a/p2p/trust/store.go b/p2p/trust/store.go index fc1ad399e..b0324a1a7 100644 --- a/p2p/trust/store.go +++ b/p2p/trust/store.go @@ -10,7 +10,7 @@ import ( "time" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" + dbm "github.com/tendermint/tm-db" ) const defaultStorePeriodicSaveInterval = 1 * time.Minute diff --git a/p2p/trust/store_test.go b/p2p/trust/store_test.go index e1bea8636..d6498d823 100644 --- a/p2p/trust/store_test.go +++ b/p2p/trust/store_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" ) func TestTrustMetricStoreSaveLoad(t *testing.T) { diff --git a/privval/wire.go b/privval/codec.go similarity index 100% rename from privval/wire.go rename to privval/codec.go diff --git a/privval/doc.go b/privval/doc.go index 80869a6a7..ad60673b6 100644 --- a/privval/doc.go +++ b/privval/doc.go @@ -6,16 +6,16 @@ FilePV FilePV is the simplest implementation and developer default. It uses one file for the private key and another to store state. -SignerValidatorEndpoint +SignerListenerEndpoint -SignerValidatorEndpoint establishes a connection to an external process, like a Key Management Server (KMS), using a socket. -SignerValidatorEndpoint listens for the external KMS process to dial in. -SignerValidatorEndpoint takes a listener, which determines the type of connection +SignerListenerEndpoint establishes a connection to an external process, like a Key Management Server (KMS), using a socket. +SignerListenerEndpoint listens for the external KMS process to dial in. +SignerListenerEndpoint takes a listener, which determines the type of connection (ie. encrypted over tcp, or unencrypted over unix). -SignerServiceEndpoint +SignerDialerEndpoint -SignerServiceEndpoint is a simple wrapper around a net.Conn. It's used by both IPCVal and TCPVal. +SignerDialerEndpoint is a simple wrapper around a net.Conn. It's used by both IPCVal and TCPVal. */ package privval diff --git a/privval/errors.go b/privval/errors.go index 75fb25fc6..9f151f11d 100644 --- a/privval/errors.go +++ b/privval/errors.go @@ -4,10 +4,21 @@ import ( "fmt" ) +type EndpointTimeoutError struct{} + +// Implement the net.Error interface. +func (e EndpointTimeoutError) Error() string { return "endpoint connection timed out" } +func (e EndpointTimeoutError) Timeout() bool { return true } +func (e EndpointTimeoutError) Temporary() bool { return true } + // Socket errors. var ( ErrUnexpectedResponse = fmt.Errorf("received unexpected response") - ErrConnTimeout = fmt.Errorf("remote signer timed out") + ErrNoConnection = fmt.Errorf("endpoint is not connected") + ErrConnectionTimeout = EndpointTimeoutError{} + + ErrReadTimeout = fmt.Errorf("endpoint read timed out") + ErrWriteTimeout = fmt.Errorf("endpoint write timed out") ) // RemoteSignerError allows (remote) validators to include meaningful error descriptions in their reply. @@ -18,5 +29,5 @@ type RemoteSignerError struct { } func (e *RemoteSignerError) Error() string { - return fmt.Sprintf("signerServiceEndpoint returned error #%d: %s", e.Code, e.Description) + return fmt.Sprintf("signerEndpoint returned error #%d: %s", e.Code, e.Description) } diff --git a/privval/file_deprecated_test.go b/privval/file_deprecated_test.go index 46391a3fe..e678bfc09 100644 --- a/privval/file_deprecated_test.go +++ b/privval/file_deprecated_test.go @@ -67,11 +67,11 @@ func assertEqualPV(t *testing.T, oldPV *privval.OldFilePV, newPV *privval.FilePV } func initTmpOldFile(t *testing.T) string { - tmpfile, err := ioutil.TempFile("", "priv_validator_*.json") + tmpFile, err := ioutil.TempFile("", "priv_validator_*.json") require.NoError(t, err) - t.Logf("created test file %s", tmpfile.Name()) - _, err = tmpfile.WriteString(oldPrivvalContent) + t.Logf("created test file %s", tmpFile.Name()) + _, err = tmpFile.WriteString(oldPrivvalContent) require.NoError(t, err) - return tmpfile.Name() + return tmpFile.Name() } diff --git a/privval/file_test.go b/privval/file_test.go index 98de69480..38f6e6fe3 100644 --- a/privval/file_test.go +++ b/privval/file_test.go @@ -58,7 +58,7 @@ func TestResetValidator(t *testing.T) { // priv val after signing is not same as empty assert.NotEqual(t, privVal.LastSignState, emptyState) - // priv val after reset is same as empty + // priv val after AcceptNewConnection is same as empty privVal.Reset() assert.Equal(t, privVal.LastSignState, emptyState) } @@ -164,6 +164,7 @@ func TestSignVote(t *testing.T) { block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{}} + height, round := int64(10), 1 voteType := byte(types.PrevoteType) diff --git a/privval/messages.go b/privval/messages.go index 6774a2795..c172a5ea1 100644 --- a/privval/messages.go +++ b/privval/messages.go @@ -6,56 +6,59 @@ import ( "github.com/tendermint/tendermint/types" ) -// RemoteSignerMsg is sent between SignerServiceEndpoint and the SignerServiceEndpoint client. -type RemoteSignerMsg interface{} +// SignerMessage is sent between Signer Clients and Servers. +type SignerMessage interface{} func RegisterRemoteSignerMsg(cdc *amino.Codec) { - cdc.RegisterInterface((*RemoteSignerMsg)(nil), nil) + cdc.RegisterInterface((*SignerMessage)(nil), nil) cdc.RegisterConcrete(&PubKeyRequest{}, "tendermint/remotesigner/PubKeyRequest", nil) cdc.RegisterConcrete(&PubKeyResponse{}, "tendermint/remotesigner/PubKeyResponse", nil) cdc.RegisterConcrete(&SignVoteRequest{}, "tendermint/remotesigner/SignVoteRequest", nil) cdc.RegisterConcrete(&SignedVoteResponse{}, "tendermint/remotesigner/SignedVoteResponse", nil) cdc.RegisterConcrete(&SignProposalRequest{}, "tendermint/remotesigner/SignProposalRequest", nil) cdc.RegisterConcrete(&SignedProposalResponse{}, "tendermint/remotesigner/SignedProposalResponse", nil) + cdc.RegisterConcrete(&PingRequest{}, "tendermint/remotesigner/PingRequest", nil) cdc.RegisterConcrete(&PingResponse{}, "tendermint/remotesigner/PingResponse", nil) } +// TODO: Add ChainIDRequest + // PubKeyRequest requests the consensus public key from the remote signer. type PubKeyRequest struct{} -// PubKeyResponse is a PrivValidatorSocket message containing the public key. +// PubKeyResponse is a response message containing the public key. type PubKeyResponse struct { PubKey crypto.PubKey Error *RemoteSignerError } -// SignVoteRequest is a PrivValidatorSocket message containing a vote. +// SignVoteRequest is a request to sign a vote type SignVoteRequest struct { Vote *types.Vote } -// SignedVoteResponse is a PrivValidatorSocket message containing a signed vote along with a potenial error message. +// SignedVoteResponse is a response containing a signed vote or an error type SignedVoteResponse struct { Vote *types.Vote Error *RemoteSignerError } -// SignProposalRequest is a PrivValidatorSocket message containing a Proposal. +// SignProposalRequest is a request to sign a proposal type SignProposalRequest struct { Proposal *types.Proposal } -// SignedProposalResponse is a PrivValidatorSocket message containing a proposal response +// SignedProposalResponse is response containing a signed proposal or an error type SignedProposalResponse struct { Proposal *types.Proposal Error *RemoteSignerError } -// PingRequest is a PrivValidatorSocket message to keep the connection alive. +// PingRequest is a request to confirm that the connection is alive. type PingRequest struct { } -// PingRequest is a PrivValidatorSocket response to keep the connection alive. +// PingResponse is a response to confirm that the connection is alive. type PingResponse struct { } diff --git a/privval/signer_client.go b/privval/signer_client.go new file mode 100644 index 000000000..0885ee4aa --- /dev/null +++ b/privval/signer_client.go @@ -0,0 +1,131 @@ +package privval + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/types" +) + +// SignerClient implements PrivValidator. +// Handles remote validator connections that provide signing services +type SignerClient struct { + endpoint *SignerListenerEndpoint +} + +var _ types.PrivValidator = (*SignerClient)(nil) + +// NewSignerClient returns an instance of SignerClient. +// it will start the endpoint (if not already started) +func NewSignerClient(endpoint *SignerListenerEndpoint) (*SignerClient, error) { + if !endpoint.IsRunning() { + if err := endpoint.Start(); err != nil { + return nil, errors.Wrap(err, "failed to start listener endpoint") + } + } + + return &SignerClient{endpoint: endpoint}, nil +} + +// Close closes the underlying connection +func (sc *SignerClient) Close() error { + return sc.endpoint.Close() +} + +// IsConnected indicates with the signer is connected to a remote signing service +func (sc *SignerClient) IsConnected() bool { + return sc.endpoint.IsConnected() +} + +// WaitForConnection waits maxWait for a connection or returns a timeout error +func (sc *SignerClient) WaitForConnection(maxWait time.Duration) error { + return sc.endpoint.WaitForConnection(maxWait) +} + +//-------------------------------------------------------- +// Implement PrivValidator + +// Ping sends a ping request to the remote signer +func (sc *SignerClient) Ping() error { + response, err := sc.endpoint.SendRequest(&PingRequest{}) + + if err != nil { + sc.endpoint.Logger.Error("SignerClient::Ping", "err", err) + return nil + } + + _, ok := response.(*PingResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::Ping", "err", "response != PingResponse") + return err + } + + return nil +} + +// GetPubKey retrieves a public key from a remote signer +func (sc *SignerClient) GetPubKey() crypto.PubKey { + response, err := sc.endpoint.SendRequest(&PubKeyRequest{}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", err) + return nil + } + + pubKeyResp, ok := response.(*PubKeyResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", "response != PubKeyResponse") + return nil + } + + if pubKeyResp.Error != nil { + sc.endpoint.Logger.Error("failed to get private validator's public key", "err", pubKeyResp.Error) + return nil + } + + return pubKeyResp.PubKey +} + +// SignVote requests a remote signer to sign a vote +func (sc *SignerClient) SignVote(chainID string, vote *types.Vote) error { + response, err := sc.endpoint.SendRequest(&SignVoteRequest{Vote: vote}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::SignVote", "err", err) + return err + } + + resp, ok := response.(*SignedVoteResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", "response != SignedVoteResponse") + return ErrUnexpectedResponse + } + + if resp.Error != nil { + return resp.Error + } + *vote = *resp.Vote + + return nil +} + +// SignProposal requests a remote signer to sign a proposal +func (sc *SignerClient) SignProposal(chainID string, proposal *types.Proposal) error { + response, err := sc.endpoint.SendRequest(&SignProposalRequest{Proposal: proposal}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::SignProposal", "err", err) + return err + } + + resp, ok := response.(*SignedProposalResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::SignProposal", "err", "response != SignedProposalResponse") + return ErrUnexpectedResponse + } + if resp.Error != nil { + return resp.Error + } + *proposal = *resp.Proposal + + return nil +} diff --git a/privval/signer_client_test.go b/privval/signer_client_test.go new file mode 100644 index 000000000..3d7cfb3e0 --- /dev/null +++ b/privval/signer_client_test.go @@ -0,0 +1,257 @@ +package privval + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/types" +) + +type signerTestCase struct { + chainID string + mockPV types.PrivValidator + signerClient *SignerClient + signerServer *SignerServer +} + +func getSignerTestCases(t *testing.T) []signerTestCase { + testCases := make([]signerTestCase, 0) + + // Get test cases for each possible dialer (DialTCP / DialUnix / etc) + for _, dtc := range getDialerTestCases(t) { + chainID := common.RandStr(12) + mockPV := types.NewMockPV() + + // get a pair of signer listener, signer dialer endpoints + sl, sd := getMockEndpoints(t, dtc.addr, dtc.dialer) + sc, err := NewSignerClient(sl) + require.NoError(t, err) + ss := NewSignerServer(sd, chainID, mockPV) + + err = ss.Start() + require.NoError(t, err) + + tc := signerTestCase{ + chainID: chainID, + mockPV: mockPV, + signerClient: sc, + signerServer: ss, + } + + testCases = append(testCases, tc) + } + + return testCases +} + +func TestSignerClose(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + err := tc.signerClient.Close() + assert.NoError(t, err) + + err = tc.signerServer.Stop() + assert.NoError(t, err) + } +} + +func TestSignerPing(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + err := tc.signerClient.Ping() + assert.NoError(t, err) + } +} + +func TestSignerGetPubKey(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + pubKey := tc.signerClient.GetPubKey() + expectedPubKey := tc.mockPV.GetPubKey() + + assert.Equal(t, expectedPubKey, pubKey) + + addr := tc.signerClient.GetPubKey().Address() + expectedAddr := tc.mockPV.GetPubKey().Address() + + assert.Equal(t, expectedAddr, addr) + } +} + +func TestSignerProposal(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Proposal{Timestamp: ts} + have := &types.Proposal{Timestamp: ts} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + require.NoError(t, tc.mockPV.SignProposal(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignProposal(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVote(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVoteResetDeadline(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + time.Sleep(testTimeoutReadWrite2o3) + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + assert.Equal(t, want.Signature, have.Signature) + + // TODO(jleni): Clarify what is actually being tested + + // This would exceed the deadline if it was not extended by the previous message + time.Sleep(testTimeoutReadWrite2o3) + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVoteKeepAlive(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + // Check that even if the client does not request a + // signature for a long time. The service is still available + + // in this particular case, we use the dialer logger to ensure that + // test messages are properly interleaved in the test logs + tc.signerServer.Logger.Debug("TEST: Forced Wait -------------------------------------------------") + time.Sleep(testTimeoutReadWrite * 3) + tc.signerServer.Logger.Debug("TEST: Forced Wait DONE---------------------------------------------") + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerSignProposalErrors(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + // Replace service with a mock that always fails + tc.signerServer.privVal = types.NewErroringMockPV() + tc.mockPV = types.NewErroringMockPV() + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + ts := time.Now() + proposal := &types.Proposal{Timestamp: ts} + err := tc.signerClient.SignProposal(tc.chainID, proposal) + require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) + + err = tc.mockPV.SignProposal(tc.chainID, proposal) + require.Error(t, err) + + err = tc.signerClient.SignProposal(tc.chainID, proposal) + require.Error(t, err) + } +} + +func TestSignerSignVoteErrors(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + vote := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + // Replace signer service privval with one that always fails + tc.signerServer.privVal = types.NewErroringMockPV() + tc.mockPV = types.NewErroringMockPV() + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + err := tc.signerClient.SignVote(tc.chainID, vote) + require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) + + err = tc.mockPV.SignVote(tc.chainID, vote) + require.Error(t, err) + + err = tc.signerClient.SignVote(tc.chainID, vote) + require.Error(t, err) + } +} + +func brokenHandler(privVal types.PrivValidator, request SignerMessage, chainID string) (SignerMessage, error) { + var res SignerMessage + var err error + + switch r := request.(type) { + + // This is broken and will answer most requests with a pubkey response + case *PubKeyRequest: + res = &PubKeyResponse{nil, nil} + case *SignVoteRequest: + res = &PubKeyResponse{nil, nil} + case *SignProposalRequest: + res = &PubKeyResponse{nil, nil} + + case *PingRequest: + err, res = nil, &PingResponse{} + + default: + err = fmt.Errorf("unknown msg: %v", r) + } + + return res, err +} + +func TestSignerUnexpectedResponse(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + tc.signerServer.privVal = types.NewMockPV() + tc.mockPV = types.NewMockPV() + + tc.signerServer.SetRequestHandler(brokenHandler) + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + e := tc.signerClient.SignVote(tc.chainID, want) + assert.EqualError(t, e, "received unexpected response") + } +} diff --git a/privval/signer_dialer_endpoint.go b/privval/signer_dialer_endpoint.go new file mode 100644 index 000000000..95094c6d0 --- /dev/null +++ b/privval/signer_dialer_endpoint.go @@ -0,0 +1,84 @@ +package privval + +import ( + "time" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" +) + +const ( + defaultMaxDialRetries = 10 + defaultRetryWaitMilliseconds = 100 +) + +// SignerServiceEndpointOption sets an optional parameter on the SignerDialerEndpoint. +type SignerServiceEndpointOption func(*SignerDialerEndpoint) + +// SignerDialerEndpointTimeoutReadWrite sets the read and write timeout for connections +// from external signing processes. +func SignerDialerEndpointTimeoutReadWrite(timeout time.Duration) SignerServiceEndpointOption { + return func(ss *SignerDialerEndpoint) { ss.timeoutReadWrite = timeout } +} + +// SignerDialerEndpointConnRetries sets the amount of attempted retries to acceptNewConnection. +func SignerDialerEndpointConnRetries(retries int) SignerServiceEndpointOption { + return func(ss *SignerDialerEndpoint) { ss.maxConnRetries = retries } +} + +// SignerDialerEndpoint dials using its dialer and responds to any +// signature requests using its privVal. +type SignerDialerEndpoint struct { + signerEndpoint + + dialer SocketDialer + + retryWait time.Duration + maxConnRetries int +} + +// NewSignerDialerEndpoint returns a SignerDialerEndpoint that will dial using the given +// dialer and respond to any signature requests over the connection +// using the given privVal. +func NewSignerDialerEndpoint( + logger log.Logger, + dialer SocketDialer, +) *SignerDialerEndpoint { + + sd := &SignerDialerEndpoint{ + dialer: dialer, + retryWait: defaultRetryWaitMilliseconds * time.Millisecond, + maxConnRetries: defaultMaxDialRetries, + } + + sd.BaseService = *cmn.NewBaseService(logger, "SignerDialerEndpoint", sd) + sd.signerEndpoint.timeoutReadWrite = defaultTimeoutReadWriteSeconds * time.Second + + return sd +} + +func (sd *SignerDialerEndpoint) ensureConnection() error { + if sd.IsConnected() { + return nil + } + + retries := 0 + for retries < sd.maxConnRetries { + conn, err := sd.dialer() + + if err != nil { + retries++ + sd.Logger.Debug("SignerDialer: Reconnection failed", "retries", retries, "max", sd.maxConnRetries, "err", err) + // Wait between retries + time.Sleep(sd.retryWait) + } else { + sd.SetConnection(conn) + sd.Logger.Debug("SignerDialer: Connection Ready") + return nil + } + } + + sd.Logger.Debug("SignerDialer: Max retries exceeded", "retries", retries, "max", sd.maxConnRetries) + + return ErrNoConnection +} diff --git a/privval/signer_endpoint.go b/privval/signer_endpoint.go new file mode 100644 index 000000000..425f73fea --- /dev/null +++ b/privval/signer_endpoint.go @@ -0,0 +1,156 @@ +package privval + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/pkg/errors" + + cmn "github.com/tendermint/tendermint/libs/common" +) + +const ( + defaultTimeoutReadWriteSeconds = 3 +) + +type signerEndpoint struct { + cmn.BaseService + + connMtx sync.Mutex + conn net.Conn + + timeoutReadWrite time.Duration +} + +// Close closes the underlying net.Conn. +func (se *signerEndpoint) Close() error { + se.DropConnection() + return nil +} + +// IsConnected indicates if there is an active connection +func (se *signerEndpoint) IsConnected() bool { + se.connMtx.Lock() + defer se.connMtx.Unlock() + return se.isConnected() +} + +// TryGetConnection retrieves a connection if it is already available +func (se *signerEndpoint) GetAvailableConnection(connectionAvailableCh chan net.Conn) bool { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + // Is there a connection ready? + select { + case se.conn = <-connectionAvailableCh: + return true + default: + } + return false +} + +// TryGetConnection retrieves a connection if it is already available +func (se *signerEndpoint) WaitConnection(connectionAvailableCh chan net.Conn, maxWait time.Duration) error { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + select { + case se.conn = <-connectionAvailableCh: + case <-time.After(maxWait): + return ErrConnectionTimeout + } + + return nil +} + +// SetConnection replaces the current connection object +func (se *signerEndpoint) SetConnection(newConnection net.Conn) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + se.conn = newConnection +} + +// IsConnected indicates if there is an active connection +func (se *signerEndpoint) DropConnection() { + se.connMtx.Lock() + defer se.connMtx.Unlock() + se.dropConnection() +} + +// ReadMessage reads a message from the endpoint +func (se *signerEndpoint) ReadMessage() (msg SignerMessage, err error) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + if !se.isConnected() { + return nil, fmt.Errorf("endpoint is not connected") + } + + // Reset read deadline + deadline := time.Now().Add(se.timeoutReadWrite) + + err = se.conn.SetReadDeadline(deadline) + if err != nil { + return + } + + const maxRemoteSignerMsgSize = 1024 * 10 + _, err = cdc.UnmarshalBinaryLengthPrefixedReader(se.conn, &msg, maxRemoteSignerMsgSize) + if _, ok := err.(timeoutError); ok { + if err != nil { + err = errors.Wrap(ErrReadTimeout, err.Error()) + } else { + err = errors.Wrap(ErrReadTimeout, "Empty error") + } + se.Logger.Debug("Dropping [read]", "obj", se) + se.dropConnection() + } + + return +} + +// WriteMessage writes a message from the endpoint +func (se *signerEndpoint) WriteMessage(msg SignerMessage) (err error) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + if !se.isConnected() { + return errors.Wrap(ErrNoConnection, "endpoint is not connected") + } + + // Reset read deadline + deadline := time.Now().Add(se.timeoutReadWrite) + se.Logger.Debug("Write::Error Resetting deadline", "obj", se) + + err = se.conn.SetWriteDeadline(deadline) + if err != nil { + return + } + + _, err = cdc.MarshalBinaryLengthPrefixedWriter(se.conn, msg) + if _, ok := err.(timeoutError); ok { + if err != nil { + err = errors.Wrap(ErrWriteTimeout, err.Error()) + } else { + err = errors.Wrap(ErrWriteTimeout, "Empty error") + } + se.dropConnection() + } + + return +} + +func (se *signerEndpoint) isConnected() bool { + return se.conn != nil +} + +func (se *signerEndpoint) dropConnection() { + if se.conn != nil { + if err := se.conn.Close(); err != nil { + se.Logger.Error("signerEndpoint::dropConnection", "err", err) + } + se.conn = nil + } +} diff --git a/privval/signer_listener_endpoint.go b/privval/signer_listener_endpoint.go new file mode 100644 index 000000000..e25f18756 --- /dev/null +++ b/privval/signer_listener_endpoint.go @@ -0,0 +1,198 @@ +package privval + +import ( + "fmt" + "net" + "sync" + "time" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" +) + +// SignerValidatorEndpointOption sets an optional parameter on the SocketVal. +type SignerValidatorEndpointOption func(*SignerListenerEndpoint) + +// SignerListenerEndpoint listens for an external process to dial in +// and keeps the connection alive by dropping and reconnecting +type SignerListenerEndpoint struct { + signerEndpoint + + listener net.Listener + connectRequestCh chan struct{} + connectionAvailableCh chan net.Conn + + timeoutAccept time.Duration + pingTimer *time.Ticker + + instanceMtx sync.Mutex // Ensures instance public methods access, i.e. SendRequest +} + +// NewSignerListenerEndpoint returns an instance of SignerListenerEndpoint. +func NewSignerListenerEndpoint( + logger log.Logger, + listener net.Listener, +) *SignerListenerEndpoint { + sc := &SignerListenerEndpoint{ + listener: listener, + timeoutAccept: defaultTimeoutAcceptSeconds * time.Second, + } + + sc.BaseService = *cmn.NewBaseService(logger, "SignerListenerEndpoint", sc) + sc.signerEndpoint.timeoutReadWrite = defaultTimeoutReadWriteSeconds * time.Second + return sc +} + +// OnStart implements cmn.Service. +func (sl *SignerListenerEndpoint) OnStart() error { + sl.connectRequestCh = make(chan struct{}) + sl.connectionAvailableCh = make(chan net.Conn) + + sl.pingTimer = time.NewTicker(defaultPingPeriodMilliseconds * time.Millisecond) + + go sl.serviceLoop() + go sl.pingLoop() + + sl.connectRequestCh <- struct{}{} + + return nil +} + +// OnStop implements cmn.Service +func (sl *SignerListenerEndpoint) OnStop() { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + _ = sl.Close() + + // Stop listening + if sl.listener != nil { + if err := sl.listener.Close(); err != nil { + sl.Logger.Error("Closing Listener", "err", err) + sl.listener = nil + } + } + + sl.pingTimer.Stop() +} + +// WaitForConnection waits maxWait for a connection or returns a timeout error +func (sl *SignerListenerEndpoint) WaitForConnection(maxWait time.Duration) error { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + return sl.ensureConnection(maxWait) +} + +// SendRequest ensures there is a connection, sends a request and waits for a response +func (sl *SignerListenerEndpoint) SendRequest(request SignerMessage) (SignerMessage, error) { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + + err := sl.ensureConnection(sl.timeoutAccept) + if err != nil { + return nil, err + } + + err = sl.WriteMessage(request) + if err != nil { + return nil, err + } + + res, err := sl.ReadMessage() + if err != nil { + return nil, err + } + + return res, nil +} + +func (sl *SignerListenerEndpoint) ensureConnection(maxWait time.Duration) error { + if sl.IsConnected() { + return nil + } + + // Is there a connection ready? then use it + if sl.GetAvailableConnection(sl.connectionAvailableCh) { + return nil + } + + // block until connected or timeout + sl.triggerConnect() + err := sl.WaitConnection(sl.connectionAvailableCh, maxWait) + if err != nil { + return err + } + + return nil +} + +func (sl *SignerListenerEndpoint) acceptNewConnection() (net.Conn, error) { + if !sl.IsRunning() || sl.listener == nil { + return nil, fmt.Errorf("endpoint is closing") + } + + // wait for a new conn + sl.Logger.Info("SignerListener: Listening for new connection") + conn, err := sl.listener.Accept() + if err != nil { + return nil, err + } + + return conn, nil +} + +func (sl *SignerListenerEndpoint) triggerConnect() { + select { + case sl.connectRequestCh <- struct{}{}: + default: + } +} + +func (sl *SignerListenerEndpoint) triggerReconnect() { + sl.DropConnection() + sl.triggerConnect() +} + +func (sl *SignerListenerEndpoint) serviceLoop() { + for { + select { + case <-sl.connectRequestCh: + { + conn, err := sl.acceptNewConnection() + if err == nil { + sl.Logger.Info("SignerListener: Connected") + + // We have a good connection, wait for someone that needs one otherwise cancellation + select { + case sl.connectionAvailableCh <- conn: + case <-sl.Quit(): + return + } + } + + select { + case sl.connectRequestCh <- struct{}{}: + default: + } + } + case <-sl.Quit(): + return + } + } +} + +func (sl *SignerListenerEndpoint) pingLoop() { + for { + select { + case <-sl.pingTimer.C: + { + _, err := sl.SendRequest(&PingRequest{}) + if err != nil { + sl.Logger.Error("SignerListener: Ping timeout") + sl.triggerReconnect() + } + } + case <-sl.Quit(): + return + } + } +} diff --git a/privval/signer_listener_endpoint_test.go b/privval/signer_listener_endpoint_test.go new file mode 100644 index 000000000..7058ff8b8 --- /dev/null +++ b/privval/signer_listener_endpoint_test.go @@ -0,0 +1,198 @@ +package privval + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto/ed25519" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/types" +) + +var ( + testTimeoutAccept = defaultTimeoutAcceptSeconds * time.Second + + testTimeoutReadWrite = 100 * time.Millisecond + testTimeoutReadWrite2o3 = 60 * time.Millisecond // 2/3 of the other one +) + +type dialerTestCase struct { + addr string + dialer SocketDialer +} + +// TestSignerRemoteRetryTCPOnly will test connection retry attempts over TCP. We +// don't need this for Unix sockets because the OS instantly knows the state of +// both ends of the socket connection. This basically causes the +// SignerDialerEndpoint.dialer() call inside SignerDialerEndpoint.acceptNewConnection() to return +// successfully immediately, putting an instant stop to any retry attempts. +func TestSignerRemoteRetryTCPOnly(t *testing.T) { + var ( + attemptCh = make(chan int) + retries = 10 + ) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + // Continuously Accept connection and close {attempts} times + go func(ln net.Listener, attemptCh chan<- int) { + attempts := 0 + for { + conn, err := ln.Accept() + require.NoError(t, err) + + err = conn.Close() + require.NoError(t, err) + + attempts++ + + if attempts == retries { + attemptCh <- attempts + break + } + } + }(ln, attemptCh) + + dialerEndpoint := NewSignerDialerEndpoint( + log.TestingLogger(), + DialTCPFn(ln.Addr().String(), testTimeoutReadWrite, ed25519.GenPrivKey()), + ) + SignerDialerEndpointTimeoutReadWrite(time.Millisecond)(dialerEndpoint) + SignerDialerEndpointConnRetries(retries)(dialerEndpoint) + + chainId := cmn.RandStr(12) + mockPV := types.NewMockPV() + signerServer := NewSignerServer(dialerEndpoint, chainId, mockPV) + + err = signerServer.Start() + require.NoError(t, err) + defer signerServer.Stop() + + select { + case attempts := <-attemptCh: + assert.Equal(t, retries, attempts) + case <-time.After(1500 * time.Millisecond): + t.Error("expected remote to observe connection attempts") + } +} + +func TestRetryConnToRemoteSigner(t *testing.T) { + for _, tc := range getDialerTestCases(t) { + var ( + logger = log.TestingLogger() + chainID = cmn.RandStr(12) + mockPV = types.NewMockPV() + endpointIsOpenCh = make(chan struct{}) + thisConnTimeout = testTimeoutReadWrite + listenerEndpoint = newSignerListenerEndpoint(logger, tc.addr, thisConnTimeout) + ) + + dialerEndpoint := NewSignerDialerEndpoint( + logger, + tc.dialer, + ) + SignerDialerEndpointTimeoutReadWrite(testTimeoutReadWrite)(dialerEndpoint) + SignerDialerEndpointConnRetries(10)(dialerEndpoint) + + signerServer := NewSignerServer(dialerEndpoint, chainID, mockPV) + + startListenerEndpointAsync(t, listenerEndpoint, endpointIsOpenCh) + defer listenerEndpoint.Stop() + + require.NoError(t, signerServer.Start()) + assert.True(t, signerServer.IsRunning()) + <-endpointIsOpenCh + signerServer.Stop() + + dialerEndpoint2 := NewSignerDialerEndpoint( + logger, + tc.dialer, + ) + signerServer2 := NewSignerServer(dialerEndpoint2, chainID, mockPV) + + // let some pings pass + require.NoError(t, signerServer2.Start()) + assert.True(t, signerServer2.IsRunning()) + defer signerServer2.Stop() + + // give the client some time to re-establish the conn to the remote signer + // should see sth like this in the logs: + // + // E[10016-01-10|17:12:46.128] Ping err="remote signer timed out" + // I[10016-01-10|17:16:42.447] Re-created connection to remote signer impl=SocketVal + time.Sleep(testTimeoutReadWrite * 2) + } +} + +/////////////////////////////////// + +func newSignerListenerEndpoint(logger log.Logger, addr string, timeoutReadWrite time.Duration) *SignerListenerEndpoint { + proto, address := cmn.ProtocolAndAddress(addr) + + ln, err := net.Listen(proto, address) + logger.Info("SignerListener: Listening", "proto", proto, "address", address) + if err != nil { + panic(err) + } + + var listener net.Listener + + if proto == "unix" { + unixLn := NewUnixListener(ln) + UnixListenerTimeoutAccept(testTimeoutAccept)(unixLn) + UnixListenerTimeoutReadWrite(timeoutReadWrite)(unixLn) + listener = unixLn + } else { + tcpLn := NewTCPListener(ln, ed25519.GenPrivKey()) + TCPListenerTimeoutAccept(testTimeoutAccept)(tcpLn) + TCPListenerTimeoutReadWrite(timeoutReadWrite)(tcpLn) + listener = tcpLn + } + + return NewSignerListenerEndpoint(logger, listener) +} + +func startListenerEndpointAsync(t *testing.T, sle *SignerListenerEndpoint, endpointIsOpenCh chan struct{}) { + go func(sle *SignerListenerEndpoint) { + require.NoError(t, sle.Start()) + assert.True(t, sle.IsRunning()) + close(endpointIsOpenCh) + }(sle) +} + +func getMockEndpoints( + t *testing.T, + addr string, + socketDialer SocketDialer, +) (*SignerListenerEndpoint, *SignerDialerEndpoint) { + + var ( + logger = log.TestingLogger() + endpointIsOpenCh = make(chan struct{}) + + dialerEndpoint = NewSignerDialerEndpoint( + logger, + socketDialer, + ) + + listenerEndpoint = newSignerListenerEndpoint(logger, addr, testTimeoutReadWrite) + ) + + SignerDialerEndpointTimeoutReadWrite(testTimeoutReadWrite)(dialerEndpoint) + SignerDialerEndpointConnRetries(1e6)(dialerEndpoint) + + startListenerEndpointAsync(t, listenerEndpoint, endpointIsOpenCh) + + require.NoError(t, dialerEndpoint.Start()) + assert.True(t, dialerEndpoint.IsRunning()) + + <-endpointIsOpenCh + + return listenerEndpoint, dialerEndpoint +} diff --git a/privval/signer_remote.go b/privval/signer_remote.go deleted file mode 100644 index 53b0cb773..000000000 --- a/privval/signer_remote.go +++ /dev/null @@ -1,192 +0,0 @@ -package privval - -import ( - "fmt" - "io" - "net" - - "github.com/pkg/errors" - - "github.com/tendermint/tendermint/crypto" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/types" -) - -// SignerRemote implements PrivValidator. -// It uses a net.Conn to request signatures from an external process. -type SignerRemote struct { - conn net.Conn - - // memoized - consensusPubKey crypto.PubKey -} - -// Check that SignerRemote implements PrivValidator. -var _ types.PrivValidator = (*SignerRemote)(nil) - -// NewSignerRemote returns an instance of SignerRemote. -func NewSignerRemote(conn net.Conn) (*SignerRemote, error) { - - // retrieve and memoize the consensus public key once. - pubKey, err := getPubKey(conn) - if err != nil { - return nil, cmn.ErrorWrap(err, "error while retrieving public key for remote signer") - } - return &SignerRemote{ - conn: conn, - consensusPubKey: pubKey, - }, nil -} - -// Close calls Close on the underlying net.Conn. -func (sc *SignerRemote) Close() error { - return sc.conn.Close() -} - -// GetPubKey implements PrivValidator. -func (sc *SignerRemote) GetPubKey() crypto.PubKey { - return sc.consensusPubKey -} - -// not thread-safe (only called on startup). -func getPubKey(conn net.Conn) (crypto.PubKey, error) { - err := writeMsg(conn, &PubKeyRequest{}) - if err != nil { - return nil, err - } - - res, err := readMsg(conn) - if err != nil { - return nil, err - } - - pubKeyResp, ok := res.(*PubKeyResponse) - if !ok { - return nil, errors.Wrap(ErrUnexpectedResponse, "response is not PubKeyResponse") - } - - if pubKeyResp.Error != nil { - return nil, errors.Wrap(pubKeyResp.Error, "failed to get private validator's public key") - } - - return pubKeyResp.PubKey, nil -} - -// SignVote implements PrivValidator. -func (sc *SignerRemote) SignVote(chainID string, vote *types.Vote) error { - err := writeMsg(sc.conn, &SignVoteRequest{Vote: vote}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - - resp, ok := res.(*SignedVoteResponse) - if !ok { - return ErrUnexpectedResponse - } - if resp.Error != nil { - return resp.Error - } - *vote = *resp.Vote - - return nil -} - -// SignProposal implements PrivValidator. -func (sc *SignerRemote) SignProposal(chainID string, proposal *types.Proposal) error { - err := writeMsg(sc.conn, &SignProposalRequest{Proposal: proposal}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - resp, ok := res.(*SignedProposalResponse) - if !ok { - return ErrUnexpectedResponse - } - if resp.Error != nil { - return resp.Error - } - *proposal = *resp.Proposal - - return nil -} - -// Ping is used to check connection health. -func (sc *SignerRemote) Ping() error { - err := writeMsg(sc.conn, &PingRequest{}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - _, ok := res.(*PingResponse) - if !ok { - return ErrUnexpectedResponse - } - - return nil -} - -func readMsg(r io.Reader) (msg RemoteSignerMsg, err error) { - const maxRemoteSignerMsgSize = 1024 * 10 - _, err = cdc.UnmarshalBinaryLengthPrefixedReader(r, &msg, maxRemoteSignerMsgSize) - if _, ok := err.(timeoutError); ok { - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) - } - return -} - -func writeMsg(w io.Writer, msg interface{}) (err error) { - _, err = cdc.MarshalBinaryLengthPrefixedWriter(w, msg) - if _, ok := err.(timeoutError); ok { - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) - } - return -} - -func handleRequest(req RemoteSignerMsg, chainID string, privVal types.PrivValidator) (RemoteSignerMsg, error) { - var res RemoteSignerMsg - var err error - - switch r := req.(type) { - case *PubKeyRequest: - var p crypto.PubKey - p = privVal.GetPubKey() - res = &PubKeyResponse{p, nil} - - case *SignVoteRequest: - err = privVal.SignVote(chainID, r.Vote) - if err != nil { - res = &SignedVoteResponse{nil, &RemoteSignerError{0, err.Error()}} - } else { - res = &SignedVoteResponse{r.Vote, nil} - } - - case *SignProposalRequest: - err = privVal.SignProposal(chainID, r.Proposal) - if err != nil { - res = &SignedProposalResponse{nil, &RemoteSignerError{0, err.Error()}} - } else { - res = &SignedProposalResponse{r.Proposal, nil} - } - - case *PingRequest: - res = &PingResponse{} - - default: - err = fmt.Errorf("unknown msg: %v", r) - } - - return res, err -} diff --git a/privval/signer_remote_test.go b/privval/signer_remote_test.go deleted file mode 100644 index 28230b803..000000000 --- a/privval/signer_remote_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package privval - -import ( - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -// TestSignerRemoteRetryTCPOnly will test connection retry attempts over TCP. We -// don't need this for Unix sockets because the OS instantly knows the state of -// both ends of the socket connection. This basically causes the -// SignerServiceEndpoint.dialer() call inside SignerServiceEndpoint.connect() to return -// successfully immediately, putting an instant stop to any retry attempts. -func TestSignerRemoteRetryTCPOnly(t *testing.T) { - var ( - attemptCh = make(chan int) - retries = 2 - ) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - go func(ln net.Listener, attemptCh chan<- int) { - attempts := 0 - - for { - conn, err := ln.Accept() - require.NoError(t, err) - - err = conn.Close() - require.NoError(t, err) - - attempts++ - - if attempts == retries { - attemptCh <- attempts - break - } - } - }(ln, attemptCh) - - serviceEndpoint := NewSignerServiceEndpoint( - log.TestingLogger(), - cmn.RandStr(12), - types.NewMockPV(), - DialTCPFn(ln.Addr().String(), testTimeoutReadWrite, ed25519.GenPrivKey()), - ) - defer serviceEndpoint.Stop() - - SignerServiceEndpointTimeoutReadWrite(time.Millisecond)(serviceEndpoint) - SignerServiceEndpointConnRetries(retries)(serviceEndpoint) - - assert.Equal(t, serviceEndpoint.Start(), ErrDialRetryMax) - - select { - case attempts := <-attemptCh: - assert.Equal(t, retries, attempts) - case <-time.After(100 * time.Millisecond): - t.Error("expected remote to observe connection attempts") - } -} diff --git a/privval/signer_requestHandler.go b/privval/signer_requestHandler.go new file mode 100644 index 000000000..dcab7752e --- /dev/null +++ b/privval/signer_requestHandler.go @@ -0,0 +1,44 @@ +package privval + +import ( + "fmt" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/types" +) + +func DefaultValidationRequestHandler(privVal types.PrivValidator, req SignerMessage, chainID string) (SignerMessage, error) { + var res SignerMessage + var err error + + switch r := req.(type) { + case *PubKeyRequest: + var p crypto.PubKey + p = privVal.GetPubKey() + res = &PubKeyResponse{p, nil} + + case *SignVoteRequest: + err = privVal.SignVote(chainID, r.Vote) + if err != nil { + res = &SignedVoteResponse{nil, &RemoteSignerError{0, err.Error()}} + } else { + res = &SignedVoteResponse{r.Vote, nil} + } + + case *SignProposalRequest: + err = privVal.SignProposal(chainID, r.Proposal) + if err != nil { + res = &SignedProposalResponse{nil, &RemoteSignerError{0, err.Error()}} + } else { + res = &SignedProposalResponse{r.Proposal, nil} + } + + case *PingRequest: + err, res = nil, &PingResponse{} + + default: + err = fmt.Errorf("unknown msg: %v", r) + } + + return res, err +} diff --git a/privval/signer_server.go b/privval/signer_server.go new file mode 100644 index 000000000..62dcc461c --- /dev/null +++ b/privval/signer_server.go @@ -0,0 +1,107 @@ +package privval + +import ( + "io" + "sync" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/types" +) + +// ValidationRequestHandlerFunc handles different remoteSigner requests +type ValidationRequestHandlerFunc func( + privVal types.PrivValidator, + requestMessage SignerMessage, + chainID string) (SignerMessage, error) + +type SignerServer struct { + cmn.BaseService + + endpoint *SignerDialerEndpoint + chainID string + privVal types.PrivValidator + + handlerMtx sync.Mutex + validationRequestHandler ValidationRequestHandlerFunc +} + +func NewSignerServer(endpoint *SignerDialerEndpoint, chainID string, privVal types.PrivValidator) *SignerServer { + ss := &SignerServer{ + endpoint: endpoint, + chainID: chainID, + privVal: privVal, + validationRequestHandler: DefaultValidationRequestHandler, + } + + ss.BaseService = *cmn.NewBaseService(endpoint.Logger, "SignerServer", ss) + + return ss +} + +// OnStart implements cmn.Service. +func (ss *SignerServer) OnStart() error { + go ss.serviceLoop() + return nil +} + +// OnStop implements cmn.Service. +func (ss *SignerServer) OnStop() { + ss.endpoint.Logger.Debug("SignerServer: OnStop calling Close") + _ = ss.endpoint.Close() +} + +// SetRequestHandler override the default function that is used to service requests +func (ss *SignerServer) SetRequestHandler(validationRequestHandler ValidationRequestHandlerFunc) { + ss.handlerMtx.Lock() + defer ss.handlerMtx.Unlock() + ss.validationRequestHandler = validationRequestHandler +} + +func (ss *SignerServer) servicePendingRequest() { + if !ss.IsRunning() { + return // Ignore error from closing. + } + + req, err := ss.endpoint.ReadMessage() + if err != nil { + if err != io.EOF { + ss.Logger.Error("SignerServer: HandleMessage", "err", err) + } + return + } + + var res SignerMessage + { + // limit the scope of the lock + ss.handlerMtx.Lock() + defer ss.handlerMtx.Unlock() + res, err = ss.validationRequestHandler(ss.privVal, req, ss.chainID) + if err != nil { + // only log the error; we'll reply with an error in res + ss.Logger.Error("SignerServer: handleMessage", "err", err) + } + } + + if res != nil { + err = ss.endpoint.WriteMessage(res) + if err != nil { + ss.Logger.Error("SignerServer: writeMessage", "err", err) + } + } +} + +func (ss *SignerServer) serviceLoop() { + for { + select { + default: + err := ss.endpoint.ensureConnection() + if err != nil { + return + } + ss.servicePendingRequest() + + case <-ss.Quit(): + return + } + } +} diff --git a/privval/signer_service_endpoint.go b/privval/signer_service_endpoint.go deleted file mode 100644 index 1b37d5fc6..000000000 --- a/privval/signer_service_endpoint.go +++ /dev/null @@ -1,139 +0,0 @@ -package privval - -import ( - "io" - "net" - "time" - - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -// SignerServiceEndpointOption sets an optional parameter on the SignerServiceEndpoint. -type SignerServiceEndpointOption func(*SignerServiceEndpoint) - -// SignerServiceEndpointTimeoutReadWrite sets the read and write timeout for connections -// from external signing processes. -func SignerServiceEndpointTimeoutReadWrite(timeout time.Duration) SignerServiceEndpointOption { - return func(ss *SignerServiceEndpoint) { ss.timeoutReadWrite = timeout } -} - -// SignerServiceEndpointConnRetries sets the amount of attempted retries to connect. -func SignerServiceEndpointConnRetries(retries int) SignerServiceEndpointOption { - return func(ss *SignerServiceEndpoint) { ss.connRetries = retries } -} - -// SignerServiceEndpoint dials using its dialer and responds to any -// signature requests using its privVal. -type SignerServiceEndpoint struct { - cmn.BaseService - - chainID string - timeoutReadWrite time.Duration - connRetries int - privVal types.PrivValidator - - dialer SocketDialer - conn net.Conn -} - -// NewSignerServiceEndpoint returns a SignerServiceEndpoint that will dial using the given -// dialer and respond to any signature requests over the connection -// using the given privVal. -func NewSignerServiceEndpoint( - logger log.Logger, - chainID string, - privVal types.PrivValidator, - dialer SocketDialer, -) *SignerServiceEndpoint { - se := &SignerServiceEndpoint{ - chainID: chainID, - timeoutReadWrite: time.Second * defaultTimeoutReadWriteSeconds, - connRetries: defaultMaxDialRetries, - privVal: privVal, - dialer: dialer, - } - - se.BaseService = *cmn.NewBaseService(logger, "SignerServiceEndpoint", se) - return se -} - -// OnStart implements cmn.Service. -func (se *SignerServiceEndpoint) OnStart() error { - conn, err := se.connect() - if err != nil { - se.Logger.Error("OnStart", "err", err) - return err - } - - se.conn = conn - go se.handleConnection(conn) - - return nil -} - -// OnStop implements cmn.Service. -func (se *SignerServiceEndpoint) OnStop() { - if se.conn == nil { - return - } - - if err := se.conn.Close(); err != nil { - se.Logger.Error("OnStop", "err", cmn.ErrorWrap(err, "closing listener failed")) - } -} - -func (se *SignerServiceEndpoint) connect() (net.Conn, error) { - for retries := 0; retries < se.connRetries; retries++ { - // Don't sleep if it is the first retry. - if retries > 0 { - time.Sleep(se.timeoutReadWrite) - } - - conn, err := se.dialer() - if err == nil { - return conn, nil - } - - se.Logger.Error("dialing", "err", err) - } - - return nil, ErrDialRetryMax -} - -func (se *SignerServiceEndpoint) handleConnection(conn net.Conn) { - for { - if !se.IsRunning() { - return // Ignore error from listener closing. - } - - // Reset the connection deadline - deadline := time.Now().Add(se.timeoutReadWrite) - err := conn.SetDeadline(deadline) - if err != nil { - return - } - - req, err := readMsg(conn) - if err != nil { - if err != io.EOF { - se.Logger.Error("handleConnection readMsg", "err", err) - } - return - } - - res, err := handleRequest(req, se.chainID, se.privVal) - - if err != nil { - // only log the error; we'll reply with an error in res - se.Logger.Error("handleConnection handleRequest", "err", err) - } - - err = writeMsg(conn, res) - if err != nil { - se.Logger.Error("handleConnection writeMsg", "err", err) - return - } - } -} diff --git a/privval/signer_validator_endpoint.go b/privval/signer_validator_endpoint.go deleted file mode 100644 index 6dc7f99d5..000000000 --- a/privval/signer_validator_endpoint.go +++ /dev/null @@ -1,230 +0,0 @@ -package privval - -import ( - "fmt" - "net" - "sync" - "time" - - "github.com/tendermint/tendermint/crypto" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -const ( - defaultHeartbeatSeconds = 2 - defaultMaxDialRetries = 10 -) - -var ( - heartbeatPeriod = time.Second * defaultHeartbeatSeconds -) - -// SignerValidatorEndpointOption sets an optional parameter on the SocketVal. -type SignerValidatorEndpointOption func(*SignerValidatorEndpoint) - -// SignerValidatorEndpointSetHeartbeat sets the period on which to check the liveness of the -// connected Signer connections. -func SignerValidatorEndpointSetHeartbeat(period time.Duration) SignerValidatorEndpointOption { - return func(sc *SignerValidatorEndpoint) { sc.heartbeatPeriod = period } -} - -// SocketVal implements PrivValidator. -// It listens for an external process to dial in and uses -// the socket to request signatures. -type SignerValidatorEndpoint struct { - cmn.BaseService - - listener net.Listener - - // ping - cancelPingCh chan struct{} - pingTicker *time.Ticker - heartbeatPeriod time.Duration - - // signer is mutable since it can be reset if the connection fails. - // failures are detected by a background ping routine. - // All messages are request/response, so we hold the mutex - // so only one request/response pair can happen at a time. - // Methods on the underlying net.Conn itself are already goroutine safe. - mtx sync.Mutex - - // TODO: Signer should encapsulate and hide the endpoint completely. Invert the relation - signer *SignerRemote -} - -// Check that SignerValidatorEndpoint implements PrivValidator. -var _ types.PrivValidator = (*SignerValidatorEndpoint)(nil) - -// NewSignerValidatorEndpoint returns an instance of SignerValidatorEndpoint. -func NewSignerValidatorEndpoint(logger log.Logger, listener net.Listener) *SignerValidatorEndpoint { - sc := &SignerValidatorEndpoint{ - listener: listener, - heartbeatPeriod: heartbeatPeriod, - } - - sc.BaseService = *cmn.NewBaseService(logger, "SignerValidatorEndpoint", sc) - - return sc -} - -//-------------------------------------------------------- -// Implement PrivValidator - -// GetPubKey implements PrivValidator. -func (ve *SignerValidatorEndpoint) GetPubKey() crypto.PubKey { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.GetPubKey() -} - -// SignVote implements PrivValidator. -func (ve *SignerValidatorEndpoint) SignVote(chainID string, vote *types.Vote) error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.SignVote(chainID, vote) -} - -// SignProposal implements PrivValidator. -func (ve *SignerValidatorEndpoint) SignProposal(chainID string, proposal *types.Proposal) error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.SignProposal(chainID, proposal) -} - -//-------------------------------------------------------- -// More thread safe methods proxied to the signer - -// Ping is used to check connection health. -func (ve *SignerValidatorEndpoint) Ping() error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.Ping() -} - -// Close closes the underlying net.Conn. -func (ve *SignerValidatorEndpoint) Close() { - ve.mtx.Lock() - defer ve.mtx.Unlock() - if ve.signer != nil { - if err := ve.signer.Close(); err != nil { - ve.Logger.Error("OnStop", "err", err) - } - } - - if ve.listener != nil { - if err := ve.listener.Close(); err != nil { - ve.Logger.Error("OnStop", "err", err) - } - } -} - -//-------------------------------------------------------- -// Service start and stop - -// OnStart implements cmn.Service. -func (ve *SignerValidatorEndpoint) OnStart() error { - if closed, err := ve.reset(); err != nil { - ve.Logger.Error("OnStart", "err", err) - return err - } else if closed { - return fmt.Errorf("listener is closed") - } - - // Start a routine to keep the connection alive - ve.cancelPingCh = make(chan struct{}, 1) - ve.pingTicker = time.NewTicker(ve.heartbeatPeriod) - go func() { - for { - select { - case <-ve.pingTicker.C: - err := ve.Ping() - if err != nil { - ve.Logger.Error("Ping", "err", err) - if err == ErrUnexpectedResponse { - return - } - - closed, err := ve.reset() - if err != nil { - ve.Logger.Error("Reconnecting to remote signer failed", "err", err) - continue - } - if closed { - ve.Logger.Info("listener is closing") - return - } - - ve.Logger.Info("Re-created connection to remote signer", "impl", ve) - } - case <-ve.cancelPingCh: - ve.pingTicker.Stop() - return - } - } - }() - - return nil -} - -// OnStop implements cmn.Service. -func (ve *SignerValidatorEndpoint) OnStop() { - if ve.cancelPingCh != nil { - close(ve.cancelPingCh) - } - ve.Close() -} - -//-------------------------------------------------------- -// Connection and signer management - -// waits to accept and sets a new connection. -// connection is closed in OnStop. -// returns true if the listener is closed -// (ie. it returns a nil conn). -func (ve *SignerValidatorEndpoint) reset() (closed bool, err error) { - ve.mtx.Lock() - defer ve.mtx.Unlock() - - // first check if the conn already exists and close it. - if ve.signer != nil { - if tmpErr := ve.signer.Close(); tmpErr != nil { - ve.Logger.Error("error closing socket val connection during reset", "err", tmpErr) - } - } - - // wait for a new conn - conn, err := ve.acceptConnection() - if err != nil { - return false, err - } - - // listener is closed - if conn == nil { - return true, nil - } - - ve.signer, err = NewSignerRemote(conn) - if err != nil { - // failed to fetch the pubkey. close out the connection. - if tmpErr := conn.Close(); tmpErr != nil { - ve.Logger.Error("error closing connection", "err", tmpErr) - } - return false, err - } - return false, nil -} - -// Attempt to accept a connection. -// Times out after the listener's timeoutAccept -func (ve *SignerValidatorEndpoint) acceptConnection() (net.Conn, error) { - conn, err := ve.listener.Accept() - if err != nil { - if !ve.IsRunning() { - return nil, nil // Ignore error from listener closing. - } - return nil, err - } - return conn, nil -} diff --git a/privval/signer_validator_endpoint_test.go b/privval/signer_validator_endpoint_test.go deleted file mode 100644 index bf4c29930..000000000 --- a/privval/signer_validator_endpoint_test.go +++ /dev/null @@ -1,505 +0,0 @@ -package privval - -import ( - "fmt" - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/crypto/ed25519" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - - "github.com/tendermint/tendermint/types" -) - -var ( - testTimeoutAccept = defaultTimeoutAcceptSeconds * time.Second - - testTimeoutReadWrite = 100 * time.Millisecond - testTimeoutReadWrite2o3 = 66 * time.Millisecond // 2/3 of the other one - - testTimeoutHeartbeat = 10 * time.Millisecond - testTimeoutHeartbeat3o2 = 6 * time.Millisecond // 3/2 of the other one -) - -type socketTestCase struct { - addr string - dialer SocketDialer -} - -func socketTestCases(t *testing.T) []socketTestCase { - tcpAddr := fmt.Sprintf("tcp://%s", testFreeTCPAddr(t)) - unixFilePath, err := testUnixAddr() - require.NoError(t, err) - unixAddr := fmt.Sprintf("unix://%s", unixFilePath) - return []socketTestCase{ - { - addr: tcpAddr, - dialer: DialTCPFn(tcpAddr, testTimeoutReadWrite, ed25519.GenPrivKey()), - }, - { - addr: unixAddr, - dialer: DialUnixFn(unixFilePath), - }, - } -} - -func TestSocketPVAddress(t *testing.T) { - for _, tc := range socketTestCases(t) { - // Execute the test within a closure to ensure the deferred statements - // are called between each for loop iteration, for isolated test cases. - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair(t, chainID, types.NewMockPV(), tc.addr, tc.dialer) - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - serviceAddr := serviceEndpoint.privVal.GetPubKey().Address() - validatorAddr := validatorEndpoint.GetPubKey().Address() - - assert.Equal(t, serviceAddr, validatorAddr) - }() - } -} - -func TestSocketPVPubKey(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - clientKey := validatorEndpoint.GetPubKey() - privvalPubKey := serviceEndpoint.privVal.GetPubKey() - - assert.Equal(t, privvalPubKey, clientKey) - }() - } -} - -func TestSocketPVProposal(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - privProposal = &types.Proposal{Timestamp: ts} - clientProposal = &types.Proposal{Timestamp: ts} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - require.NoError(t, serviceEndpoint.privVal.SignProposal(chainID, privProposal)) - require.NoError(t, validatorEndpoint.SignProposal(chainID, clientProposal)) - - assert.Equal(t, privProposal.Signature, clientProposal.Signature) - }() - } -} - -func TestSocketPVVote(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVVoteResetDeadline(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - time.Sleep(testTimeoutReadWrite2o3) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - - // This would exceed the deadline if it was not extended by the previous message - time.Sleep(testTimeoutReadWrite2o3) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVVoteKeepalive(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - time.Sleep(testTimeoutReadWrite * 2) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVDeadline(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - listenc = make(chan struct{}) - thisConnTimeout = 100 * time.Millisecond - validatorEndpoint = newSignerValidatorEndpoint(log.TestingLogger(), tc.addr, thisConnTimeout) - ) - - go func(sc *SignerValidatorEndpoint) { - defer close(listenc) - - // Note: the TCP connection times out at the accept() phase, - // whereas the Unix domain sockets connection times out while - // attempting to fetch the remote signer's public key. - assert.True(t, IsConnTimeout(sc.Start())) - - assert.False(t, sc.IsRunning()) - }(validatorEndpoint) - - for { - _, err := cmn.Connect(tc.addr) - if err == nil { - break - } - } - - <-listenc - }() - } -} - -func TestRemoteSignVoteErrors(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewErroringMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - vote = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - err := validatorEndpoint.SignVote("", vote) - require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) - - err = serviceEndpoint.privVal.SignVote(chainID, vote) - require.Error(t, err) - err = validatorEndpoint.SignVote(chainID, vote) - require.Error(t, err) - }() - } -} - -func TestRemoteSignProposalErrors(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewErroringMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - proposal = &types.Proposal{Timestamp: ts} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - err := validatorEndpoint.SignProposal("", proposal) - require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) - - err = serviceEndpoint.privVal.SignProposal(chainID, proposal) - require.Error(t, err) - - err = validatorEndpoint.SignProposal(chainID, proposal) - require.Error(t, err) - }() - } -} - -func TestErrUnexpectedResponse(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - logger = log.TestingLogger() - chainID = cmn.RandStr(12) - readyCh = make(chan struct{}) - errCh = make(chan error, 1) - - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - - validatorEndpoint = newSignerValidatorEndpoint( - logger, - tc.addr, - testTimeoutReadWrite) - ) - - testStartEndpoint(t, readyCh, validatorEndpoint) - defer validatorEndpoint.Stop() - SignerServiceEndpointTimeoutReadWrite(time.Millisecond)(serviceEndpoint) - SignerServiceEndpointConnRetries(100)(serviceEndpoint) - // we do not want to Start() the remote signer here and instead use the connection to - // reply with intentionally wrong replies below: - rsConn, err := serviceEndpoint.connect() - defer rsConn.Close() - require.NoError(t, err) - require.NotNil(t, rsConn) - // send over public key to get the remote signer running: - go testReadWriteResponse(t, &PubKeyResponse{}, rsConn) - <-readyCh - - // Proposal: - go func(errc chan error) { - errc <- validatorEndpoint.SignProposal(chainID, &types.Proposal{}) - }(errCh) - - // read request and write wrong response: - go testReadWriteResponse(t, &SignedVoteResponse{}, rsConn) - err = <-errCh - require.Error(t, err) - require.Equal(t, err, ErrUnexpectedResponse) - - // Vote: - go func(errc chan error) { - errc <- validatorEndpoint.SignVote(chainID, &types.Vote{}) - }(errCh) - // read request and write wrong response: - go testReadWriteResponse(t, &SignedProposalResponse{}, rsConn) - err = <-errCh - require.Error(t, err) - require.Equal(t, err, ErrUnexpectedResponse) - }() - } -} - -func TestRetryConnToRemoteSigner(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - logger = log.TestingLogger() - chainID = cmn.RandStr(12) - readyCh = make(chan struct{}) - - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - thisConnTimeout = testTimeoutReadWrite - validatorEndpoint = newSignerValidatorEndpoint(logger, tc.addr, thisConnTimeout) - ) - // Ping every: - SignerValidatorEndpointSetHeartbeat(testTimeoutHeartbeat)(validatorEndpoint) - - SignerServiceEndpointTimeoutReadWrite(testTimeoutReadWrite)(serviceEndpoint) - SignerServiceEndpointConnRetries(10)(serviceEndpoint) - - testStartEndpoint(t, readyCh, validatorEndpoint) - defer validatorEndpoint.Stop() - require.NoError(t, serviceEndpoint.Start()) - assert.True(t, serviceEndpoint.IsRunning()) - - <-readyCh - time.Sleep(testTimeoutHeartbeat * 2) - - serviceEndpoint.Stop() - rs2 := NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - // let some pings pass - time.Sleep(testTimeoutHeartbeat3o2) - require.NoError(t, rs2.Start()) - assert.True(t, rs2.IsRunning()) - defer rs2.Stop() - - // give the client some time to re-establish the conn to the remote signer - // should see sth like this in the logs: - // - // E[10016-01-10|17:12:46.128] Ping err="remote signer timed out" - // I[10016-01-10|17:16:42.447] Re-created connection to remote signer impl=SocketVal - time.Sleep(testTimeoutReadWrite * 2) - }() - } -} - -func newSignerValidatorEndpoint(logger log.Logger, addr string, timeoutReadWrite time.Duration) *SignerValidatorEndpoint { - proto, address := cmn.ProtocolAndAddress(addr) - - ln, err := net.Listen(proto, address) - logger.Info("Listening at", "proto", proto, "address", address) - if err != nil { - panic(err) - } - - var listener net.Listener - - if proto == "unix" { - unixLn := NewUnixListener(ln) - UnixListenerTimeoutAccept(testTimeoutAccept)(unixLn) - UnixListenerTimeoutReadWrite(timeoutReadWrite)(unixLn) - listener = unixLn - } else { - tcpLn := NewTCPListener(ln, ed25519.GenPrivKey()) - TCPListenerTimeoutAccept(testTimeoutAccept)(tcpLn) - TCPListenerTimeoutReadWrite(timeoutReadWrite)(tcpLn) - listener = tcpLn - } - - return NewSignerValidatorEndpoint(logger, listener) -} - -func testSetupSocketPair( - t *testing.T, - chainID string, - privValidator types.PrivValidator, - addr string, - socketDialer SocketDialer, -) (*SignerValidatorEndpoint, *SignerServiceEndpoint) { - var ( - logger = log.TestingLogger() - privVal = privValidator - readyc = make(chan struct{}) - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - privVal, - socketDialer, - ) - - thisConnTimeout = testTimeoutReadWrite - validatorEndpoint = newSignerValidatorEndpoint(logger, addr, thisConnTimeout) - ) - - SignerValidatorEndpointSetHeartbeat(testTimeoutHeartbeat)(validatorEndpoint) - SignerServiceEndpointTimeoutReadWrite(testTimeoutReadWrite)(serviceEndpoint) - SignerServiceEndpointConnRetries(1e6)(serviceEndpoint) - - testStartEndpoint(t, readyc, validatorEndpoint) - - require.NoError(t, serviceEndpoint.Start()) - assert.True(t, serviceEndpoint.IsRunning()) - - <-readyc - - return validatorEndpoint, serviceEndpoint -} - -func testReadWriteResponse(t *testing.T, resp RemoteSignerMsg, rsConn net.Conn) { - _, err := readMsg(rsConn) - require.NoError(t, err) - - err = writeMsg(rsConn, resp) - require.NoError(t, err) -} - -func testStartEndpoint(t *testing.T, readyCh chan struct{}, sc *SignerValidatorEndpoint) { - go func(sc *SignerValidatorEndpoint) { - require.NoError(t, sc.Start()) - assert.True(t, sc.IsRunning()) - - readyCh <- struct{}{} - }(sc) -} - -// testFreeTCPAddr claims a free port so we don't block on listener being ready. -func testFreeTCPAddr(t *testing.T) string { - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer ln.Close() - - return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) -} diff --git a/privval/socket_dialers_test.go b/privval/socket_dialers_test.go index 9d5d5cc2b..d7b372b85 100644 --- a/privval/socket_dialers_test.go +++ b/privval/socket_dialers_test.go @@ -1,26 +1,49 @@ package privval import ( + "fmt" "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" - cmn "github.com/tendermint/tendermint/libs/common" ) +func getDialerTestCases(t *testing.T) []dialerTestCase { + tcpAddr := GetFreeLocalhostAddrPort() + unixFilePath, err := testUnixAddr() + require.NoError(t, err) + unixAddr := fmt.Sprintf("unix://%s", unixFilePath) + + return []dialerTestCase{ + { + addr: tcpAddr, + dialer: DialTCPFn(tcpAddr, testTimeoutReadWrite, ed25519.GenPrivKey()), + }, + { + addr: unixAddr, + dialer: DialUnixFn(unixFilePath), + }, + } +} + func TestIsConnTimeoutForFundamentalTimeouts(t *testing.T) { // Generate a networking timeout - dialer := DialTCPFn(testFreeTCPAddr(t), time.Millisecond, ed25519.GenPrivKey()) + tcpAddr := GetFreeLocalhostAddrPort() + dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) _, err := dialer() assert.Error(t, err) assert.True(t, IsConnTimeout(err)) } func TestIsConnTimeoutForWrappedConnTimeouts(t *testing.T) { - dialer := DialTCPFn(testFreeTCPAddr(t), time.Millisecond, ed25519.GenPrivKey()) + tcpAddr := GetFreeLocalhostAddrPort() + dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) _, err := dialer() assert.Error(t, err) - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) + err = errors.Wrap(ErrConnectionTimeout, err.Error()) assert.True(t, IsConnTimeout(err)) } diff --git a/privval/socket_listeners.go b/privval/socket_listeners.go index 7c8835791..f4d875e71 100644 --- a/privval/socket_listeners.go +++ b/privval/socket_listeners.go @@ -9,8 +9,8 @@ import ( ) const ( - defaultTimeoutAcceptSeconds = 3 - defaultTimeoutReadWriteSeconds = 3 + defaultTimeoutAcceptSeconds = 3 + defaultPingPeriodMilliseconds = 100 ) // timeoutError can be used to check if an error returned from the netp package diff --git a/privval/utils.go b/privval/utils.go index d8837bdf0..65368eb28 100644 --- a/privval/utils.go +++ b/privval/utils.go @@ -1,20 +1,62 @@ package privval import ( + "fmt" + "net" + + "github.com/pkg/errors" + + "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" ) // IsConnTimeout returns a boolean indicating whether the error is known to // report that a connection timeout occurred. This detects both fundamental // network timeouts, as well as ErrConnTimeout errors. func IsConnTimeout(err error) bool { - if cmnErr, ok := err.(cmn.Error); ok { - if cmnErr.Data() == ErrConnTimeout { - return true - } - } - if _, ok := err.(timeoutError); ok { + switch errors.Cause(err).(type) { + case EndpointTimeoutError: return true + case timeoutError: + return true + default: + return false } - return false +} + +// NewSignerListener creates a new SignerListenerEndpoint using the corresponding listen address +func NewSignerListener(listenAddr string, logger log.Logger) (*SignerListenerEndpoint, error) { + var listener net.Listener + + protocol, address := cmn.ProtocolAndAddress(listenAddr) + ln, err := net.Listen(protocol, address) + if err != nil { + return nil, err + } + switch protocol { + case "unix": + listener = NewUnixListener(ln) + case "tcp": + // TODO: persist this key so external signer can actually authenticate us + listener = NewTCPListener(ln, ed25519.GenPrivKey()) + default: + return nil, fmt.Errorf( + "wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", + protocol, + ) + } + + pve := NewSignerListenerEndpoint(logger.With("module", "privval"), listener) + + return pve, nil +} + +// GetFreeLocalhostAddrPort returns a free localhost:port address +func GetFreeLocalhostAddrPort() string { + port, err := cmn.GetFreePort() + if err != nil { + panic(err) + } + return fmt.Sprintf("127.0.0.1:%d", port) } diff --git a/privval/utils_test.go b/privval/utils_test.go index 23f6f6a3b..5648efec5 100644 --- a/privval/utils_test.go +++ b/privval/utils_test.go @@ -1,14 +1,13 @@ package privval import ( - "fmt" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" - cmn "github.com/tendermint/tendermint/libs/common" ) func TestIsConnTimeoutForNonTimeoutErrors(t *testing.T) { - assert.False(t, IsConnTimeout(cmn.ErrorWrap(ErrDialRetryMax, "max retries exceeded"))) - assert.False(t, IsConnTimeout(fmt.Errorf("completely irrelevant error"))) + assert.False(t, IsConnTimeout(errors.Wrap(ErrDialRetryMax, "max retries exceeded"))) + assert.False(t, IsConnTimeout(errors.New("completely irrelevant error"))) } diff --git a/rpc/client/codec.go b/rpc/client/codec.go new file mode 100644 index 000000000..ef1a00ec4 --- /dev/null +++ b/rpc/client/codec.go @@ -0,0 +1,12 @@ +package client + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + types.RegisterEvidences(cdc) +} diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 3fd13da37..85f065b61 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -333,6 +333,15 @@ func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, err return result, nil } +func (c *baseRPCClient) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + result := new(ctypes.ResultBroadcastEvidence) + _, err := c.caller.Call("broadcast_evidence", map[string]interface{}{"evidence": ev}, result) + if err != nil { + return nil, errors.Wrap(err, "BroadcastEvidence") + } + return result, nil +} + //----------------------------------------------------------------------------- // WSEvents diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 8f9ed9372..383e0b480 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -28,9 +28,24 @@ import ( "github.com/tendermint/tendermint/types" ) -// ABCIClient groups together the functionality that principally -// affects the ABCI app. In many cases this will be all we want, -// so we can accept an interface which is easier to mock +// Client wraps most important rpc calls a client would make if you want to +// listen for events, test if it also implements events.EventSwitch. +type Client interface { + cmn.Service + ABCIClient + EventsClient + HistoryClient + NetworkClient + SignClient + StatusClient + EvidenceClient +} + +// ABCIClient groups together the functionality that principally affects the +// ABCI app. +// +// In many cases this will be all we want, so we can accept an interface which +// is easier to mock. type ABCIClient interface { // Reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) @@ -44,8 +59,8 @@ type ABCIClient interface { BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) } -// SignClient groups together the interfaces need to get valid -// signatures and prove anything about the chain +// SignClient groups together the functionality needed to get valid signatures +// and prove anything about the chain. type SignClient interface { Block(height *int64) (*ctypes.ResultBlock, error) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) @@ -55,32 +70,19 @@ type SignClient interface { TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) } -// HistoryClient shows us data from genesis to now in large chunks. +// HistoryClient provides access to data from genesis to now in large chunks. type HistoryClient interface { Genesis() (*ctypes.ResultGenesis, error) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) } +// StatusClient provides access to general chain info. type StatusClient interface { - // General chain info Status() (*ctypes.ResultStatus, error) } -// Client wraps most important rpc calls a client would make -// if you want to listen for events, test if it also -// implements events.EventSwitch -type Client interface { - cmn.Service - ABCIClient - EventsClient - HistoryClient - NetworkClient - SignClient - StatusClient -} - -// NetworkClient is general info about the network state. May not -// be needed usually. +// NetworkClient is general info about the network state. May not be needed +// usually. type NetworkClient interface { NetInfo() (*ctypes.ResultNetInfo, error) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) @@ -110,3 +112,9 @@ type MempoolClient interface { UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) } + +// EvidenceClient is used for submitting an evidence of the malicious +// behaviour. +type EvidenceClient interface { + BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) +} diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 161f44fdf..3c3a1dcc6 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -157,6 +157,10 @@ func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.R return core.TxSearch(c.ctx, query, prove, page, perPage) } +func (c *Local) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(c.ctx, ev) +} + func (c *Local) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) { q, err := tmquery.New(query) if err != nil { diff --git a/rpc/client/main_test.go b/rpc/client/main_test.go index 6ec7b7b0e..d600b32f8 100644 --- a/rpc/client/main_test.go +++ b/rpc/client/main_test.go @@ -1,6 +1,7 @@ package client_test import ( + "io/ioutil" "os" "testing" @@ -13,7 +14,11 @@ var node *nm.Node func TestMain(m *testing.M) { // start a tendermint node (and kvstore) in the background to test against - app := kvstore.NewKVStoreApplication() + dir, err := ioutil.TempDir("/tmp", "rpc-client-test") + if err != nil { + panic(err) + } + app := kvstore.NewPersistentKVStoreApplication(dir) node = rpctest.StartTendermint(app) code := m.Run() diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go index c2e19b6d4..3ec40d6cc 100644 --- a/rpc/client/mock/client.go +++ b/rpc/client/mock/client.go @@ -36,6 +36,7 @@ type Client struct { client.HistoryClient client.StatusClient client.EventsClient + client.EvidenceClient cmn.Service } @@ -147,3 +148,7 @@ func (c Client) Commit(height *int64) (*ctypes.ResultCommit, error) { func (c Client) Validators(height *int64) (*ctypes.ResultValidators, error) { return core.Validators(&rpctypes.Context{}, height) } + +func (c Client) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(&rpctypes.Context{}, ev) +} diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index a1a48abc4..de5e18f11 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -1,7 +1,9 @@ package client_test import ( + "bytes" "fmt" + "math/rand" "net/http" "strings" "sync" @@ -12,7 +14,10 @@ import ( abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/tmhash" cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -446,6 +451,145 @@ func TestTxSearch(t *testing.T) { } } +func deepcpVote(vote *types.Vote) (res *types.Vote) { + res = &types.Vote{ + ValidatorAddress: make([]byte, len(vote.ValidatorAddress)), + ValidatorIndex: vote.ValidatorIndex, + Height: vote.Height, + Round: vote.Round, + Type: vote.Type, + BlockID: types.BlockID{ + Hash: make([]byte, len(vote.BlockID.Hash)), + PartsHeader: vote.BlockID.PartsHeader, + }, + Signature: make([]byte, len(vote.Signature)), + } + copy(res.ValidatorAddress, vote.ValidatorAddress) + copy(res.BlockID.Hash, vote.BlockID.Hash) + copy(res.Signature, vote.Signature) + return +} + +func newEvidence(t *testing.T, val *privval.FilePV, vote *types.Vote, vote2 *types.Vote, chainID string) types.DuplicateVoteEvidence { + var err error + vote2_ := deepcpVote(vote2) + vote2_.Signature, err = val.Key.PrivKey.Sign(vote2_.SignBytes(chainID)) + require.NoError(t, err) + + return types.DuplicateVoteEvidence{ + PubKey: val.Key.PubKey, + VoteA: vote, + VoteB: vote2_, + } +} + +func makeEvidences(t *testing.T, val *privval.FilePV, chainID string) (ev types.DuplicateVoteEvidence, fakes []types.DuplicateVoteEvidence) { + vote := &types.Vote{ + ValidatorAddress: val.Key.Address, + ValidatorIndex: 0, + Height: 1, + Round: 0, + Type: types.PrevoteType, + BlockID: types.BlockID{ + Hash: tmhash.Sum([]byte("blockhash")), + PartsHeader: types.PartSetHeader{ + Total: 1000, + Hash: tmhash.Sum([]byte("partset")), + }, + }, + } + + var err error + vote.Signature, err = val.Key.PrivKey.Sign(vote.SignBytes(chainID)) + require.NoError(t, err) + + vote2 := deepcpVote(vote) + vote2.BlockID.Hash = tmhash.Sum([]byte("blockhash2")) + + ev = newEvidence(t, val, vote, vote2, chainID) + + fakes = make([]types.DuplicateVoteEvidence, 42) + + // different address + vote2 = deepcpVote(vote) + for i := 0; i < 10; i++ { + rand.Read(vote2.ValidatorAddress) // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different index + vote2 = deepcpVote(vote) + for i := 10; i < 20; i++ { + vote2.ValidatorIndex = rand.Int()%100 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different height + vote2 = deepcpVote(vote) + for i := 20; i < 30; i++ { + vote2.Height = rand.Int63()%1000 + 100 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different round + vote2 = deepcpVote(vote) + for i := 30; i < 40; i++ { + vote2.Round = rand.Int()%10 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different type + vote2 = deepcpVote(vote) + vote2.Type = types.PrecommitType + fakes[40] = newEvidence(t, val, vote, vote2, chainID) + // exactly same vote + vote2 = deepcpVote(vote) + fakes[41] = newEvidence(t, val, vote, vote2, chainID) + return +} + +func TestBroadcastEvidenceDuplicateVote(t *testing.T) { + config := rpctest.GetConfig() + chainID := config.ChainID() + pvKeyFile := config.PrivValidatorKeyFile() + pvKeyStateFile := config.PrivValidatorStateFile() + pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) + + ev, fakes := makeEvidences(t, pv, chainID) + + t.Logf("evidence %v", ev) + + for i, c := range GetClients() { + t.Logf("client %d", i) + + result, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) + require.Nil(t, err) + require.Equal(t, ev.Hash(), result.Hash, "Invalid response, result %+v", result) + + status, err := c.Status() + require.NoError(t, err) + client.WaitForHeight(c, status.SyncInfo.LatestBlockHeight+2, nil) + + ed25519pub := ev.PubKey.(ed25519.PubKeyEd25519) + rawpub := ed25519pub[:] + result2, err := c.ABCIQuery("/val", rawpub) + require.Nil(t, err, "Error querying evidence, err %v", err) + qres := result2.Response + require.True(t, qres.IsOK(), "Response not OK") + + var v abci.ValidatorUpdate + err = abci.ReadMessage(bytes.NewReader(qres.Value), &v) + require.NoError(t, err, "Error reading query result, value %v", qres.Value) + + require.EqualValues(t, rawpub, v.PubKey.Data, "Stored PubKey not equal with expected, value %v", string(qres.Value)) + require.Equal(t, int64(9), v.Power, "Stored Power not equal with expected, value %v", string(qres.Value)) + + for _, fake := range fakes { + _, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{ + PubKey: fake.PubKey, + VoteA: fake.VoteA, + VoteB: fake.VoteB}) + require.Error(t, err, "Broadcasting fake evidence succeed: %s", fake.String()) + } + } +} + func TestBatchedJSONRPCCalls(t *testing.T) { c := getHTTPClient() testBatchedJSONRPCCalls(t, c) diff --git a/rpc/core/evidence.go b/rpc/core/evidence.go new file mode 100644 index 000000000..b2dfd097f --- /dev/null +++ b/rpc/core/evidence.go @@ -0,0 +1,39 @@ +package core + +import ( + ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/types" +) + +// Broadcast evidence of the misbehavior. +// +// ```shell +// curl 'localhost:26657/broadcast_evidence?evidence={amino-encoded DuplicateVoteEvidence}' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") +// err := client.Start() +// if err != nil { +// // handle error +// } +// defer client.Stop() +// res, err := client.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) +// ``` +// +// > The above command returns JSON structured like this: +// +// ```json +// ``` +// +// | Parameter | Type | Default | Required | Description | +// |-----------+----------------+---------+----------+-----------------------------| +// | evidence | types.Evidence | nil | true | Amino-encoded JSON evidence | +func BroadcastEvidence(ctx *rpctypes.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + err := evidencePool.AddEvidence(ev) + if err != nil { + return nil, err + } + return &ctypes.ResultBroadcastEvidence{Hash: ev.Hash()}, nil +} diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 28a492e6f..19abf62f6 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -1,12 +1,12 @@ package core import ( + "fmt" "time" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/crypto" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" @@ -14,6 +14,7 @@ import ( sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) const ( @@ -145,19 +146,24 @@ func SetConfig(c cfg.RPCConfig) { config = c } -func validatePage(page, perPage, totalCount int) int { +func validatePage(page, perPage, totalCount int) (int, error) { if perPage < 1 { - return 1 + panic(fmt.Sprintf("zero or negative perPage: %d", perPage)) + } + + if page == 0 { + return 1, nil // default } pages := ((totalCount - 1) / perPage) + 1 - if page < 1 { - page = 1 - } else if page > pages { - page = pages + if pages == 0 { + pages = 1 // one page (even if it's empty) + } + if page < 0 || page > pages { + return 1, fmt.Errorf("page should be within [0, %d] range, given %d", pages, page) } - return page + return page, nil } func validatePerPage(perPage int) int { diff --git a/rpc/core/pipe_test.go b/rpc/core/pipe_test.go index 19ed11fcc..93aff3e58 100644 --- a/rpc/core/pipe_test.go +++ b/rpc/core/pipe_test.go @@ -14,33 +14,39 @@ func TestPaginationPage(t *testing.T) { perPage int page int newPage int + expErr bool }{ - {0, 0, 1, 1}, + {0, 10, 1, 1, false}, - {0, 10, 0, 1}, - {0, 10, 1, 1}, - {0, 10, 2, 1}, + {0, 10, 0, 1, false}, + {0, 10, 1, 1, false}, + {0, 10, 2, 0, true}, - {5, 10, -1, 1}, - {5, 10, 0, 1}, - {5, 10, 1, 1}, - {5, 10, 2, 1}, - {5, 10, 2, 1}, + {5, 10, -1, 0, true}, + {5, 10, 0, 1, false}, + {5, 10, 1, 1, false}, + {5, 10, 2, 0, true}, + {5, 10, 2, 0, true}, - {5, 5, 1, 1}, - {5, 5, 2, 1}, - {5, 5, 3, 1}, + {5, 5, 1, 1, false}, + {5, 5, 2, 0, true}, + {5, 5, 3, 0, true}, - {5, 3, 2, 2}, - {5, 3, 3, 2}, + {5, 3, 2, 2, false}, + {5, 3, 3, 0, true}, - {5, 2, 2, 2}, - {5, 2, 3, 3}, - {5, 2, 4, 3}, + {5, 2, 2, 2, false}, + {5, 2, 3, 3, false}, + {5, 2, 4, 0, true}, } for _, c := range cases { - p := validatePage(c.page, c.perPage, c.totalCount) + p, err := validatePage(c.page, c.perPage, c.totalCount) + if c.expErr { + assert.Error(t, err) + continue + } + assert.Equal(t, c.newPage, p, fmt.Sprintf("%v", c)) } diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 736ded607..df7cef905 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -30,7 +30,7 @@ var Routes = map[string]*rpc.RPCFunc{ "unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, "limit"), "num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""), - // broadcast API + // tx broadcast API "broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommit, "tx"), "broadcast_tx_sync": rpc.NewRPCFunc(BroadcastTxSync, "tx"), "broadcast_tx_async": rpc.NewRPCFunc(BroadcastTxAsync, "tx"), @@ -38,6 +38,9 @@ var Routes = map[string]*rpc.RPCFunc{ // abci API "abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"), "abci_info": rpc.NewRPCFunc(ABCIInfo, ""), + + // evidence API + "broadcast_evidence": rpc.NewRPCFunc(BroadcastEvidence, "evidence"), } func AddUnsafeRoutes() { diff --git a/rpc/core/tx.go b/rpc/core/tx.go index 575553f85..dba457c30 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -202,7 +202,10 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int totalCount := len(results) perPage = validatePerPage(perPage) - page = validatePage(page, perPage, totalCount) + page, err = validatePage(page, perPage, totalCount) + if err != nil { + return nil, err + } skipCount := validateSkipCount(page, perPage) apiResults := make([]*ctypes.ResultTx, cmn.MinInt(perPage, totalCount-skipCount)) diff --git a/rpc/core/types/wire.go b/rpc/core/types/codec.go similarity index 100% rename from rpc/core/types/wire.go rename to rpc/core/types/codec.go diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index f1ae16a39..f8a9476f3 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -194,6 +194,11 @@ type ResultABCIQuery struct { Response abci.ResponseQuery `json:"response"` } +// Result of broadcasting evidence +type ResultBroadcastEvidence struct { + Hash []byte `json:"hash"` +} + // empty results type ( ResultUnsafeFlushMempool struct{} diff --git a/rpc/grpc/client_server.go b/rpc/grpc/client_server.go index 922016dd5..d02120e10 100644 --- a/rpc/grpc/client_server.go +++ b/rpc/grpc/client_server.go @@ -2,8 +2,8 @@ package core_grpc import ( "net" - "time" + "golang.org/x/net/context" "google.golang.org/grpc" cmn "github.com/tendermint/tendermint/libs/common" @@ -26,13 +26,13 @@ func StartGRPCServer(ln net.Listener) error { // StartGRPCClient dials the gRPC server using protoAddr and returns a new // BroadcastAPIClient. func StartGRPCClient(protoAddr string) BroadcastAPIClient { - conn, err := grpc.Dial(protoAddr, grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial(protoAddr, grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { panic(err) } return NewBroadcastAPIClient(conn) } -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } diff --git a/rpc/lib/client/http_client.go b/rpc/lib/client/http_client.go index 3b545a5dd..db57c536e 100644 --- a/rpc/lib/client/http_client.go +++ b/rpc/lib/client/http_client.go @@ -42,12 +42,13 @@ func makeHTTPDialer(remoteAddr string) (string, string, func(string, string) (ne parts := strings.SplitN(remoteAddr, "://", 2) var protocol, address string - if len(parts) == 1 { + switch { + case len(parts) == 1: // default to tcp if nothing specified protocol, address = protoTCP, remoteAddr - } else if len(parts) == 2 { + case len(parts) == 2: protocol, address = parts[0], parts[1] - } else { + default: // return a invalid message msg := fmt.Sprintf("Invalid addr: %s", remoteAddr) return clientProtocol, msg, func(_ string, _ string) (net.Conn, error) { diff --git a/rpc/lib/client/ws_client.go b/rpc/lib/client/ws_client.go index e3b559569..05180c753 100644 --- a/rpc/lib/client/ws_client.go +++ b/rpc/lib/client/ws_client.go @@ -369,10 +369,11 @@ func (c *WSClient) writeRoutine() { defer func() { ticker.Stop() - if err := c.conn.Close(); err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - } + c.conn.Close() + // err != nil { + // ignore error; it will trigger in tests + // likely because it's closing an already closed connection + // } c.wg.Done() }() @@ -421,10 +422,11 @@ func (c *WSClient) writeRoutine() { // executing all reads from this goroutine. func (c *WSClient) readRoutine() { defer func() { - if err := c.conn.Close(); err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - } + c.conn.Close() + // err != nil { + // ignore error; it will trigger in tests + // likely because it's closing an already closed connection + // } c.wg.Done() }() diff --git a/rpc/lib/client/ws_client_test.go b/rpc/lib/client/ws_client_test.go index e902fe21a..4f2cc9ada 100644 --- a/rpc/lib/client/ws_client_test.go +++ b/rpc/lib/client/ws_client_test.go @@ -212,7 +212,8 @@ func callWgDoneOnResult(t *testing.T, c *WSClient, wg *sync.WaitGroup) { select { case resp := <-c.ResponsesCh: if resp.Error != nil { - t.Fatalf("unexpected error: %v", resp.Error) + t.Errorf("unexpected error: %v", resp.Error) + return } if resp.Result != nil { wg.Done() diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index c1c1ebf1a..5b5c9f8b9 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -339,13 +339,14 @@ func jsonStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Val func nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { if rt.Kind() == reflect.Ptr { rv_, err, ok := nonJSONStringToArg(cdc, rt.Elem(), arg) - if err != nil { + switch { + case err != nil: return reflect.Value{}, err, false - } else if ok { + case ok: rv := reflect.New(rt.Elem()) rv.Elem().Set(rv_) return rv, nil, true - } else { + default: return reflect.Value{}, nil, false } } else { @@ -448,6 +449,9 @@ type wsConnection struct { // Send pings to server with this period. Must be less than readWait, but greater than zero. pingPeriod time.Duration + // Maximum message size. + readLimit int64 + // callback which is called upon disconnect onDisconnect func(remoteAddr string) @@ -467,7 +471,6 @@ func NewWSConnection( cdc *amino.Codec, options ...func(*wsConnection), ) *wsConnection { - baseConn.SetReadLimit(maxBodyBytes) wsc := &wsConnection{ remoteAddr: baseConn.RemoteAddr().String(), baseConn: baseConn, @@ -481,6 +484,7 @@ func NewWSConnection( for _, option := range options { option(wsc) } + wsc.baseConn.SetReadLimit(wsc.readLimit) wsc.BaseService = *cmn.NewBaseService(nil, "wsConnection", wsc) return wsc } @@ -525,6 +529,14 @@ func PingPeriod(pingPeriod time.Duration) func(*wsConnection) { } } +// ReadLimit sets the maximum size for reading message. +// It should only be used in the constructor - not Goroutine-safe. +func ReadLimit(readLimit int64) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.readLimit = readLimit + } +} + // OnStart implements cmn.Service by starting the read and write routines. It // blocks until the connection closes. func (wsc *wsConnection) OnStart() error { @@ -724,12 +736,10 @@ func (wsc *wsConnection) writeRoutine() { jsonBytes, err := json.MarshalIndent(msg, "", " ") if err != nil { wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) - } else { - if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { - wsc.Logger.Error("Failed to write response", "err", err) - wsc.Stop() - return - } + } else if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { + wsc.Logger.Error("Failed to write response", "err", err) + wsc.Stop() + return } case <-wsc.Quit(): return diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go index 7825605eb..c97739bd2 100644 --- a/rpc/lib/server/http_server.go +++ b/rpc/lib/server/http_server.go @@ -26,6 +26,11 @@ type Config struct { ReadTimeout time.Duration // mirrors http.Server#WriteTimeout WriteTimeout time.Duration + // MaxBodyBytes controls the maximum number of bytes the + // server will read parsing the request body. + MaxBodyBytes int64 + // mirrors http.Server#MaxHeaderBytes + MaxHeaderBytes int } // DefaultConfig returns a default configuration. @@ -34,28 +39,21 @@ func DefaultConfig() *Config { MaxOpenConnections: 0, // unlimited ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + MaxBodyBytes: int64(1000000), // 1MB + MaxHeaderBytes: 1 << 20, // same as the net/http default } } -const ( - // maxBodyBytes controls the maximum number of bytes the - // server will read parsing the request body. - maxBodyBytes = int64(1000000) // 1MB - - // same as the net/http default - maxHeaderBytes = 1 << 20 -) - // StartHTTPServer takes a listener and starts an HTTP server with the given handler. // It wraps handler with RecoverAndLogHandler. // NOTE: This function blocks - you may want to call it in a go-routine. func StartHTTPServer(listener net.Listener, handler http.Handler, logger log.Logger, config *Config) error { logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr())) s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), ReadTimeout: config.ReadTimeout, WriteTimeout: config.WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, + MaxHeaderBytes: config.MaxHeaderBytes, } err := s.Serve(listener) logger.Info("RPC HTTP server stopped", "err", err) @@ -75,10 +73,10 @@ func StartHTTPAndTLSServer( logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)", listener.Addr(), certFile, keyFile)) s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), ReadTimeout: config.ReadTimeout, WriteTimeout: config.WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, + MaxHeaderBytes: config.MaxHeaderBytes, } err := s.ServeTLS(listener, certFile, keyFile) diff --git a/scripts/gitian-build.sh b/scripts/gitian-build.sh new file mode 100755 index 000000000..a7a6acec3 --- /dev/null +++ b/scripts/gitian-build.sh @@ -0,0 +1,201 @@ +#!/bin/bash + +# symbol prefixes: +# g_ -> global +# l_ - local variable +# f_ -> function + +set -euo pipefail + +GITIAN_CACHE_DIRNAME='.gitian-builder-cache' +GO_DEBIAN_RELEASE='1.12.5-1' +GO_TARBALL="golang-debian-${GO_DEBIAN_RELEASE}.tar.gz" +GO_TARBALL_URL="https://salsa.debian.org/go-team/compiler/golang/-/archive/debian/${GO_DEBIAN_RELEASE}/${GO_TARBALL}" + +# Defaults + +DEFAULT_SIGN_COMMAND='gpg --detach-sign' +DEFAULT_TENDERMINT_SIGS=${TENDERMINT_SIGS:-'tendermint.sigs'} +DEFAULT_GITIAN_REPO='https://github.com/devrandom/gitian-builder' +DEFAULT_GBUILD_FLAGS='' +DEFAULT_SIGS_REPO='https://github.com/tendermint/tendermint.sigs' + +# Overrides + +SIGN_COMMAND=${SIGN_COMMAND:-${DEFAULT_SIGN_COMMAND}} +GITIAN_REPO=${GITIAN_REPO:-${DEFAULT_GITIAN_REPO}} +GBUILD_FLAGS=${GBUILD_FLAGS:-${DEFAULT_GBUILD_FLAGS}} + +# Globals + +g_workdir='' +g_gitian_cache='' +g_cached_gitian='' +g_cached_go_tarball='' +g_sign_identity='' +g_sigs_dir='' +g_flag_commit='' + + +f_help() { + cat >&2 <&2 + mkdir "${l_builddir}/inputs/" + cp -v "${g_cached_go_tarball}" "${l_builddir}/inputs/" + done +} + +f_build() { + local l_descriptor + + l_descriptor=$1 + + bin/gbuild --commit tendermint="$g_commit" ${GBUILD_FLAGS} "$l_descriptor" + libexec/stop-target || f_echo_stderr "warning: couldn't stop target" +} + +f_sign_verify() { + local l_descriptor + + l_descriptor=$1 + + bin/gsign -p "${SIGN_COMMAND}" -s "${g_sign_identity}" --destination="${g_sigs_dir}" --release=${g_release} ${l_descriptor} + bin/gverify --destination="${g_sigs_dir}" --release="${g_release}" ${l_descriptor} +} + +f_commit_sig() { + local l_release_name + + l_release_name=$1 + + pushd "${g_sigs_dir}" + git add . || echo "git add failed" >&2 + git commit -m "Add ${l_release_name} reproducible build" || echo "git commit failed" >&2 + popd +} + +f_prep_docker_image() { + pushd $1 + bin/make-base-vm --docker --suite bionic --arch amd64 + popd +} + +f_ensure_cache() { + g_gitian_cache="${g_workdir}/${GITIAN_CACHE_DIRNAME}" + [ -d "${g_gitian_cache}" ] || mkdir "${g_gitian_cache}" + + g_cached_go_tarball="${g_gitian_cache}/${GO_TARBALL}" + if [ ! -f "${g_cached_go_tarball}" ]; then + f_echo_stderr "${g_cached_go_tarball}: cache miss, caching..." + curl -L "${GO_TARBALL_URL}" --output "${g_cached_go_tarball}" + fi + + g_cached_gitian="${g_gitian_cache}/gitian-builder" + if [ ! -d "${g_cached_gitian}" ]; then + f_echo_stderr "${g_cached_gitian}: cache miss, caching..." + git clone ${GITIAN_REPO} "${g_cached_gitian}" + fi +} + +f_demangle_platforms() { + case "${1}" in + all) + printf '%s' 'darwin linux windows' ;; + linux|darwin|windows) + printf '%s' "${1}" ;; + *) + echo "invalid platform -- ${1}" + exit 1 + esac +} + +f_echo_stderr() { + echo $@ >&2 +} + + +while getopts ":cs:h" opt; do + case "${opt}" in + h) f_help ; exit 0 ;; + c) g_flag_commit=y ;; + s) g_sign_identity="${OPTARG}" ;; + esac +done + +shift "$((OPTIND-1))" + +g_platforms=$(f_demangle_platforms "${1}") +g_workdir="$(pwd)" +g_commit="$(git rev-parse HEAD)" +g_sigs_dir=${TENDERMINT_SIGS:-"${g_workdir}/${DEFAULT_TENDERMINT_SIGS}"} + +f_ensure_cache + +f_prep_docker_image "${g_cached_gitian}" + +f_prep_build "${g_platforms}" + +export USE_DOCKER=1 +for g_os in ${g_platforms}; do + g_release="$(git describe --tags --abbrev=9 | sed 's/^v//')-${g_os}" + g_descriptor="${g_workdir}/scripts/gitian-descriptors/gitian-${g_os}.yml" + [ -f ${g_descriptor} ] + g_builddir="$(f_builddir ${g_os})" + + pushd "${g_builddir}" + f_build "${g_descriptor}" + if [ -n "${g_sign_identity}" ]; then + f_sign_verify "${g_descriptor}" + fi + popd + + if [ -n "${g_sign_identity}" -a -n "${g_flag_commit}" ]; then + [ -d "${g_sigs_dir}/.git/" ] && f_commit_sig ${g_release} || f_echo_stderr "couldn't commit, ${g_sigs_dir} is not a git clone" + fi +done + +exit 0 diff --git a/scripts/gitian-descriptors/gitian-darwin.yml b/scripts/gitian-descriptors/gitian-darwin.yml new file mode 100644 index 000000000..03ba1f1a4 --- /dev/null +++ b/scripts/gitian-descriptors/gitian-darwin.yml @@ -0,0 +1,111 @@ +--- +name: "tendermint-darwin" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOOS=darwin + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-darwin-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-descriptors/gitian-linux.yml b/scripts/gitian-descriptors/gitian-linux.yml new file mode 100644 index 000000000..f1c31c40e --- /dev/null +++ b/scripts/gitian-descriptors/gitian-linux.yml @@ -0,0 +1,110 @@ +--- +name: "tendermint-linux" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64 arm arm64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-linux-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-descriptors/gitian-windows.yml b/scripts/gitian-descriptors/gitian-windows.yml new file mode 100644 index 000000000..80b2e60d3 --- /dev/null +++ b/scripts/gitian-descriptors/gitian-windows.yml @@ -0,0 +1,111 @@ +--- +name: "tendermint-windows" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOOS=windows + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint.exe ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-windows-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-keys/README.md b/scripts/gitian-keys/README.md new file mode 100644 index 000000000..f4ad711a9 --- /dev/null +++ b/scripts/gitian-keys/README.md @@ -0,0 +1,29 @@ +## PGP keys of Gitian builders and Tendermint Developers + +The file `keys.txt` contains fingerprints of the public keys of Gitian builders +and active developers. + +The associated keys are mainly used to sign git commits or the build results +of Gitian builds. + +The most recent version of each pgp key can be found on most PGP key servers. + +Fetch the latest version from the key server to see if any key was revoked in +the meantime. +To fetch the latest version of all pgp keys in your gpg homedir, + +```sh +gpg --refresh-keys +``` + +To fetch keys of Gitian builders and active core developers, feed the list of +fingerprints of the primary keys into gpg: + +```sh +while read fingerprint keyholder_name; \ +do gpg --keyserver hkp://subset.pool.sks-keyservers.net \ +--recv-keys ${fingerprint}; done < ./keys.txt +``` + +Add your key to the list if you are a Tendermint core developer or you have +provided Gitian signatures for two major or minor releases of Tendermint. diff --git a/scripts/gitian-keys/keys.txt b/scripts/gitian-keys/keys.txt new file mode 100644 index 000000000..91330ae0b --- /dev/null +++ b/scripts/gitian-keys/keys.txt @@ -0,0 +1 @@ +04160004A8276E40BB9890FBE8A48AE5311D765A Alessio Treglia diff --git a/state/wire.go b/state/codec.go similarity index 100% rename from state/wire.go rename to state/codec.go diff --git a/state/execution.go b/state/execution.go index fd75b2959..2ac2e9689 100644 --- a/state/execution.go +++ b/state/execution.go @@ -5,12 +5,12 @@ import ( "time" abci "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/fail" "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) //----------------------------------------------------------------------------- @@ -249,8 +249,7 @@ func execBlockOnProxyApp( // Execute transactions and get hash. proxyCb := func(req *abci.Request, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_DeliverTx: + if r, ok := res.Value.(*abci.Response_DeliverTx); ok { // TODO: make use of res.Log // TODO: make use of this info // Blocks may include invalid txs. diff --git a/state/export_test.go b/state/export_test.go index af7f5cc23..823eb4251 100644 --- a/state/export_test.go +++ b/state/export_test.go @@ -2,8 +2,8 @@ package state import ( abci "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) // diff --git a/state/helpers_test.go b/state/helpers_test.go index e8cb27585..d6589c574 100644 --- a/state/helpers_test.go +++ b/state/helpers_test.go @@ -7,11 +7,11 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-db" ) type paramsChangeTestCase struct { @@ -69,29 +69,11 @@ func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commi return state, blockID, nil } -func makeVote(height int64, blockID types.BlockID, valSet *types.ValidatorSet, privVal types.PrivValidator) (*types.Vote, error) { - addr := privVal.GetPubKey().Address() - idx, _ := valSet.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: height, - Round: 0, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - if err := privVal.SignVote(chainID, vote); err != nil { - return nil, err - } - return vote, nil -} - func makeValidCommit(height int64, blockID types.BlockID, vals *types.ValidatorSet, privVals map[string]types.PrivValidator) (*types.Commit, error) { sigs := make([]*types.CommitSig, 0) for i := 0; i < vals.Size(); i++ { _, val := vals.GetByIndex(i) - vote, err := makeVote(height, blockID, vals, privVals[val.Address.String()]) + vote, err := types.MakeVote(height, blockID, vals, privVals[val.Address.String()], chainID) if err != nil { return nil, err } diff --git a/state/state_test.go b/state/state_test.go index a0f7a4a2a..062e62bb5 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -13,8 +13,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" sm "github.com/tendermint/tendermint/state" + dbm "github.com/tendermint/tm-db" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/types" @@ -185,11 +185,11 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert := assert.New(t) // Can't load anything for height 0. - v, err := sm.LoadValidators(stateDB, 0) + _, err := sm.LoadValidators(stateDB, 0) assert.IsType(sm.ErrNoValSetForHeight{}, err, "expected err at height 0") // Should be able to load for height 1. - v, err = sm.LoadValidators(stateDB, 1) + v, err := sm.LoadValidators(stateDB, 1) assert.Nil(err, "expected no err at height 1") assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match") @@ -440,13 +440,13 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { // 3. Center - with avg, resulting val2:-61, val1:62 avg := big.NewInt(0).Add(big.NewInt(wantVal1Prio), big.NewInt(wantVal2Prio)) avg.Div(avg, big.NewInt(2)) - wantVal2Prio = wantVal2Prio - avg.Int64() // -61 - wantVal1Prio = wantVal1Prio - avg.Int64() // 62 + wantVal2Prio -= avg.Int64() // -61 + wantVal1Prio -= avg.Int64() // 62 // 4. Steps from IncrementProposerPriority - wantVal1Prio = wantVal1Prio + val1VotingPower // 72 - wantVal2Prio = wantVal2Prio + val2VotingPower // 39 - wantVal1Prio = wantVal1Prio - totalPowerAfter // -38 as val1 is proposer + wantVal1Prio += val1VotingPower // 72 + wantVal2Prio += val2VotingPower // 39 + wantVal1Prio -= totalPowerAfter // -38 as val1 is proposer assert.Equal(t, wantVal1Prio, updatedVal1.ProposerPriority) assert.Equal(t, wantVal2Prio, addedVal2.ProposerPriority) @@ -563,9 +563,9 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { expectedVal2Prio := v2PrioWhenAddedVal2 - avg.Int64() // -11 expectedVal1Prio := oldVal1.ProposerPriority - avg.Int64() // 11 // 4. Increment - expectedVal2Prio = expectedVal2Prio + val2VotingPower // -11 + 10 = -1 - expectedVal1Prio = expectedVal1Prio + val1VotingPower // 11 + 10 == 21 - expectedVal1Prio = expectedVal1Prio - totalPower // 1, val1 proposer + expectedVal2Prio += val2VotingPower // -11 + 10 = -1 + expectedVal1Prio += val1VotingPower // 11 + 10 == 21 + expectedVal1Prio -= totalPower // 1, val1 proposer assert.EqualValues(t, expectedVal1Prio, updatedVal1.ProposerPriority) assert.EqualValues(t, expectedVal2Prio, updatedVal2.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) @@ -589,7 +589,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { // Increment expectedVal2Prio2 := expectedVal2Prio + val2VotingPower // -1 + 10 = 9 expectedVal1Prio2 := expectedVal1Prio + val1VotingPower // 1 + 10 == 11 - expectedVal1Prio2 = expectedVal1Prio2 - totalPower // -9, val1 proposer + expectedVal1Prio2 -= totalPower // -9, val1 proposer assert.EqualValues(t, expectedVal1Prio2, updatedVal1.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) assert.EqualValues(t, expectedVal2Prio2, updatedVal2.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) diff --git a/state/store.go b/state/store.go index f0bb9e142..4f47ace5f 100644 --- a/state/store.go +++ b/state/store.go @@ -5,8 +5,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) const ( diff --git a/state/store_test.go b/state/store_test.go index 0cf217722..4549e8f89 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/require" cfg "github.com/tendermint/tendermint/config" - dbm "github.com/tendermint/tendermint/libs/db" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) func TestStoreLoadValidators(t *testing.T) { diff --git a/state/tx_filter_test.go b/state/tx_filter_test.go index bd3243168..21c4daf14 100644 --- a/state/tx_filter_test.go +++ b/state/tx_filter_test.go @@ -8,9 +8,9 @@ import ( "github.com/stretchr/testify/require" cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) func TestTxFilter(t *testing.T) { diff --git a/state/txindex/indexer_service_test.go b/state/txindex/indexer_service_test.go index 079f9cec2..277304c45 100644 --- a/state/txindex/indexer_service_test.go +++ b/state/txindex/indexer_service_test.go @@ -8,11 +8,11 @@ import ( "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/state/txindex/kv" "github.com/tendermint/tendermint/types" + db "github.com/tendermint/tm-db" ) func TestIndexerServiceIndexesBlocks(t *testing.T) { diff --git a/state/txindex/kv/wire.go b/state/txindex/kv/codec.go similarity index 100% rename from state/txindex/kv/wire.go rename to state/txindex/kv/codec.go diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 053d26a71..2695e18a0 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -11,8 +11,9 @@ import ( "github.com/pkg/errors" + dbm "github.com/tendermint/tm-db" + cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" @@ -163,8 +164,8 @@ func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.S // 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. func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { - var hashes [][]byte var hashesInitialized bool + filteredHashes := make(map[string][]byte) // get a list of conditions (like "tx.height > 5") conditions := q.Conditions() @@ -193,10 +194,16 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { for _, r := range ranges { if !hashesInitialized { - hashes = txi.matchRange(r, startKey(r.key)) + filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, true) hashesInitialized = true + + // Ignore any remaining conditions if the first condition resulted + // in no matches (assuming implicit AND operand). + if len(filteredHashes) == 0 { + break + } } else { - hashes = intersect(hashes, txi.matchRange(r, startKey(r.key))) + filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, false) } } } @@ -211,21 +218,26 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { } if !hashesInitialized { - hashes = txi.match(c, startKeyForCondition(c, height)) + filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, true) hashesInitialized = true + + // Ignore any remaining conditions if the first condition resulted + // in no matches (assuming implicit AND operand). + if len(filteredHashes) == 0 { + break + } } else { - hashes = intersect(hashes, txi.match(c, startKeyForCondition(c, height))) + filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, false) } } - results := make([]*types.TxResult, len(hashes)) - i := 0 - for _, h := range hashes { - results[i], err = txi.Get(h) + results := make([]*types.TxResult, 0, len(filteredHashes)) + for _, h := range filteredHashes { + res, err := txi.Get(h) if err != nil { return nil, errors.Wrapf(err, "failed to get Tx{%X}", h) } - i++ + results = append(results, res) } // sort by height & index by default @@ -353,63 +365,115 @@ func isRangeOperation(op query.Operator) bool { } } -func (txi *TxIndex) match(c query.Condition, startKeyBz []byte) (hashes [][]byte) { - if c.Op == query.OpEqual { +// match returns all matching txs by hash that meet a given condition and start +// key. An already filtered result (filteredHashes) is provided such that any +// non-intersecting matches are removed. +// +// NOTE: filteredHashes may be empty if no previous condition has matched. +func (txi *TxIndex) match(c query.Condition, startKeyBz []byte, filteredHashes map[string][]byte, firstRun bool) map[string][]byte { + // A previous match was attempted but resulted in no matches, so we return + // no matches (assuming AND operand). + if !firstRun && len(filteredHashes) == 0 { + return filteredHashes + } + + tmpHashes := make(map[string][]byte) + + switch { + case c.Op == query.OpEqual: it := dbm.IteratePrefix(txi.store, startKeyBz) defer it.Close() + for ; it.Valid(); it.Next() { - hashes = append(hashes, it.Value()) + tmpHashes[string(it.Value())] = it.Value() } - } else if c.Op == query.OpContains { + + case c.Op == query.OpContains: // XXX: startKey does not apply here. // For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an" // we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/" it := dbm.IteratePrefix(txi.store, startKey(c.Tag)) defer it.Close() + for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } + if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { - hashes = append(hashes, it.Value()) + tmpHashes[string(it.Value())] = it.Value() } } - } else { + default: panic("other operators should be handled already") } - return + + if len(tmpHashes) == 0 || firstRun { + // Either: + // + // 1. Regardless if a previous match was attempted, which may have had + // results, but no match was found for the current condition, then we + // return no matches (assuming AND operand). + // + // 2. A previous match was not attempted, so we return all results. + return tmpHashes + } + + // Remove/reduce matches in filteredHashes that were not found in this + // match (tmpHashes). + for k := range filteredHashes { + if tmpHashes[k] == nil { + delete(filteredHashes, k) + } + } + + return filteredHashes } -func (txi *TxIndex) matchRange(r queryRange, startKey []byte) (hashes [][]byte) { - // create a map to prevent duplicates - hashesMap := make(map[string][]byte) +// matchRange returns all matching txs by hash that meet a given queryRange and +// start key. An already filtered result (filteredHashes) is provided such that +// any non-intersecting matches are removed. +// +// NOTE: filteredHashes may be empty if no previous condition has matched. +func (txi *TxIndex) matchRange(r queryRange, startKey []byte, filteredHashes map[string][]byte, firstRun bool) map[string][]byte { + // A previous match was attempted but resulted in no matches, so we return + // no matches (assuming AND operand). + if !firstRun && len(filteredHashes) == 0 { + return filteredHashes + } + tmpHashes := make(map[string][]byte) lowerBound := r.lowerBoundValue() upperBound := r.upperBoundValue() it := dbm.IteratePrefix(txi.store, startKey) defer it.Close() + LOOP: for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } - switch r.AnyBound().(type) { - case int64: + + if _, ok := r.AnyBound().(int64); ok { v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) if err != nil { continue LOOP } + include := true if lowerBound != nil && v < lowerBound.(int64) { include = false } + if upperBound != nil && v > upperBound.(int64) { include = false } + if include { - hashesMap[fmt.Sprintf("%X", it.Value())] = it.Value() + tmpHashes[string(it.Value())] = it.Value() } + // XXX: passing time in a ABCI Tags is not yet implemented // case time.Time: // v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) @@ -418,13 +482,27 @@ LOOP: // } } } - hashes = make([][]byte, len(hashesMap)) - i := 0 - for _, h := range hashesMap { - hashes[i] = h - i++ + + if len(tmpHashes) == 0 || firstRun { + // Either: + // + // 1. Regardless if a previous match was attempted, which may have had + // results, but no match was found for the current condition, then we + // return no matches (assuming AND operand). + // + // 2. A previous match was not attempted, so we return all results. + return tmpHashes } - return + + // Remove/reduce matches in filteredHashes that were not found in this + // match (tmpHashes). + for k := range filteredHashes { + if tmpHashes[k] == nil { + delete(filteredHashes, k) + } + } + + return filteredHashes } /////////////////////////////////////////////////////////////////////////////// @@ -471,18 +549,3 @@ func startKey(fields ...interface{}) []byte { } return b.Bytes() } - -/////////////////////////////////////////////////////////////////////////////// -// Utils - -func intersect(as, bs [][]byte) [][]byte { - i := make([][]byte, 0, cmn.MinInt(len(as), len(bs))) - for _, a := range as { - for _, b := range bs { - if bytes.Equal(a, b) { - i = append(i, a) - } - } - } - return i -} diff --git a/state/txindex/kv/kv_bench_test.go b/state/txindex/kv/kv_bench_test.go new file mode 100644 index 000000000..9c3442a01 --- /dev/null +++ b/state/txindex/kv/kv_bench_test.go @@ -0,0 +1,72 @@ +package kv + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "testing" + + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/pubsub/query" + "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" +) + +func BenchmarkTxSearch(b *testing.B) { + dbDir, err := ioutil.TempDir("", "benchmark_tx_search_test") + if err != nil { + b.Errorf("failed to create temporary directory: %s", err) + } + + db, err := dbm.NewGoLevelDB("benchmark_tx_search_test", dbDir) + if err != nil { + b.Errorf("failed to create database: %s", err) + } + + allowedTags := []string{"transfer.address", "transfer.amount"} + indexer := NewTxIndex(db, IndexTags(allowedTags)) + + for i := 0; i < 35000; i++ { + events := []abci.Event{ + { + Type: "transfer", + Attributes: []cmn.KVPair{ + {Key: []byte("address"), Value: []byte(fmt.Sprintf("address_%d", i%100))}, + {Key: []byte("amount"), Value: []byte("50")}, + }, + }, + } + + txBz := make([]byte, 8) + if _, err := rand.Read(txBz); err != nil { + b.Errorf("failed produce random bytes: %s", err) + } + + txResult := &types.TxResult{ + Height: int64(i), + Index: 0, + Tx: types.Tx(string(txBz)), + Result: abci.ResponseDeliverTx{ + Data: []byte{0}, + Code: abci.CodeTypeOK, + Log: "", + Events: events, + }, + } + + if err := indexer.Index(txResult); err != nil { + b.Errorf("failed to index tx: %s", err) + } + } + + txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50") + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + if _, err := indexer.Search(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 cacfaad01..a0c833e49 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -8,10 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + db "github.com/tendermint/tm-db" + abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" - db "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" @@ -89,6 +90,11 @@ func TestTxSearch(t *testing.T) { {"account.number = 1 AND account.owner = 'Ivan'", 1}, // search by exact match (two tags) {"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 @@ -310,7 +316,7 @@ func benchmarkTxIndex(txsCount int64, b *testing.B) { } defer os.RemoveAll(dir) // nolint: errcheck - store := db.NewDB("tx_index", "leveldb", dir) + store := db.NewDB("tx_index", "goleveldb", dir) indexer := NewTxIndex(store) batch := txindex.NewBatch(txsCount) diff --git a/state/validation.go b/state/validation.go index 1d365e90c..f2218f15c 100644 --- a/state/validation.go +++ b/state/validation.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/tendermint/tendermint/crypto" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" ) //----------------------------------------------------- diff --git a/state/validation_test.go b/state/validation_test.go index c53cf0102..c0dd6e569 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -101,7 +101,7 @@ func TestValidateBlockCommit(t *testing.T) { #2589: ensure state.LastValidators.VerifyCommit fails here */ // should be height-1 instead of height - wrongHeightVote, err := makeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()]) + wrongHeightVote, err := types.MakeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()], chainID) require.NoError(t, err, "height %d", height) wrongHeightCommit := types.NewCommit(state.LastBlockID, []*types.CommitSig{wrongHeightVote.CommitSig()}) block, _ := state.MakeBlock(height, makeTxs(height), wrongHeightCommit, nil, proposerAddr) @@ -129,7 +129,7 @@ func TestValidateBlockCommit(t *testing.T) { /* wrongPrecommitsCommit is fine except for the extra bad precommit */ - goodVote, err := makeVote(height, blockID, state.Validators, privVals[proposerAddr.String()]) + goodVote, err := types.MakeVote(height, blockID, state.Validators, privVals[proposerAddr.String()], chainID) require.NoError(t, err, "height %d", height) badVote := &types.Vote{ ValidatorAddress: badPrivVal.GetPubKey().Address(), diff --git a/store/codec.go b/store/codec.go new file mode 100644 index 000000000..4895e8994 --- /dev/null +++ b/store/codec.go @@ -0,0 +1,12 @@ +package store + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + types.RegisterBlockAmino(cdc) +} diff --git a/blockchain/store.go b/store/store.go similarity index 94% rename from blockchain/store.go rename to store/store.go index b7f4e07c8..c16d5efec 100644 --- a/blockchain/store.go +++ b/store/store.go @@ -1,11 +1,12 @@ -package blockchain +package store import ( "fmt" "sync" - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" + "github.com/pkg/errors" + + dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/types" ) @@ -67,7 +68,7 @@ func (bs *BlockStore) LoadBlock(height int64) *types.Block { if err != nil { // NOTE: The existence of meta should imply the existence of the // block. So, make sure meta is only saved after blocks are saved. - panic(cmn.ErrorWrap(err, "Error reading block")) + panic(errors.Wrap(err, "Error reading block")) } return block } @@ -83,7 +84,7 @@ func (bs *BlockStore) LoadBlockPart(height int64, index int) *types.Part { } err := cdc.UnmarshalBinaryBare(bz, part) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block part")) + panic(errors.Wrap(err, "Error reading block part")) } return part } @@ -98,7 +99,7 @@ func (bs *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { } err := cdc.UnmarshalBinaryBare(bz, blockMeta) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block meta")) + panic(errors.Wrap(err, "Error reading block meta")) } return blockMeta } @@ -115,7 +116,7 @@ func (bs *BlockStore) LoadBlockCommit(height int64) *types.Commit { } err := cdc.UnmarshalBinaryBare(bz, commit) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block commit")) + panic(errors.Wrap(err, "Error reading block commit")) } return commit } @@ -131,7 +132,7 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit { } err := cdc.UnmarshalBinaryBare(bz, commit) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block seen commit")) + panic(errors.Wrap(err, "Error reading block seen commit")) } return commit } @@ -216,6 +217,7 @@ func calcSeenCommitKey(height int64) []byte { var blockStoreKey = []byte("blockStore") +// BlockStoreStateJSON is the block store state JSON structure. type BlockStoreStateJSON struct { Height int64 `json:"height"` } diff --git a/blockchain/store_test.go b/store/store_test.go similarity index 95% rename from blockchain/store_test.go rename to store/store_test.go index bd30bc6d2..0122a44ac 100644 --- a/blockchain/store_test.go +++ b/store/store_test.go @@ -1,4 +1,4 @@ -package blockchain +package store import ( "bytes" @@ -9,12 +9,13 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + db "github.com/tendermint/tm-db" + dbm "github.com/tendermint/tm-db" + cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/db" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" sm "github.com/tendermint/tendermint/state" @@ -32,6 +33,18 @@ func makeTestCommit(height int64, timestamp time.Time) *types.Commit { return types.NewCommit(types.BlockID{}, commitSigs) } +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 +} + func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) { config := cfg.ResetTestRoot("blockchain_reactor_test") // blockDB := dbm.NewDebugDB("blockDB", dbm.NewMemDB()) @@ -40,7 +53,7 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu stateDB := dbm.NewMemDB() state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + panic(errors.Wrap(err, "error constructing state from genesis file")) } return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } } diff --git a/tools/tm-monitor/Makefile b/tools/tm-monitor/Makefile index 901b0a14d..a71eb2642 100644 --- a/tools/tm-monitor/Makefile +++ b/tools/tm-monitor/Makefile @@ -36,7 +36,7 @@ dist: build-all build-docker: rm -f ./tm-monitor - docker run -it --rm -v "$(PWD)/../../:/go/src/github.com/tendermint/tendermint" -w "/go/src/github.com/tendermint/tendermint/tools/tm-monitor" -e "CGO_ENABLED=0" golang:alpine go build -ldflags "-s -w" -o tm-monitor + docker run -it --rm -v "$(PWD)/../../:/go/src/github.com/tendermint/tendermint" -w "/go/src/github.com/tendermint/tendermint/tools/tm-monitor" -e "GO111MODULE=on" -e "CGO_ENABLED=0" golang:1.12 go build -ldflags "-s -w" -o tm-monitor docker build -t "tendermint/monitor" . clean: diff --git a/tools/tm-monitor/wire.go b/tools/tm-monitor/codec.go similarity index 100% rename from tools/tm-monitor/wire.go rename to tools/tm-monitor/codec.go diff --git a/tools/tm-monitor/mock/eventmeter.go b/tools/tm-monitor/mock/eventmeter.go index 7bbedc7fa..7119c4399 100644 --- a/tools/tm-monitor/mock/eventmeter.go +++ b/tools/tm-monitor/mock/eventmeter.go @@ -54,7 +54,7 @@ func (c *RpcClient) Call(method string, params map[string]interface{}, result in } rv, rt := reflect.ValueOf(result), reflect.TypeOf(result) - rv, rt = rv.Elem(), rt.Elem() + rv, _ = rv.Elem(), rt.Elem() rv.Set(reflect.ValueOf(s)) return s, nil diff --git a/tools/tm-monitor/monitor/wire.go b/tools/tm-monitor/monitor/codec.go similarity index 100% rename from tools/tm-monitor/monitor/wire.go rename to tools/tm-monitor/monitor/codec.go diff --git a/tools/tm-monitor/monitor/network.go b/tools/tm-monitor/monitor/network.go index bb5dd0baa..4d85d7ed6 100644 --- a/tools/tm-monitor/monitor/network.go +++ b/tools/tm-monitor/monitor/network.go @@ -163,11 +163,12 @@ func (n *Network) updateHealth() { // TODO: make sure they're all at the same height (within a block) // and all proposing (and possibly validating ) Alternatively, just // check there hasn't been a new round in numValidators rounds - if n.NumValidators != 0 && n.NumNodesMonitoredOnline == n.NumValidators { + switch { + case n.NumValidators != 0 && n.NumNodesMonitoredOnline == n.NumValidators: n.Health = FullHealth - } else if n.NumNodesMonitoredOnline > 0 && n.NumNodesMonitoredOnline <= n.NumNodesMonitored { + case n.NumNodesMonitoredOnline > 0 && n.NumNodesMonitoredOnline <= n.NumNodesMonitored: n.Health = ModerateHealth - } else { + default: n.Health = Dead } } diff --git a/tools/tm-monitor/rpc.go b/tools/tm-monitor/rpc.go index 4412e6e0b..42cc23075 100644 --- a/tools/tm-monitor/rpc.go +++ b/tools/tm-monitor/rpc.go @@ -5,9 +5,11 @@ import ( "net" "net/http" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/libs/log" rpc "github.com/tendermint/tendermint/rpc/lib/server" - monitor "github.com/tendermint/tendermint/tools/tm-monitor/monitor" + "github.com/tendermint/tendermint/tools/tm-monitor/monitor" ) func startRPC(listenAddr string, m *monitor.Monitor, logger log.Logger) net.Listener { @@ -41,33 +43,33 @@ func routes(m *monitor.Monitor) map[string]*rpc.RPCFunc { } // RPCStatus returns common statistics for the network and statistics per node. -func RPCStatus(m *monitor.Monitor) interface{} { - return func() (networkAndNodes, error) { +func RPCStatus(m *monitor.Monitor) func(*rpctypes.Context) (networkAndNodes, error) { + return func(_ *rpctypes.Context) (networkAndNodes, error) { return networkAndNodes{m.Network, m.Nodes}, nil } } // RPCNetworkStatus returns common statistics for the network. -func RPCNetworkStatus(m *monitor.Monitor) interface{} { - return func() (*monitor.Network, error) { +func RPCNetworkStatus(m *monitor.Monitor) func(*rpctypes.Context) (*monitor.Network, error) { + return func(_ *rpctypes.Context) (*monitor.Network, error) { return m.Network, nil } } // RPCNodeStatus returns statistics for the given node. -func RPCNodeStatus(m *monitor.Monitor) interface{} { - return func(name string) (*monitor.Node, error) { +func RPCNodeStatus(m *monitor.Monitor) func(*rpctypes.Context, string) (*monitor.Node, error) { + return func(_ *rpctypes.Context, name string) (*monitor.Node, error) { if i, n := m.NodeByName(name); i != -1 { return n, nil } - return nil, errors.New("Cannot find node with that name") + return nil, errors.New("cannot find node with that name") } } // RPCMonitor allows to dynamically add a endpoint to under the monitor. Safe // to call multiple times. -func RPCMonitor(m *monitor.Monitor) interface{} { - return func(endpoint string) (*monitor.Node, error) { +func RPCMonitor(m *monitor.Monitor) func(*rpctypes.Context, string) (*monitor.Node, error) { + return func(_ *rpctypes.Context, endpoint string) (*monitor.Node, error) { i, n := m.NodeByName(endpoint) if i == -1 { n = monitor.NewNode(endpoint) @@ -80,13 +82,13 @@ func RPCMonitor(m *monitor.Monitor) interface{} { } // RPCUnmonitor removes the given endpoint from under the monitor. -func RPCUnmonitor(m *monitor.Monitor) interface{} { - return func(endpoint string) (bool, error) { +func RPCUnmonitor(m *monitor.Monitor) func(*rpctypes.Context, string) (bool, error) { + return func(_ *rpctypes.Context, endpoint string) (bool, error) { if i, n := m.NodeByName(endpoint); i != -1 { m.Unmonitor(n) return true, nil } - return false, errors.New("Cannot find node with that name") + return false, errors.New("cannot find node with that name") } } diff --git a/tools/tm-signer-harness/internal/test_harness.go b/tools/tm-signer-harness/internal/test_harness.go index 7fefdfb42..216cf6851 100644 --- a/tools/tm-signer-harness/internal/test_harness.go +++ b/tools/tm-signer-harness/internal/test_harness.go @@ -49,7 +49,7 @@ var _ error = (*TestHarnessError)(nil) // with this version of Tendermint. type TestHarness struct { addr string - spv *privval.SignerValidatorEndpoint + signerClient *privval.SignerClient fpv *privval.FilePV chainID string acceptRetries int @@ -101,14 +101,19 @@ func NewTestHarness(logger log.Logger, cfg TestHarnessConfig) (*TestHarness, err } logger.Info("Loaded genesis file", "chainID", st.ChainID) - spv, err := newTestHarnessSocketVal(logger, cfg) + spv, err := newTestHarnessListener(logger, cfg) + if err != nil { + return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") + } + + signerClient, err := privval.NewSignerClient(spv) if err != nil { return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") } return &TestHarness{ addr: cfg.BindAddr, - spv: spv, + signerClient: signerClient, fpv: fpv, chainID: st.ChainID, acceptRetries: cfg.AcceptRetries, @@ -135,9 +140,11 @@ func (th *TestHarness) Run() { th.logger.Info("Starting test harness") accepted := false var startErr error + for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- { th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries) - if err := th.spv.Start(); err != nil { + + if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil { // if it wasn't a timeout error if _, ok := err.(timeoutError); !ok { th.logger.Error("Failed to start listener", "err", err) @@ -149,6 +156,7 @@ func (th *TestHarness) Run() { } startErr = err } else { + th.logger.Info("Accepted external connection") accepted = true break } @@ -182,8 +190,8 @@ func (th *TestHarness) Run() { func (th *TestHarness) TestPublicKey() error { th.logger.Info("TEST: Public key of remote signer") th.logger.Info("Local", "pubKey", th.fpv.GetPubKey()) - th.logger.Info("Remote", "pubKey", th.spv.GetPubKey()) - if th.fpv.GetPubKey() != th.spv.GetPubKey() { + th.logger.Info("Remote", "pubKey", th.signerClient.GetPubKey()) + if th.fpv.GetPubKey() != th.signerClient.GetPubKey() { th.logger.Error("FAILED: Local and remote public keys do not match") return newTestHarnessError(ErrTestPublicKeyFailed, nil, "") } @@ -211,7 +219,7 @@ func (th *TestHarness) TestSignProposal() error { Timestamp: time.Now(), } propBytes := prop.SignBytes(th.chainID) - if err := th.spv.SignProposal(th.chainID, prop); err != nil { + if err := th.signerClient.SignProposal(th.chainID, prop); err != nil { th.logger.Error("FAILED: Signing of proposal", "err", err) return newTestHarnessError(ErrTestSignProposalFailed, err, "") } @@ -222,7 +230,7 @@ func (th *TestHarness) TestSignProposal() error { return newTestHarnessError(ErrTestSignProposalFailed, err, "") } // now validate the signature on the proposal - if th.spv.GetPubKey().VerifyBytes(propBytes, prop.Signature) { + if th.signerClient.GetPubKey().VerifyBytes(propBytes, prop.Signature) { th.logger.Info("Successfully validated proposal signature") } else { th.logger.Error("FAILED: Proposal signature validation failed") @@ -255,7 +263,7 @@ func (th *TestHarness) TestSignVote() error { } voteBytes := vote.SignBytes(th.chainID) // sign the vote - if err := th.spv.SignVote(th.chainID, vote); err != nil { + if err := th.signerClient.SignVote(th.chainID, vote); err != nil { th.logger.Error("FAILED: Signing of vote", "err", err) return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) } @@ -266,7 +274,7 @@ func (th *TestHarness) TestSignVote() error { return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) } // now validate the signature on the proposal - if th.spv.GetPubKey().VerifyBytes(voteBytes, vote.Signature) { + if th.signerClient.GetPubKey().VerifyBytes(voteBytes, vote.Signature) { th.logger.Info("Successfully validated vote signature", "type", voteType) } else { th.logger.Error("FAILED: Vote signature validation failed", "type", voteType) @@ -301,10 +309,9 @@ func (th *TestHarness) Shutdown(err error) { }() } - if th.spv.IsRunning() { - if err := th.spv.Stop(); err != nil { - th.logger.Error("Failed to cleanly stop listener: %s", err.Error()) - } + err = th.signerClient.Close() + if err != nil { + th.logger.Error("Failed to cleanly stop listener: %s", err.Error()) } if th.exitWhenComplete { @@ -312,9 +319,8 @@ func (th *TestHarness) Shutdown(err error) { } } -// newTestHarnessSocketVal creates our client instance which we will use for -// testing. -func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerValidatorEndpoint, error) { +// newTestHarnessListener creates our client instance which we will use for testing. +func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) { proto, addr := cmn.ProtocolAndAddress(cfg.BindAddr) if proto == "unix" { // make sure the socket doesn't exist - if so, try to delete it @@ -329,7 +335,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval if err != nil { return nil, err } - logger.Info("Listening at", "proto", proto, "addr", addr) + logger.Info("Listening", "proto", proto, "addr", addr) var svln net.Listener switch proto { case "unix": @@ -347,7 +353,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto) return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto)) } - return privval.NewSignerValidatorEndpoint(logger, svln), nil + return privval.NewSignerListenerEndpoint(logger, svln), nil } func newTestHarnessError(code int, err error, info string) *TestHarnessError { diff --git a/tools/tm-signer-harness/internal/test_harness_test.go b/tools/tm-signer-harness/internal/test_harness_test.go index c249bd2b6..47e510666 100644 --- a/tools/tm-signer-harness/internal/test_harness_test.go +++ b/tools/tm-signer-harness/internal/test_harness_test.go @@ -3,19 +3,18 @@ package internal import ( "fmt" "io/ioutil" - "net" "os" "testing" "time" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/types" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/types" ) const ( @@ -85,8 +84,8 @@ func TestRemoteSignerTestHarnessMaxAcceptRetriesReached(t *testing.T) { func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, false) }, NoError, ) @@ -95,8 +94,8 @@ func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) { func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, ed25519.GenPrivKey(), false, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, ed25519.GenPrivKey(), false, false) }, ErrTestPublicKeyFailed, ) @@ -105,8 +104,8 @@ func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) { func TestRemoteSignerProposalSigningFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, true, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, true, false) }, ErrTestSignProposalFailed, ) @@ -115,28 +114,30 @@ func TestRemoteSignerProposalSigningFailed(t *testing.T) { func TestRemoteSignerVoteSigningFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, true) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, true) }, ErrTestSignVoteFailed, ) } -func newMockRemoteSigner(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServiceEndpoint { - return privval.NewSignerServiceEndpoint( +func newMockSignerServer(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServer { + mockPV := types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning) + + dialerEndpoint := privval.NewSignerDialerEndpoint( th.logger, - th.chainID, - types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning), privval.DialTCPFn( th.addr, time.Duration(defaultConnDeadline)*time.Millisecond, ed25519.GenPrivKey(), ), ) + + return privval.NewSignerServer(dialerEndpoint, th.chainID, mockPV) } // For running relatively standard tests. -func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServiceEndpoint, expectedExitCode int) { +func harnessTest(t *testing.T, signerServerMaker func(th *TestHarness) *privval.SignerServer, expectedExitCode int) { cfg := makeConfig(t, 100, 3) defer cleanup(cfg) @@ -148,10 +149,10 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ th.Run() }() - rs := rsMaker(th) - require.NoError(t, rs.Start()) - assert.True(t, rs.IsRunning()) - defer rs.Stop() + ss := signerServerMaker(th) + require.NoError(t, ss.Start()) + assert.True(t, ss.IsRunning()) + defer ss.Stop() <-donec assert.Equal(t, expectedExitCode, th.exitCode) @@ -159,7 +160,7 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ func makeConfig(t *testing.T, acceptDeadline, acceptRetries int) TestHarnessConfig { return TestHarnessConfig{ - BindAddr: testFreeTCPAddr(t), + BindAddr: privval.GetFreeLocalhostAddrPort(), KeyFile: makeTempFile("tm-testharness-keyfile", keyFileContents), StateFile: makeTempFile("tm-testharness-statefile", stateFileContents), GenesisFile: makeTempFile("tm-testharness-genesisfile", genesisFileContents), @@ -191,12 +192,3 @@ func makeTempFile(name, content string) string { } return tempFile.Name() } - -// testFreeTCPAddr claims a free port so we don't block on listener being ready. -func testFreeTCPAddr(t *testing.T) string { - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer ln.Close() - - return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) -} diff --git a/types/block.go b/types/block.go index 8a8989ab6..6a8fb2a47 100644 --- a/types/block.go +++ b/types/block.go @@ -41,25 +41,6 @@ type Block struct { LastCommit *Commit `json:"last_commit"` } -// MakeBlock returns a new block with an empty header, except what can be -// computed from itself. -// It populates the same set of fields validated by ValidateBasic. -func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block { - block := &Block{ - Header: Header{ - Height: height, - NumTxs: int64(len(txs)), - }, - Data: Data{ - Txs: txs, - }, - Evidence: EvidenceData{Evidence: evidence}, - LastCommit: lastCommit, - } - block.fillHeader() - return block -} - // ValidateBasic performs basic validation that doesn't involve state data. // It checks the internal consistency of the block. // Further validation is done using state#ValidateBlock. @@ -800,7 +781,7 @@ func (sh SignedHeader) ValidateBasic(chainID string) error { // ValidateBasic on the Commit. err := sh.Commit.ValidateBasic() if err != nil { - return cmn.ErrorWrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") + return errors.Wrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") } return nil } diff --git a/types/block_test.go b/types/block_test.go index ff7edd27a..716229bbc 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -366,3 +366,91 @@ func TestCommitToVoteSet(t *testing.T) { assert.Equal(t, vote1bz, vote3bz) } } + +func TestSignedHeaderValidateBasic(t *testing.T) { + commit := randCommit() + chainID := "𠜎" + timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) + h := Header{ + Version: version.Consensus{Block: math.MaxInt64, App: math.MaxInt64}, + ChainID: chainID, + Height: commit.Height(), + Time: timestamp, + NumTxs: math.MaxInt64, + TotalTxs: math.MaxInt64, + LastBlockID: commit.BlockID, + LastCommitHash: commit.Hash(), + DataHash: commit.Hash(), + ValidatorsHash: commit.Hash(), + NextValidatorsHash: commit.Hash(), + ConsensusHash: commit.Hash(), + AppHash: commit.Hash(), + LastResultsHash: commit.Hash(), + EvidenceHash: commit.Hash(), + ProposerAddress: crypto.AddressHash([]byte("proposer_address")), + } + + validSignedHeader := SignedHeader{Header: &h, Commit: commit} + validSignedHeader.Commit.BlockID.Hash = validSignedHeader.Hash() + invalidSignedHeader := SignedHeader{} + + testCases := []struct { + testName string + shHeader *Header + shCommit *Commit + expectErr bool + }{ + {"Valid Signed Header", validSignedHeader.Header, validSignedHeader.Commit, false}, + {"Invalid Signed Header", invalidSignedHeader.Header, validSignedHeader.Commit, true}, + {"Invalid Signed Header", validSignedHeader.Header, invalidSignedHeader.Commit, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + sh := SignedHeader{ + Header: tc.shHeader, + Commit: tc.shCommit, + } + assert.Equal(t, tc.expectErr, sh.ValidateBasic(validSignedHeader.Header.ChainID) != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBlockIDValidateBasic(t *testing.T) { + validBlockID := BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: PartSetHeader{ + Total: 1, + Hash: cmn.HexBytes{}, + }, + } + + invalidBlockID := BlockID{ + Hash: []byte{0}, + PartsHeader: PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + + testCases := []struct { + testName string + blockIDHash cmn.HexBytes + blockIDPartsHeader PartSetHeader + expectErr bool + }{ + {"Valid BlockID", validBlockID.Hash, validBlockID.PartsHeader, false}, + {"Invalid BlockID", invalidBlockID.Hash, validBlockID.PartsHeader, true}, + {"Invalid BlockID", validBlockID.Hash, invalidBlockID.PartsHeader, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + blockID := BlockID{ + Hash: tc.blockIDHash, + PartsHeader: tc.blockIDPartsHeader, + } + assert.Equal(t, tc.expectErr, blockID.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} diff --git a/types/wire.go b/types/codec.go similarity index 100% rename from types/wire.go rename to types/codec.go diff --git a/types/evidence_test.go b/types/evidence_test.go index 1f1338cad..fc97ae409 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -157,3 +157,13 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { }) } } + +func TestMockGoodEvidenceValidateBasic(t *testing.T) { + goodEvidence := NewMockGoodEvidence(int64(1), 1, []byte{1}) + assert.Nil(t, goodEvidence.ValidateBasic()) +} + +func TestMockBadEvidenceValidateBasic(t *testing.T) { + badEvidence := MockBadEvidence{MockGoodEvidence: NewMockGoodEvidence(int64(1), 1, []byte{1})} + assert.Nil(t, badEvidence.ValidateBasic()) +} diff --git a/types/genesis.go b/types/genesis.go index 54b81e9e2..94680bca8 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" tmtime "github.com/tendermint/tendermint/types/time" @@ -64,26 +66,24 @@ func (genDoc *GenesisDoc) ValidatorHash() []byte { // and fills in defaults for optional fields left empty func (genDoc *GenesisDoc) ValidateAndComplete() error { if genDoc.ChainID == "" { - return cmn.NewError("Genesis doc must include non-empty chain_id") + return errors.New("Genesis doc must include non-empty chain_id") } if len(genDoc.ChainID) > MaxChainIDLen { - return cmn.NewError("chain_id in genesis doc is too long (max: %d)", MaxChainIDLen) + return errors.Errorf("chain_id in genesis doc is too long (max: %d)", MaxChainIDLen) } if genDoc.ConsensusParams == nil { genDoc.ConsensusParams = DefaultConsensusParams() - } else { - if err := genDoc.ConsensusParams.Validate(); err != nil { - return err - } + } else if err := genDoc.ConsensusParams.Validate(); err != nil { + return err } for i, v := range genDoc.Validators { if v.Power == 0 { - return cmn.NewError("The genesis file cannot contain validators with no voting power: %v", v) + return errors.Errorf("The genesis file cannot contain validators with no voting power: %v", v) } if len(v.Address) > 0 && !bytes.Equal(v.PubKey.Address(), v.Address) { - return cmn.NewError("Incorrect address for validator %v in the genesis file, should be %v", v, v.PubKey.Address()) + return errors.Errorf("Incorrect address for validator %v in the genesis file, should be %v", v, v.PubKey.Address()) } if len(v.Address) == 0 { genDoc.Validators[i].Address = v.PubKey.Address() @@ -119,11 +119,11 @@ func GenesisDocFromJSON(jsonBlob []byte) (*GenesisDoc, error) { func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { jsonBlob, err := ioutil.ReadFile(genDocFile) if err != nil { - return nil, cmn.ErrorWrap(err, "Couldn't read GenesisDoc file") + return nil, errors.Wrap(err, "Couldn't read GenesisDoc file") } genDoc, err := GenesisDocFromJSON(jsonBlob) if err != nil { - return nil, cmn.ErrorWrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) + return nil, errors.Wrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) } return genDoc, nil } diff --git a/types/genesis_test.go b/types/genesis_test.go index f977513e7..33bdd34c1 100644 --- a/types/genesis_test.go +++ b/types/genesis_test.go @@ -68,7 +68,7 @@ func TestGenesisGood(t *testing.T) { genDoc.ConsensusParams.Block.MaxBytes = 0 genDocBytes, err = cdc.MarshalJSON(genDoc) assert.NoError(t, err, "error marshalling genDoc") - genDoc, err = GenesisDocFromJSON(genDocBytes) + _, err = GenesisDocFromJSON(genDocBytes) assert.Error(t, err, "expected error for genDoc json with block size of 0") // Genesis doc from raw json diff --git a/types/params.go b/types/params.go index 162aaeada..c9ab4aaf7 100644 --- a/types/params.go +++ b/types/params.go @@ -1,6 +1,8 @@ package types import ( + "github.com/pkg/errors" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/tmhash" cmn "github.com/tendermint/tendermint/libs/common" @@ -95,38 +97,38 @@ func (params *ValidatorParams) IsValidPubkeyType(pubkeyType string) bool { // allowed limits, and returns an error if they are not. func (params *ConsensusParams) Validate() error { if params.Block.MaxBytes <= 0 { - return cmn.NewError("Block.MaxBytes must be greater than 0. Got %d", + return errors.Errorf("Block.MaxBytes must be greater than 0. Got %d", params.Block.MaxBytes) } if params.Block.MaxBytes > MaxBlockSizeBytes { - return cmn.NewError("Block.MaxBytes is too big. %d > %d", + return errors.Errorf("Block.MaxBytes is too big. %d > %d", params.Block.MaxBytes, MaxBlockSizeBytes) } if params.Block.MaxGas < -1 { - return cmn.NewError("Block.MaxGas must be greater or equal to -1. Got %d", + return errors.Errorf("Block.MaxGas must be greater or equal to -1. Got %d", params.Block.MaxGas) } if params.Block.TimeIotaMs <= 0 { - return cmn.NewError("Block.TimeIotaMs must be greater than 0. Got %v", + return errors.Errorf("Block.TimeIotaMs must be greater than 0. Got %v", params.Block.TimeIotaMs) } if params.Evidence.MaxAge <= 0 { - return cmn.NewError("EvidenceParams.MaxAge must be greater than 0. Got %d", + return errors.Errorf("EvidenceParams.MaxAge must be greater than 0. Got %d", params.Evidence.MaxAge) } if len(params.Validator.PubKeyTypes) == 0 { - return cmn.NewError("len(Validator.PubKeyTypes) must be greater than 0") + return errors.New("len(Validator.PubKeyTypes) must be greater than 0") } // Check if keyType is a known ABCIPubKeyType for i := 0; i < len(params.Validator.PubKeyTypes); i++ { keyType := params.Validator.PubKeyTypes[i] if _, ok := ABCIPubKeyTypesToAminoNames[keyType]; !ok { - return cmn.NewError("params.Validator.PubKeyTypes[%d], %s, is an unknown pubkey type", + return errors.Errorf("params.Validator.PubKeyTypes[%d], %s, is an unknown pubkey type", i, keyType) } } diff --git a/types/priv_validator.go b/types/priv_validator.go index 8acab243a..45d0a67b5 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -12,6 +12,7 @@ import ( // PrivValidator defines the functionality of a local Tendermint validator // that signs votes and proposals, and never double signs. type PrivValidator interface { + // TODO: Extend the interface to return errors too. Issue: https://github.com/tendermint/tendermint/issues/3602 GetPubKey() crypto.PubKey SignVote(chainID string, vote *Vote) error diff --git a/types/test_util.go b/types/test_util.go index 18e472148..d226fd99e 100644 --- a/types/test_util.go +++ b/types/test_util.go @@ -5,8 +5,7 @@ import ( ) func MakeCommit(blockID BlockID, height int64, round int, - voteSet *VoteSet, - validators []PrivValidator) (*Commit, error) { + voteSet *VoteSet, validators []PrivValidator) (*Commit, error) { // all sign for i := 0; i < len(validators); i++ { @@ -37,3 +36,40 @@ func signAddVote(privVal PrivValidator, vote *Vote, voteSet *VoteSet) (signed bo } return voteSet.AddVote(vote) } + +func MakeVote(height int64, blockID BlockID, valSet *ValidatorSet, privVal PrivValidator, chainID string) (*Vote, error) { + addr := privVal.GetPubKey().Address() + idx, _ := valSet.GetByAddress(addr) + vote := &Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: PrecommitType, + BlockID: blockID, + } + if err := privVal.SignVote(chainID, vote); err != nil { + return nil, err + } + return vote, nil +} + +// MakeBlock returns a new block with an empty header, except what can be +// computed from itself. +// It populates the same set of fields validated by ValidateBasic. +func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block { + block := &Block{ + Header: Header{ + Height: height, + NumTxs: int64(len(txs)), + }, + Data: Data{ + Txs: txs, + }, + Evidence: EvidenceData{Evidence: evidence}, + LastCommit: lastCommit, + } + block.fillHeader() + return block +} diff --git a/types/validator.go b/types/validator.go index a662eb6c0..20069ff9a 100644 --- a/types/validator.go +++ b/types/validator.go @@ -41,17 +41,19 @@ func (v *Validator) CompareProposerPriority(other *Validator) *Validator { if v == nil { return other } - if v.ProposerPriority > other.ProposerPriority { + switch { + case v.ProposerPriority > other.ProposerPriority: return v - } else if v.ProposerPriority < other.ProposerPriority { + case v.ProposerPriority < other.ProposerPriority: return other - } else { + default: result := bytes.Compare(v.Address, other.Address) - if result < 0 { + switch { + case result < 0: return v - } else if result > 0 { + case result > 0: return other - } else { + default: panic("Cannot compare identical validators") } } diff --git a/types/validator_set.go b/types/validator_set.go index f24a12fbe..7e2305a71 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -2,15 +2,15 @@ package types import ( "bytes" - "errors" "fmt" "math" "math/big" "sort" "strings" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto/merkle" - cmn "github.com/tendermint/tendermint/libs/common" ) // MaxTotalVotingPower - the maximum allowed total voting power. @@ -121,7 +121,7 @@ func (vals *ValidatorSet) RescalePriorities(diffMax int64) { ratio := (diff + diffMax - 1) / diffMax if diff > diffMax { for _, val := range vals.Validators { - val.ProposerPriority = val.ProposerPriority / ratio + val.ProposerPriority /= ratio } } } @@ -525,7 +525,7 @@ func (vals *ValidatorSet) applyRemovals(deletes []*Validator) { // The 'allowDeletes' flag is set to false by NewValidatorSet() and to true by UpdateWithChangeSet(). func (vals *ValidatorSet) updateWithChangeSet(changes []*Validator, allowDeletes bool) error { - if len(changes) <= 0 { + if len(changes) == 0 { return nil } @@ -626,9 +626,10 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i if blockID.Equals(precommit.BlockID) { talliedVotingPower += val.VotingPower } - + // else { // It's OK that the BlockID doesn't match. We include stray // precommits to measure validator availability. + // } } if talliedVotingPower > vals.TotalVotingPower()*2/3 { @@ -642,15 +643,8 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i // IsErrTooMuchChange func IsErrTooMuchChange(err error) bool { - switch err_ := err.(type) { - case cmn.Error: - _, ok := err_.Data().(errTooMuchChange) - return ok - case errTooMuchChange: - return true - default: - return false - } + _, ok := errors.Cause(err).(errTooMuchChange) + return ok } type errTooMuchChange struct { diff --git a/types/vote_set_test.go b/types/vote_set_test.go index 59205efc6..2e217e940 100644 --- a/types/vote_set_test.go +++ b/types/vote_set_test.go @@ -4,9 +4,10 @@ import ( "bytes" "testing" + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" - tst "github.com/tendermint/tendermint/libs/test" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -490,7 +491,7 @@ func TestMakeCommit(t *testing.T) { } // MakeCommit should fail. - tst.AssertPanics(t, "Doesn't have +2/3 majority", func() { voteSet.MakeCommit() }) + assert.Panics(t, func() { voteSet.MakeCommit() }, "Doesn't have +2/3 majority") // 7th voted for some other block. { diff --git a/types/vote_test.go b/types/vote_test.go index af8a9625b..b6eb1f586 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + amino "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" diff --git a/version/version.go b/version/version.go index 2d1c41fd5..9fb7c7869 100644 --- a/version/version.go +++ b/version/version.go @@ -20,10 +20,10 @@ 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.32.0" + TMCoreSemVer = "0.32.2" // ABCISemVer is the semantic version of the ABCI library - ABCISemVer = "0.16.0" + ABCISemVer = "0.16.1" ABCIVersion = ABCISemVer )