diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..902469b41 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ + + +* [ ] Updated all relevant documentation in docs +* [ ] Updated all code comments where relevant +* [ ] Wrote tests +* [ ] Updated CHANGELOG.md diff --git a/.gitignore b/.gitignore index 22a6be0b9..b031ce185 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ test/logs coverage.txt docs/_build docs/tools +*.log scripts/wal2json/wal2json scripts/cutWALUntil/cutWALUntil diff --git a/CHANGELOG.md b/CHANGELOG.md index cda150717..3456fb58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,40 @@ BUG FIXES: - Graceful handling/recovery for apps that have non-determinism or fail to halt - Graceful handling/recovery for violations of safety, or liveness +## 0.16.0 (February 20th, 2017) + +BREAKING CHANGES: +- [config] use $TMHOME/config for all config and json files +- [p2p] old `--p2p.seeds` is now `--p2p.persistent_peers` (persistent peers to which TM will always connect to) +- [p2p] now `--p2p.seeds` only used for getting addresses (if addrbook is empty; not persistent) +- [p2p] NodeInfo: remove RemoteAddr and add Channels + - we must have at least one overlapping channel with peer + - we only send msgs for channels the peer advertised +- [p2p/conn] pong timeout +- [lite] comment out IAVL related code + +FEATURES: +- [p2p] added new `/dial_peers&persistent=_` **unsafe** endpoint +- [p2p] persistent node key in `$THMHOME/config/node_key.json` +- [p2p] introduce peer ID and authenticate peers by ID using addresses like `ID@IP:PORT` +- [p2p/pex] new seed mode crawls the network and serves as a seed. +- [config] MempoolConfig.CacheSize +- [config] P2P.SeedMode (`--p2p.seed_mode`) + +IMPROVEMENT: +- [p2p/pex] stricter rules in the PEX reactor for better handling of abuse +- [p2p] various improvements to code structure including subpackages for `pex` and `conn` +- [docs] new spec! +- [all] speed up the tests! + +BUG FIX: +- [blockchain] StopPeerForError on timeout +- [consensus] StopPeerForError on a bad Maj23 message +- [state] flush mempool conn before calling commit +- [types] fix priv val signing things that only differ by timestamp +- [mempool] fix memory leak causing zombie peers +- [p2p/conn] fix potential deadlock + ## 0.15.0 (December 29, 2017) BREAKING CHANGES: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 787fd7180..b991bcc4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,15 +42,18 @@ Run `bash scripts/glide/status.sh` to get a list of vendored dependencies that m ## Vagrant -If you are a [Vagrant](https://www.vagrantup.com/) user, all you have to do to get started hacking Tendermint is: +If you are a [Vagrant](https://www.vagrantup.com/) user, you can get started hacking Tendermint with the commands below. + +NOTE: In case you installed Vagrant in 2017, you might need to run +`vagrant box update` to upgrade to the latest `ubuntu/xenial64`. ``` vagrant up vagrant ssh -cd ~/go/src/github.com/tendermint/tendermint make test ``` + ## Testing All repos should be hooked up to circle. @@ -97,4 +100,4 @@ especially `go-p2p` and `go-rpc`, as their versions are referenced in tendermint - push to hotfix-vX.X.X to run the extended integration tests on the CI - merge hotfix-vX.X.X to master - merge hotfix-vX.X.X to develop -- delete the hotfix-vX.X.X branch \ No newline at end of file +- delete the hotfix-vX.X.X branch diff --git a/DOCKER/Dockerfile b/DOCKER/Dockerfile index c0d09d951..c00318fba 100644 --- a/DOCKER/Dockerfile +++ b/DOCKER/Dockerfile @@ -1,8 +1,8 @@ FROM alpine:3.6 # This is the release of tendermint to pull in. -ENV TM_VERSION 0.13.0 -ENV TM_SHA256SUM 36d773d4c2890addc61cc87a72c1e9c21c89516921b0defb0edfebde719b4b85 +ENV TM_VERSION 0.15.0 +ENV TM_SHA256SUM 71cc271c67eca506ca492c8b90b090132f104bf5dbfe0af2702a50886e88de17 # Tendermint will be looking for genesis file in /tendermint (unless you change # `genesis_file` in config.toml). You can put your config.toml and private diff --git a/DOCKER/README.md b/DOCKER/README.md index fceab5feb..06f400eae 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -1,6 +1,7 @@ # Supported tags and respective `Dockerfile` links -- `0.13.0`, `latest` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/a28b3fff49dce2fb31f90abb2fc693834e0029c2/DOCKER/Dockerfile) +- `0.15.0`, `latest` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/170777300ea92dc21a8aec1abc16cb51812513a4/DOCKER/Dockerfile) +- `0.13.0` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/a28b3fff49dce2fb31f90abb2fc693834e0029c2/DOCKER/Dockerfile) - `0.12.1` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/457c688346b565e90735431619ca3ca597ef9007/DOCKER/Dockerfile) - `0.12.0` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/70d8afa6e952e24c573ece345560a5971bf2cc0e/DOCKER/Dockerfile) - `0.11.0` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/9177cc1f64ca88a4a0243c5d1773d10fba67e201/DOCKER/Dockerfile) diff --git a/Makefile b/Makefile index 2aed1acf4..c765d078f 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,11 @@ GOTOOLS = \ - github.com/mitchellh/gox \ - github.com/Masterminds/glide \ - github.com/tcnksm/ghr \ - gopkg.in/alecthomas/gometalinter.v2 -GOTOOLS_CHECK = gox glide ghr gometalinter.v2 + github.com/tendermint/glide \ + # gopkg.in/alecthomas/gometalinter.v2 PACKAGES=$(shell go list ./... | grep -v '/vendor/') BUILD_TAGS?=tendermint -TMHOME = $${TMHOME:-$$HOME/.tendermint} -BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short HEAD`" +BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" -all: check build test install metalinter +all: check build test install check: check_tools get_vendor_deps @@ -18,31 +14,33 @@ check: check_tools get_vendor_deps ### Build build: - go build $(BUILD_FLAGS) -o build/tendermint ./cmd/tendermint/ + go build $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o build/tendermint ./cmd/tendermint/ build_race: - go build -race $(BUILD_FLAGS) -o build/tendermint ./cmd/tendermint + go build -race $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' -o build/tendermint ./cmd/tendermint + +install: + go install $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' ./cmd/tendermint + +######################################## +### Distribution # dist builds binaries for all platforms and packages them for distribution dist: @BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/dist.sh'" -install: - go install $(BUILD_FLAGS) ./cmd/tendermint - - ######################################## ### Tools & dependencies check_tools: @# https://stackoverflow.com/a/25668869 - @echo "Found tools: $(foreach tool,$(GOTOOLS_CHECK),\ + @echo "Found tools: $(foreach tool,$(notdir $(GOTOOLS)),\ $(if $(shell which $(tool)),$(tool),$(error "No $(tool) in PATH")))" get_tools: @echo "--> Installing tools" go get -u -v $(GOTOOLS) - @gometalinter.v2 --install + # @gometalinter.v2 --install update_tools: @echo "--> Updating tools" diff --git a/Vagrantfile b/Vagrantfile index 80d44f9c7..ac8da0cc1 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -21,29 +21,32 @@ Vagrant.configure("2") do |config| # install base requirements apt-get update - apt-get install -y --no-install-recommends wget curl jq \ + apt-get install -y --no-install-recommends wget curl jq zip \ make shellcheck bsdmainutils psmisc apt-get install -y docker-ce golang-1.9-go + apt-get install -y language-pack-en + + # cleanup + apt-get autoremove -y # needed for docker - usermod -a -G docker ubuntu + usermod -a -G docker vagrant - # use "EOF" not EOF to avoid variable substitution of $PATH - cat << "EOF" >> /home/ubuntu/.bash_profile -export PATH=$PATH:/usr/lib/go-1.9/bin:/home/ubuntu/go/bin -export GOPATH=/home/ubuntu/go -export LC_ALL=en_US.UTF-8 -cd go/src/github.com/tendermint/tendermint -EOF + # set env variables + echo 'export PATH=$PATH:/usr/lib/go-1.9/bin:/home/vagrant/go/bin' >> /home/vagrant/.bash_profile + echo 'export GOPATH=/home/vagrant/go' >> /home/vagrant/.bash_profile + echo 'export LC_ALL=en_US.UTF-8' >> /home/vagrant/.bash_profile + echo 'cd go/src/github.com/tendermint/tendermint' >> /home/vagrant/.bash_profile - mkdir -p /home/ubuntu/go/bin - mkdir -p /home/ubuntu/go/src/github.com/tendermint - ln -s /vagrant /home/ubuntu/go/src/github.com/tendermint/tendermint + mkdir -p /home/vagrant/go/bin + mkdir -p /home/vagrant/go/src/github.com/tendermint + ln -s /vagrant /home/vagrant/go/src/github.com/tendermint/tendermint - chown -R ubuntu:ubuntu /home/ubuntu/go - chown ubuntu:ubuntu /home/ubuntu/.bash_profile + chown -R vagrant:vagrant /home/vagrant/go + chown vagrant:vagrant /home/vagrant/.bash_profile # get all deps and tools, ready to install/test - su - ubuntu -c 'cd /home/ubuntu/go/src/github.com/tendermint/tendermint && make get_vendor_deps && make tools' + su - vagrant -c 'source /home/vagrant/.bash_profile' + su - vagrant -c 'cd /home/vagrant/go/src/github.com/tendermint/tendermint && make get_tools && make get_vendor_deps' SHELL end diff --git a/benchmarks/blockchain/localsync.sh b/benchmarks/blockchain/localsync.sh index e181c5655..389464b6c 100755 --- a/benchmarks/blockchain/localsync.sh +++ b/benchmarks/blockchain/localsync.sh @@ -51,7 +51,7 @@ tendermint node \ --proxy_app dummy \ --p2p.laddr tcp://127.0.0.1:56666 \ --rpc.laddr tcp://127.0.0.1:56667 \ - --p2p.seeds 127.0.0.1:56656 \ + --p2p.persistent_peers 127.0.0.1:56656 \ --log_level error & # wait for node to start up so we only count time where we are actually syncing diff --git a/benchmarks/codec_test.go b/benchmarks/codec_test.go index 631b303eb..8ac62a247 100644 --- a/benchmarks/codec_test.go +++ b/benchmarks/codec_test.go @@ -16,11 +16,10 @@ func BenchmarkEncodeStatusWire(b *testing.B) { b.StopTimer() pubKey := crypto.GenPrivKeyEd25519().PubKey() status := &ctypes.ResultStatus{ - NodeInfo: &p2p.NodeInfo{ - PubKey: pubKey.Unwrap().(crypto.PubKeyEd25519), + NodeInfo: p2p.NodeInfo{ + PubKey: pubKey, Moniker: "SOMENAME", Network: "SOMENAME", - RemoteAddr: "SOMEADDR", ListenAddr: "SOMEADDR", Version: "SOMEVER", Other: []string{"SOMESTRING", "OTHERSTRING"}, @@ -42,12 +41,11 @@ func BenchmarkEncodeStatusWire(b *testing.B) { func BenchmarkEncodeNodeInfoWire(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) - nodeInfo := &p2p.NodeInfo{ + pubKey := crypto.GenPrivKeyEd25519().PubKey() + nodeInfo := p2p.NodeInfo{ PubKey: pubKey, Moniker: "SOMENAME", Network: "SOMENAME", - RemoteAddr: "SOMEADDR", ListenAddr: "SOMEADDR", Version: "SOMEVER", Other: []string{"SOMESTRING", "OTHERSTRING"}, @@ -63,12 +61,11 @@ func BenchmarkEncodeNodeInfoWire(b *testing.B) { func BenchmarkEncodeNodeInfoBinary(b *testing.B) { b.StopTimer() - pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) - nodeInfo := &p2p.NodeInfo{ + pubKey := crypto.GenPrivKeyEd25519().PubKey() + nodeInfo := p2p.NodeInfo{ PubKey: pubKey, Moniker: "SOMENAME", Network: "SOMENAME", - RemoteAddr: "SOMEADDR", ListenAddr: "SOMEADDR", Version: "SOMEVER", Other: []string{"SOMESTRING", "OTHERSTRING"}, @@ -87,11 +84,10 @@ func BenchmarkEncodeNodeInfoProto(b *testing.B) { b.StopTimer() pubKey := crypto.GenPrivKeyEd25519().PubKey().Unwrap().(crypto.PubKeyEd25519) pubKey2 := &proto.PubKey{Ed25519: &proto.PubKeyEd25519{Bytes: pubKey[:]}} - nodeInfo := &proto.NodeInfo{ + nodeInfo := proto.NodeInfo{ PubKey: pubKey2, Moniker: "SOMENAME", Network: "SOMENAME", - RemoteAddr: "SOMEADDR", ListenAddr: "SOMEADDR", Version: "SOMEVER", Other: []string{"SOMESTRING", "OTHERSTRING"}, diff --git a/blockchain/pool.go b/blockchain/pool.go index e39749dc0..d0f4d2976 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -1,18 +1,20 @@ package blockchain import ( + "fmt" "math" "sync" "time" - "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" flow "github.com/tendermint/tmlibs/flowrate" "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" ) /* - eg, L = latency = 0.1s P = num peers = 10 FN = num full nodes @@ -22,7 +24,6 @@ eg, L = latency = 0.1s B/S = CB/P/BS = 12.8 blocks/s 12.8 * 0.1 = 1.28 blocks on conn - */ const ( @@ -30,7 +31,14 @@ const ( maxTotalRequesters = 1000 maxPendingRequests = maxTotalRequesters maxPendingRequestsPerPeer = 50 - minRecvRate = 10240 // 10Kb/s + + // Minimum recv rate to ensure we're receiving blocks from a peer fast + // enough. If a peer is not sending us 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 = 7680 ) var peerTimeoutSeconds = time.Duration(15) // not const so we can override with tests @@ -56,16 +64,16 @@ type BlockPool struct { height int64 // the lowest key in requesters. numPending int32 // number of requests pending assignment or block response // peers - peers map[string]*bpPeer + peers map[p2p.ID]*bpPeer maxPeerHeight int64 requestsCh chan<- BlockRequest - timeoutsCh chan<- string + timeoutsCh chan<- p2p.ID } -func NewBlockPool(start int64, requestsCh chan<- BlockRequest, timeoutsCh chan<- string) *BlockPool { +func NewBlockPool(start int64, requestsCh chan<- BlockRequest, timeoutsCh chan<- p2p.ID) *BlockPool { bp := &BlockPool{ - peers: make(map[string]*bpPeer), + peers: make(map[p2p.ID]*bpPeer), requesters: make(map[int64]*bpRequester), height: start, @@ -88,7 +96,6 @@ func (pool *BlockPool) OnStop() {} // Run spawns requesters as needed. func (pool *BlockPool) makeRequestersRoutine() { - for { if !pool.IsRunning() { break @@ -119,10 +126,13 @@ func (pool *BlockPool) removeTimedoutPeers() { for _, peer := range pool.peers { if !peer.didTimeout && peer.numPending > 0 { curRate := peer.recvMonitor.Status().CurRate - // XXX remove curRate != 0 + // curRate can be 0 on start if curRate != 0 && curRate < minRecvRate { pool.sendTimeout(peer.id) - pool.Logger.Error("SendTimeout", "peer", peer.id, "reason", "curRate too low") + pool.Logger.Error("SendTimeout", "peer", peer.id, + "reason", "peer is not sending us data fast enough", + "curRate", fmt.Sprintf("%d KB/s", curRate/1024), + "minRate", fmt.Sprintf("%d KB/s", minRecvRate/1024)) peer.didTimeout = true } } @@ -195,7 +205,8 @@ func (pool *BlockPool) PopRequest() { // Invalidates the block at pool.height, // Remove the peer and redo request from others. -func (pool *BlockPool) RedoRequest(height int64) { +// Returns the ID of the removed peer. +func (pool *BlockPool) RedoRequest(height int64) p2p.ID { pool.mtx.Lock() defer pool.mtx.Unlock() @@ -205,12 +216,12 @@ func (pool *BlockPool) RedoRequest(height int64) { cmn.PanicSanity("Expected block to be non-nil") } // RemovePeer will redo all requesters associated with this peer. - // TODO: record this malfeasance pool.removePeer(request.peerID) + return request.peerID } // TODO: ensure that blocks come in order for each peer. -func (pool *BlockPool) AddBlock(peerID string, block *types.Block, blockSize int) { +func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) { pool.mtx.Lock() defer pool.mtx.Unlock() @@ -240,7 +251,7 @@ func (pool *BlockPool) MaxPeerHeight() int64 { } // Sets the peer's alleged blockchain height. -func (pool *BlockPool) SetPeerHeight(peerID string, height int64) { +func (pool *BlockPool) SetPeerHeight(peerID p2p.ID, height int64) { pool.mtx.Lock() defer pool.mtx.Unlock() @@ -258,14 +269,14 @@ func (pool *BlockPool) SetPeerHeight(peerID string, height int64) { } } -func (pool *BlockPool) RemovePeer(peerID string) { +func (pool *BlockPool) RemovePeer(peerID p2p.ID) { pool.mtx.Lock() defer pool.mtx.Unlock() pool.removePeer(peerID) } -func (pool *BlockPool) removePeer(peerID string) { +func (pool *BlockPool) removePeer(peerID p2p.ID) { for _, requester := range pool.requesters { if requester.getPeerID() == peerID { if requester.getBlock() != nil { @@ -321,14 +332,14 @@ func (pool *BlockPool) requestersLen() int64 { return int64(len(pool.requesters)) } -func (pool *BlockPool) sendRequest(height int64, peerID string) { +func (pool *BlockPool) sendRequest(height int64, peerID p2p.ID) { if !pool.IsRunning() { return } pool.requestsCh <- BlockRequest{height, peerID} } -func (pool *BlockPool) sendTimeout(peerID string) { +func (pool *BlockPool) sendTimeout(peerID p2p.ID) { if !pool.IsRunning() { return } @@ -357,7 +368,7 @@ func (pool *BlockPool) debug() string { type bpPeer struct { pool *BlockPool - id string + id p2p.ID recvMonitor *flow.Monitor height int64 @@ -368,7 +379,7 @@ type bpPeer struct { logger log.Logger } -func newBPPeer(pool *BlockPool, peerID string, height int64) *bpPeer { +func newBPPeer(pool *BlockPool, peerID p2p.ID, height int64) *bpPeer { peer := &bpPeer{ pool: pool, id: peerID, @@ -434,7 +445,7 @@ type bpRequester struct { redoCh chan struct{} mtx sync.Mutex - peerID string + peerID p2p.ID block *types.Block } @@ -458,7 +469,7 @@ func (bpr *bpRequester) OnStart() error { } // Returns true if the peer matches -func (bpr *bpRequester) setBlock(block *types.Block, peerID string) bool { +func (bpr *bpRequester) setBlock(block *types.Block, peerID p2p.ID) bool { bpr.mtx.Lock() if bpr.block != nil || bpr.peerID != peerID { bpr.mtx.Unlock() @@ -477,7 +488,7 @@ func (bpr *bpRequester) getBlock() *types.Block { return bpr.block } -func (bpr *bpRequester) getPeerID() string { +func (bpr *bpRequester) getPeerID() p2p.ID { bpr.mtx.Lock() defer bpr.mtx.Unlock() return bpr.peerID @@ -502,7 +513,7 @@ func (bpr *bpRequester) requestRoutine() { OUTER_LOOP: for { // Pick a peer to send request to. - var peer *bpPeer = nil + var peer *bpPeer PICK_PEER_LOOP: for { if !bpr.IsRunning() || !bpr.pool.IsRunning() { @@ -523,10 +534,10 @@ OUTER_LOOP: // Send request and wait. bpr.pool.sendRequest(bpr.height, peer.id) select { - case <-bpr.pool.Quit: + case <-bpr.pool.Quit(): bpr.Stop() return - case <-bpr.Quit: + case <-bpr.Quit(): return case <-bpr.redoCh: bpr.reset() @@ -534,10 +545,10 @@ OUTER_LOOP: case <-bpr.gotBlockCh: // We got the block, now see if it's good. select { - case <-bpr.pool.Quit: + case <-bpr.pool.Quit(): bpr.Stop() return - case <-bpr.Quit: + case <-bpr.Quit(): return case <-bpr.redoCh: bpr.reset() @@ -551,5 +562,5 @@ OUTER_LOOP: type BlockRequest struct { Height int64 - PeerID string + PeerID p2p.ID } diff --git a/blockchain/pool_test.go b/blockchain/pool_test.go index 3e347fd29..ce16899a7 100644 --- a/blockchain/pool_test.go +++ b/blockchain/pool_test.go @@ -5,9 +5,11 @@ import ( "testing" "time" - "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" ) func init() { @@ -15,14 +17,14 @@ func init() { } type testPeer struct { - id string + id p2p.ID height int64 } -func makePeers(numPeers int, minHeight, maxHeight int64) map[string]testPeer { - peers := make(map[string]testPeer, numPeers) +func makePeers(numPeers int, minHeight, maxHeight int64) map[p2p.ID]testPeer { + peers := make(map[p2p.ID]testPeer, numPeers) for i := 0; i < numPeers; i++ { - peerID := cmn.RandStr(12) + peerID := p2p.ID(cmn.RandStr(12)) height := minHeight + rand.Int63n(maxHeight-minHeight) peers[peerID] = testPeer{peerID, height} } @@ -32,7 +34,7 @@ func makePeers(numPeers int, minHeight, maxHeight int64) map[string]testPeer { func TestBasic(t *testing.T) { start := int64(42) peers := makePeers(10, start+1, 1000) - timeoutsCh := make(chan string, 100) + timeoutsCh := make(chan p2p.ID, 100) requestsCh := make(chan BlockRequest, 100) pool := NewBlockPool(start, requestsCh, timeoutsCh) pool.SetLogger(log.TestingLogger()) @@ -89,7 +91,7 @@ func TestBasic(t *testing.T) { func TestTimeout(t *testing.T) { start := int64(42) peers := makePeers(10, start+1, 1000) - timeoutsCh := make(chan string, 100) + timeoutsCh := make(chan p2p.ID, 100) requestsCh := make(chan BlockRequest, 100) pool := NewBlockPool(start, requestsCh, timeoutsCh) pool.SetLogger(log.TestingLogger()) @@ -127,7 +129,7 @@ func TestTimeout(t *testing.T) { // Pull from channels counter := 0 - timedOut := map[string]struct{}{} + timedOut := map[p2p.ID]struct{}{} for { select { case peerID := <-timeoutsCh: diff --git a/blockchain/reactor.go b/blockchain/reactor.go index d4b803dd6..2ad6770be 100644 --- a/blockchain/reactor.go +++ b/blockchain/reactor.go @@ -3,16 +3,19 @@ package blockchain import ( "bytes" "errors" + "fmt" "reflect" "sync" "time" wire "github.com/tendermint/go-wire" + + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" - cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/log" ) const ( @@ -47,22 +50,26 @@ type BlockchainReactor struct { // immutable initialState sm.State - blockExec *sm.BlockExecutor - store *BlockStore - pool *BlockPool - fastSync bool - requestsCh chan BlockRequest - timeoutsCh chan string + blockExec *sm.BlockExecutor + store *BlockStore + pool *BlockPool + fastSync bool + + requestsCh <-chan BlockRequest + timeoutsCh <-chan p2p.ID } // NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, fastSync bool) *BlockchainReactor { +func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, + fastSync bool) *BlockchainReactor { + if state.LastBlockHeight != store.Height() { - cmn.PanicSanity(cmn.Fmt("state (%v) and store (%v) height mismatch", state.LastBlockHeight, store.Height())) + cmn.PanicSanity(cmn.Fmt("state (%v) and store (%v) height mismatch", state.LastBlockHeight, + store.Height())) } requestsCh := make(chan BlockRequest, defaultChannelCapacity) - timeoutsCh := make(chan string, defaultChannelCapacity) + timeoutsCh := make(chan p2p.ID, defaultChannelCapacity) pool := NewBlockPool( store.Height()+1, requestsCh, @@ -122,7 +129,8 @@ func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor by sending our state to peer. func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { - if !peer.Send(BlockchainChannel, struct{ BlockchainMessage }{&bcStatusResponseMessage{bcR.store.Height()}}) { + if !peer.Send(BlockchainChannel, + struct{ BlockchainMessage }{&bcStatusResponseMessage{bcR.store.Height()}}) { // doing nothing, will try later in `poolRoutine` } // peer is added to the pool once we receive the first @@ -131,14 +139,16 @@ func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { // RemovePeer implements Reactor by removing peer from the pool. func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { - bcR.pool.RemovePeer(peer.Key()) + bcR.pool.RemovePeer(peer.ID()) } // respondToPeer loads a block and sends it to the requesting peer, // if we have it. Otherwise, we'll respond saying we don't have it. // According to the Tendermint spec, if all nodes are honest, // no node should be requesting for a block that's non-existent. -func (bcR *BlockchainReactor) respondToPeer(msg *bcBlockRequestMessage, src p2p.Peer) (queued bool) { +func (bcR *BlockchainReactor) respondToPeer(msg *bcBlockRequestMessage, + src p2p.Peer) (queued bool) { + block := bcR.store.LoadBlock(msg.Height) if block != nil { msg := &bcBlockResponseMessage{Block: block} @@ -162,7 +172,6 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) bcR.Logger.Debug("Receive", "src", src, "chID", chID, "msg", msg) - // TODO: improve logic to satisfy megacheck switch msg := msg.(type) { case *bcBlockRequestMessage: if queued := bcR.respondToPeer(msg, src); !queued { @@ -170,16 +179,17 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) } case *bcBlockResponseMessage: // Got a block. - bcR.pool.AddBlock(src.Key(), msg.Block, len(msgBytes)) + bcR.pool.AddBlock(src.ID(), msg.Block, len(msgBytes)) case *bcStatusRequestMessage: // Send peer our state. - queued := src.TrySend(BlockchainChannel, struct{ BlockchainMessage }{&bcStatusResponseMessage{bcR.store.Height()}}) + queued := src.TrySend(BlockchainChannel, + struct{ BlockchainMessage }{&bcStatusResponseMessage{bcR.store.Height()}}) if !queued { // sorry } case *bcStatusResponseMessage: // Got a peer status. Unverified. - bcR.pool.SetPeerHeight(src.Key(), msg.Height) + bcR.pool.SetPeerHeight(src.ID(), msg.Height) default: bcR.Logger.Error(cmn.Fmt("Unknown message type %v", reflect.TypeOf(msg))) } @@ -277,23 +287,28 @@ FOR_LOOP: chainID, firstID, first.Height, second.LastCommit) if err != nil { bcR.Logger.Error("Error in validation", "err", err) - bcR.pool.RedoRequest(first.Height) + peerID := bcR.pool.RedoRequest(first.Height) + peer := bcR.Switch.Peers().Get(peerID) + if peer != nil { + bcR.Switch.StopPeerForError(peer, fmt.Errorf("BlockchainReactor validation error: %v", err)) + } break SYNC_LOOP } else { bcR.pool.PopRequest() + // TODO: batch saves so we dont persist to disk every block bcR.store.SaveBlock(first, firstParts, second.LastCommit) - // NOTE: we could improve performance if we - // didn't make the app commit to disk every block - // ... but we would need a way to get the hash without it persisting + // TODO: same thing for app - but we would need a way to + // get the hash without persisting the state var err error state, err = bcR.blockExec.ApplyBlock(state, firstID, first) if err != nil { // TODO This is bad, are we zombie? - cmn.PanicQ(cmn.Fmt("Failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) + cmn.PanicQ(cmn.Fmt("Failed to process committed block (%d:%X): %v", + first.Height, first.Hash(), err)) } - blocksSynced += 1 + blocksSynced++ // update the consensus params bcR.updateConsensusParams(state.ConsensusParams) @@ -307,7 +322,7 @@ FOR_LOOP: } } continue FOR_LOOP - case <-bcR.Quit: + case <-bcR.Quit(): break FOR_LOOP } } @@ -315,7 +330,8 @@ FOR_LOOP: // BroadcastStatusRequest broadcasts `BlockStore` height. func (bcR *BlockchainReactor) BroadcastStatusRequest() error { - bcR.Switch.Broadcast(BlockchainChannel, struct{ BlockchainMessage }{&bcStatusRequestMessage{bcR.store.Height()}}) + bcR.Switch.Broadcast(BlockchainChannel, + struct{ BlockchainMessage }{&bcStatusRequestMessage{bcR.store.Height()}}) return nil } diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index fcb8a6f86..263ca0f05 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -4,6 +4,7 @@ import ( "testing" wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" @@ -28,7 +29,8 @@ func newBlockchainReactor(logger log.Logger, maxBlockHeight int64) *BlockchainRe // Make the blockchainReactor itself fastSync := true var nilApp proxy.AppConnConsensus - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), nilApp, types.MockMempool{}, types.MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), nilApp, + types.MockMempool{}, types.MockEvidencePool{}) bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) bcReactor.SetLogger(logger.With("module", "blockchain")) @@ -47,7 +49,7 @@ func newBlockchainReactor(logger log.Logger, maxBlockHeight int64) *BlockchainRe return bcReactor } -func TestNoBlockMessageResponse(t *testing.T) { +func TestNoBlockResponse(t *testing.T) { maxBlockHeight := int64(20) bcr := newBlockchainReactor(log.TestingLogger(), maxBlockHeight) @@ -55,7 +57,7 @@ func TestNoBlockMessageResponse(t *testing.T) { defer bcr.Stop() // Add some peers in - peer := newbcrTestPeer(cmn.RandStr(12)) + peer := newbcrTestPeer(p2p.ID(cmn.RandStr(12))) bcr.AddPeer(peer) chID := byte(0x01) @@ -71,7 +73,7 @@ func TestNoBlockMessageResponse(t *testing.T) { } // receive a request message from peer, - // wait to hear response + // wait for our response to be received on the peer for _, tt := range tests { reqBlockMsg := &bcBlockRequestMessage{tt.height} reqBlockBytes := wire.BinaryBytes(struct{ BlockchainMessage }{reqBlockMsg}) @@ -95,6 +97,49 @@ func TestNoBlockMessageResponse(t *testing.T) { } } +/* +// 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 TestBadBlockStopsPeer(t *testing.T) { + maxBlockHeight := int64(20) + + bcr := newBlockchainReactor(log.TestingLogger(), maxBlockHeight) + bcr.Start() + defer bcr.Stop() + + // Add some peers in + peer := newbcrTestPeer(p2p.ID(cmn.RandStr(12))) + + // XXX: This doesn't add the peer to anything, + // so it's hard to check that it's later removed + bcr.AddPeer(peer) + assert.True(t, bcr.Switch.Peers().Size() > 0) + + // send a bad block from the peer + // default blocks already dont have commits, so should fail + block := bcr.store.LoadBlock(3) + msg := &bcBlockResponseMessage{Block: block} + peer.Send(BlockchainChannel, struct{ BlockchainMessage }{msg}) + + ticker := time.NewTicker(time.Millisecond * 10) + timer := time.NewTimer(time.Second * 2) +LOOP: + for { + select { + case <-ticker.C: + if bcr.Switch.Peers().Size() == 0 { + break LOOP + } + case <-timer.C: + t.Fatal("Timed out waiting to disconnect peer") + } + } +} +*/ + //---------------------------------------------- // utility funcs @@ -112,25 +157,27 @@ func makeBlock(height int64, state sm.State) *types.Block { // The Test peer type bcrTestPeer struct { - cmn.Service - key string - ch chan interface{} + cmn.BaseService + id p2p.ID + ch chan interface{} } var _ p2p.Peer = (*bcrTestPeer)(nil) -func newbcrTestPeer(key string) *bcrTestPeer { - return &bcrTestPeer{ - Service: cmn.NewBaseService(nil, "bcrTestPeer", nil), - key: key, - ch: make(chan interface{}, 2), +func newbcrTestPeer(id p2p.ID) *bcrTestPeer { + bcr := &bcrTestPeer{ + id: id, + ch: make(chan interface{}, 2), } + bcr.BaseService = *cmn.NewBaseService(nil, "bcrTestPeer", bcr) + return bcr } func (tp *bcrTestPeer) lastValue() interface{} { return <-tp.ch } func (tp *bcrTestPeer) TrySend(chID byte, value interface{}) bool { - if _, ok := value.(struct{ BlockchainMessage }).BlockchainMessage.(*bcStatusResponseMessage); ok { + if _, ok := value.(struct{ BlockchainMessage }). + BlockchainMessage.(*bcStatusResponseMessage); ok { // Discard status response messages since they skew our results // We only want to deal with: // + bcBlockResponseMessage @@ -142,9 +189,9 @@ func (tp *bcrTestPeer) TrySend(chID byte, value interface{}) bool { } func (tp *bcrTestPeer) Send(chID byte, data interface{}) bool { return tp.TrySend(chID, data) } -func (tp *bcrTestPeer) NodeInfo() *p2p.NodeInfo { return nil } +func (tp *bcrTestPeer) NodeInfo() p2p.NodeInfo { return p2p.NodeInfo{} } func (tp *bcrTestPeer) Status() p2p.ConnectionStatus { return p2p.ConnectionStatus{} } -func (tp *bcrTestPeer) Key() string { return tp.key } +func (tp *bcrTestPeer) ID() p2p.ID { return tp.id } func (tp *bcrTestPeer) IsOutbound() bool { return false } func (tp *bcrTestPeer) IsPersistent() bool { return true } func (tp *bcrTestPeer) Get(s string) interface{} { return s } diff --git a/blockchain/store.go b/blockchain/store.go index 1033999fe..a9a543436 100644 --- a/blockchain/store.go +++ b/blockchain/store.go @@ -8,9 +8,11 @@ import ( "sync" wire "github.com/tendermint/go-wire" - "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" + + "github.com/tendermint/tendermint/types" ) /* @@ -252,7 +254,7 @@ func (bsj BlockStoreStateJSON) Save(db dbm.DB) { // If no BlockStoreStateJSON was previously persisted, it returns the zero value. func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON { bytes := db.Get(blockStoreKey) - if bytes == nil { + if len(bytes) == 0 { return BlockStoreStateJSON{ Height: 0, } diff --git a/blockchain/store_test.go b/blockchain/store_test.go index 1fd88dac3..16185ca0d 100644 --- a/blockchain/store_test.go +++ b/blockchain/store_test.go @@ -13,9 +13,11 @@ import ( "github.com/stretchr/testify/require" wire "github.com/tendermint/go-wire" - "github.com/tendermint/tendermint/types" + "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/types" ) func TestLoadBlockStoreStateJSON(t *testing.T) { @@ -40,7 +42,6 @@ func TestNewBlockStore(t *testing.T) { wantErr string }{ {[]byte("artful-doger"), "not unmarshal bytes"}, - {[]byte(""), "unmarshal bytes"}, {[]byte(" "), "unmarshal bytes"}, } @@ -74,7 +75,7 @@ func TestBlockStoreGetReader(t *testing.T) { }{ 0: {key: []byte("Foo"), want: []byte("Bar")}, 1: {key: []byte("KnoxNonExistent"), want: nil}, - 2: {key: []byte("Foo1"), want: nil}, + 2: {key: []byte("Foo1"), want: []byte{}}, } for i, tt := range tests { @@ -104,7 +105,8 @@ var ( partSet = block.MakePartSet(2) part1 = partSet.GetPart(0) part2 = partSet.GetPart(1) - seenCommit1 = &types.Commit{Precommits: []*types.Vote{{Height: 10, Timestamp: time.Now().UTC()}}} + seenCommit1 = &types.Commit{Precommits: []*types.Vote{{Height: 10, + Timestamp: time.Now().UTC()}}} ) // TODO: This test should be simplified ... @@ -124,7 +126,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) { // save a block block := makeBlock(bs.Height()+1, state) validPartSet := block.MakePartSet(2) - seenCommit := &types.Commit{Precommits: []*types.Vote{{Height: 10, Timestamp: time.Now().UTC()}}} + seenCommit := &types.Commit{Precommits: []*types.Vote{{Height: 10, + Timestamp: time.Now().UTC()}}} bs.SaveBlock(block, partSet, seenCommit) require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed") @@ -143,7 +146,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) { // End of setup, test data - commitAtH10 := &types.Commit{Precommits: []*types.Vote{{Height: 10, Timestamp: time.Now().UTC()}}} + commitAtH10 := &types.Commit{Precommits: []*types.Vote{{Height: 10, + Timestamp: time.Now().UTC()}}} tuples := []struct { block *types.Block parts *types.PartSet @@ -263,7 +267,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) { db.Set(calcBlockCommitKey(commitHeight), []byte("foo-bogus")) } bCommit := bs.LoadBlockCommit(commitHeight) - return &quad{block: bBlock, seenCommit: bSeenCommit, commit: bCommit, meta: bBlockMeta}, nil + return &quad{block: bBlock, seenCommit: bSeenCommit, commit: bCommit, + meta: bBlockMeta}, nil }) if subStr := tuple.wantPanic; subStr != "" { @@ -290,10 +295,12 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) { continue } if tuple.eraseSeenCommitInDB { - assert.Nil(t, qua.seenCommit, "erased the seenCommit in the DB hence we should get back a nil seenCommit") + assert.Nil(t, qua.seenCommit, + "erased the seenCommit in the DB hence we should get back a nil seenCommit") } if tuple.eraseCommitInDB { - assert.Nil(t, qua.commit, "erased the commit in the DB hence we should get back a nil commit") + assert.Nil(t, qua.commit, + "erased the commit in the DB hence we should get back a nil commit") } } } @@ -331,7 +338,8 @@ func TestLoadBlockPart(t *testing.T) { gotPart, _, panicErr := doFn(loadPart) require.Nil(t, panicErr, "an existent and proper block should not panic") require.Nil(t, res, "a properly saved block should return a proper block") - require.Equal(t, gotPart.(*types.Part).Hash(), part1.Hash(), "expecting successful retrieval of previously saved block") + require.Equal(t, gotPart.(*types.Part).Hash(), part1.Hash(), + "expecting successful retrieval of previously saved block") } func TestLoadBlockMeta(t *testing.T) { @@ -360,7 +368,8 @@ func TestLoadBlockMeta(t *testing.T) { gotMeta, _, panicErr := doFn(loadMeta) require.Nil(t, panicErr, "an existent and proper block should not panic") require.Nil(t, res, "a properly saved blockMeta should return a proper blocMeta ") - require.Equal(t, binarySerializeIt(meta), binarySerializeIt(gotMeta), "expecting successful retrieval of previously saved blockMeta") + require.Equal(t, binarySerializeIt(meta), binarySerializeIt(gotMeta), + "expecting successful retrieval of previously saved blockMeta") } func TestBlockFetchAtHeight(t *testing.T) { @@ -369,13 +378,15 @@ func TestBlockFetchAtHeight(t *testing.T) { block := makeBlock(bs.Height()+1, state) partSet := block.MakePartSet(2) - seenCommit := &types.Commit{Precommits: []*types.Vote{{Height: 10, Timestamp: time.Now().UTC()}}} + seenCommit := &types.Commit{Precommits: []*types.Vote{{Height: 10, + Timestamp: time.Now().UTC()}}} bs.SaveBlock(block, partSet, seenCommit) require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed") blockAtHeight := bs.LoadBlock(bs.Height()) - require.Equal(t, block.Hash(), blockAtHeight.Hash(), "expecting a successful load of the last saved block") + require.Equal(t, block.Hash(), blockAtHeight.Hash(), + "expecting a successful load of the last saved block") blockAtHeightPlus1 := bs.LoadBlock(bs.Height() + 1) require.Nil(t, blockAtHeightPlus1, "expecting an unsuccessful load of Height()+1") diff --git a/circle.yml b/circle.yml index 9d03bc46e..fd5fe180b 100644 --- a/circle.yml +++ b/circle.yml @@ -31,4 +31,5 @@ test: - cd "$PROJECT_PATH" && mv test_integrations.log "${CIRCLE_ARTIFACTS}" - cd "$PROJECT_PATH" && bash <(curl -s https://codecov.io/bash) -f coverage.txt - cd "$PROJECT_PATH" && mv coverage.txt "${CIRCLE_ARTIFACTS}" - - cd "$PROJECT_PATH" && cp test/logs/messages "${CIRCLE_ARTIFACTS}/docker_logs.txt" + - cd "$PROJECT_PATH" && cp test/logs/messages "${CIRCLE_ARTIFACTS}/docker.log" + - cd "${CIRCLE_ARTIFACTS}" && tar czf logs.tar.gz *.log diff --git a/cmd/tendermint/commands/root.go b/cmd/tendermint/commands/root.go index a54b50069..f229a7889 100644 --- a/cmd/tendermint/commands/root.go +++ b/cmd/tendermint/commands/root.go @@ -14,11 +14,15 @@ import ( var ( config = cfg.DefaultConfig() - logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "main") + logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) ) func init() { - RootCmd.PersistentFlags().String("log_level", config.LogLevel, "Log level") + registerFlagsRootCmd(RootCmd) +} + +func registerFlagsRootCmd(cmd *cobra.Command) { + cmd.PersistentFlags().String("log_level", config.LogLevel, "Log level") } // ParseConfig retrieves the default environment configuration, @@ -53,6 +57,7 @@ var RootCmd = &cobra.Command{ if viper.GetBool(cli.TraceFlag) { logger = log.NewTracingLogger(logger) } + logger = logger.With("module", "main") return nil }, } diff --git a/cmd/tendermint/commands/root_test.go b/cmd/tendermint/commands/root_test.go index 8217ee166..59d258af7 100644 --- a/cmd/tendermint/commands/root_test.go +++ b/cmd/tendermint/commands/root_test.go @@ -1,7 +1,10 @@ package commands import ( + "fmt" + "io/ioutil" "os" + "path/filepath" "strconv" "testing" @@ -12,6 +15,7 @@ import ( cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" ) var ( @@ -22,89 +26,151 @@ const ( rootName = "root" ) -// isolate provides a clean setup and returns a copy of RootCmd you can -// modify in the test cases. -// NOTE: it unsets all TM* env variables. -func isolate(cmds ...*cobra.Command) cli.Executable { +// clearConfig clears env vars, the given root dir, and resets viper. +func clearConfig(dir string) { if err := os.Unsetenv("TMHOME"); err != nil { panic(err) } if err := os.Unsetenv("TM_HOME"); err != nil { panic(err) } - if err := os.RemoveAll(defaultRoot); err != nil { + + if err := os.RemoveAll(dir); err != nil { panic(err) } - viper.Reset() config = cfg.DefaultConfig() - r := &cobra.Command{ - Use: rootName, +} + +// prepare new rootCmd +func testRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: RootCmd.Use, PersistentPreRunE: RootCmd.PersistentPreRunE, + Run: func(cmd *cobra.Command, args []string) {}, } - r.AddCommand(cmds...) - wr := cli.PrepareBaseCmd(r, "TM", defaultRoot) - return wr + registerFlagsRootCmd(rootCmd) + var l string + rootCmd.PersistentFlags().String("log", l, "Log") + return rootCmd } -func TestRootConfig(t *testing.T) { - assert, require := assert.New(t), require.New(t) +func testSetup(rootDir string, args []string, env map[string]string) error { + clearConfig(defaultRoot) - // we pre-create a config file we can refer to in the rest of - // the test cases. - cvals := map[string]string{ - "moniker": "monkey", - "fast_sync": "false", + rootCmd := testRootCmd() + cmd := cli.PrepareBaseCmd(rootCmd, "TM", defaultRoot) + + // run with the args and env + args = append([]string{rootCmd.Use}, args...) + return cli.RunWithArgs(cmd, args, env) +} + +func TestRootHome(t *testing.T) { + newRoot := filepath.Join(defaultRoot, "something-else") + cases := []struct { + args []string + env map[string]string + root string + }{ + {nil, nil, defaultRoot}, + {[]string{"--home", newRoot}, nil, newRoot}, + {nil, map[string]string{"TMHOME": newRoot}, newRoot}, + } + + for i, tc := range cases { + idxString := strconv.Itoa(i) + + err := testSetup(defaultRoot, tc.args, tc.env) + require.Nil(t, err, idxString) + + assert.Equal(t, tc.root, config.RootDir, idxString) + assert.Equal(t, tc.root, config.P2P.RootDir, idxString) + assert.Equal(t, tc.root, config.Consensus.RootDir, idxString) + assert.Equal(t, tc.root, config.Mempool.RootDir, idxString) } - // proper types of the above settings - cfast := false - conf, err := cli.WriteDemoConfig(cvals) - require.Nil(err) +} + +func TestRootFlagsEnv(t *testing.T) { + // defaults defaults := cfg.DefaultConfig() - dmax := defaults.P2P.MaxNumPeers + defaultLogLvl := defaults.LogLevel cases := []struct { args []string env map[string]string - root string - moniker string - fastSync bool - maxPeer int + logLevel string }{ - {nil, nil, defaultRoot, defaults.Moniker, defaults.FastSync, dmax}, - // try multiple ways of setting root (two flags, cli vs. env) - {[]string{"--home", conf}, nil, conf, cvals["moniker"], cfast, dmax}, - {nil, map[string]string{"TMHOME": conf}, conf, cvals["moniker"], cfast, dmax}, - // check setting p2p subflags two different ways - {[]string{"--p2p.max_num_peers", "420"}, nil, defaultRoot, defaults.Moniker, defaults.FastSync, 420}, - {nil, map[string]string{"TM_P2P_MAX_NUM_PEERS": "17"}, defaultRoot, defaults.Moniker, defaults.FastSync, 17}, - // try to set env that have no flags attached... - {[]string{"--home", conf}, map[string]string{"TM_MONIKER": "funny"}, conf, "funny", cfast, dmax}, + {[]string{"--log", "debug"}, nil, defaultLogLvl}, // wrong flag + {[]string{"--log_level", "debug"}, nil, "debug"}, // right flag + {nil, map[string]string{"TM_LOW": "debug"}, defaultLogLvl}, // wrong env flag + {nil, map[string]string{"MT_LOG_LEVEL": "debug"}, defaultLogLvl}, // wrong env prefix + {nil, map[string]string{"TM_LOG_LEVEL": "debug"}, "debug"}, // right env } - for idx, tc := range cases { - i := strconv.Itoa(idx) - // test command that does nothing, except trigger unmarshalling in root - noop := &cobra.Command{ - Use: "noop", - RunE: func(cmd *cobra.Command, args []string) error { - return nil - }, - } - noop.Flags().Int("p2p.max_num_peers", defaults.P2P.MaxNumPeers, "") - cmd := isolate(noop) - - args := append([]string{rootName, noop.Use}, tc.args...) - err := cli.RunWithArgs(cmd, args, tc.env) - require.Nil(err, i) - assert.Equal(tc.root, config.RootDir, i) - assert.Equal(tc.root, config.P2P.RootDir, i) - assert.Equal(tc.root, config.Consensus.RootDir, i) - assert.Equal(tc.root, config.Mempool.RootDir, i) - assert.Equal(tc.moniker, config.Moniker, i) - assert.Equal(tc.fastSync, config.FastSync, i) - assert.Equal(tc.maxPeer, config.P2P.MaxNumPeers, i) + for i, tc := range cases { + idxString := strconv.Itoa(i) + + err := testSetup(defaultRoot, tc.args, tc.env) + require.Nil(t, err, idxString) + + assert.Equal(t, tc.logLevel, config.LogLevel, idxString) } +} +func TestRootConfig(t *testing.T) { + + // write non-default config + nonDefaultLogLvl := "abc:debug" + cvals := map[string]string{ + "log_level": nonDefaultLogLvl, + } + + cases := []struct { + args []string + env map[string]string + + logLvl string + }{ + {nil, nil, nonDefaultLogLvl}, // should load config + {[]string{"--log_level=abc:info"}, nil, "abc:info"}, // flag over rides + {nil, map[string]string{"TM_LOG_LEVEL": "abc:info"}, "abc:info"}, // env over rides + } + + for i, tc := range cases { + idxString := strconv.Itoa(i) + clearConfig(defaultRoot) + + // XXX: path must match cfg.defaultConfigPath + configFilePath := filepath.Join(defaultRoot, "config") + err := cmn.EnsureDir(configFilePath, 0700) + require.Nil(t, err) + + // write the non-defaults to a different path + // TODO: support writing sub configs so we can test that too + err = WriteConfigVals(configFilePath, cvals) + require.Nil(t, err) + + rootCmd := testRootCmd() + cmd := cli.PrepareBaseCmd(rootCmd, "TM", defaultRoot) + + // run with the args and env + tc.args = append([]string{rootCmd.Use}, tc.args...) + err = cli.RunWithArgs(cmd, tc.args, tc.env) + require.Nil(t, err, idxString) + + assert.Equal(t, tc.logLvl, config.LogLevel, idxString) + } +} + +// WriteConfigVals writes a toml file with the given values. +// It returns an error if writing was impossible. +func WriteConfigVals(dir string, vals map[string]string) error { + data := "" + for k, v := range vals { + data = 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 0f37bb319..76d61671d 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -29,8 +29,10 @@ func AddNodeFlags(cmd *cobra.Command) { // p2p flags cmd.Flags().String("p2p.laddr", config.P2P.ListenAddress, "Node listen address. (0.0.0.0:0 means any interface, any port)") cmd.Flags().String("p2p.seeds", config.P2P.Seeds, "Comma delimited host:port seed nodes") + cmd.Flags().String("p2p.persistent_peers", config.P2P.PersistentPeers, "Comma delimited host:port persistent peers") cmd.Flags().Bool("p2p.skip_upnp", config.P2P.SkipUPNP, "Skip UPNP configuration") cmd.Flags().Bool("p2p.pex", config.P2P.PexReactor, "Enable/disable Peer-Exchange") + cmd.Flags().Bool("p2p.seed_mode", config.P2P.SeedMode, "Enable/disable seed mode") // consensus flags cmd.Flags().Bool("consensus.create_empty_blocks", config.Consensus.CreateEmptyBlocks, "Set this to false to only produce blocks when there are txs or when the AppHash changes") diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index 2c859df2b..f5551a95e 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -2,11 +2,12 @@ package commands import ( "fmt" - "path" + "path/filepath" "time" "github.com/spf13/cobra" + cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" ) @@ -35,6 +36,7 @@ var TestnetFilesCmd = &cobra.Command{ func testnetFiles(cmd *cobra.Command, args []string) { genVals := make([]types.GenesisValidator, nValidators) + defaultConfig := cfg.DefaultBaseConfig() // Initialize core dir and priv_validator.json's for i := 0; i < nValidators; i++ { @@ -44,7 +46,7 @@ func testnetFiles(cmd *cobra.Command, args []string) { cmn.Exit(err.Error()) } // Read priv_validator.json to populate vals - privValFile := path.Join(dataDir, mach, "priv_validator.json") + privValFile := filepath.Join(dataDir, mach, defaultConfig.PrivValidator) privVal := types.LoadPrivValidatorFS(privValFile) genVals[i] = types.GenesisValidator{ PubKey: privVal.GetPubKey(), @@ -63,7 +65,7 @@ func testnetFiles(cmd *cobra.Command, args []string) { // Write genesis file. for i := 0; i < nValidators; i++ { mach := cmn.Fmt("mach%d", i) - if err := genDoc.SaveAs(path.Join(dataDir, mach, "genesis.json")); err != nil { + if err := genDoc.SaveAs(filepath.Join(dataDir, mach, defaultConfig.Genesis)); err != nil { panic(err) } } @@ -73,14 +75,15 @@ func testnetFiles(cmd *cobra.Command, args []string) { // Initialize per-machine core directory func initMachCoreDirectory(base, mach string) error { - dir := path.Join(base, mach) + dir := filepath.Join(base, mach) err := cmn.EnsureDir(dir, 0777) if err != nil { return err } // Create priv_validator.json file if not present - ensurePrivValidator(path.Join(dir, "priv_validator.json")) + defaultConfig := cfg.DefaultBaseConfig() + ensurePrivValidator(filepath.Join(dir, defaultConfig.PrivValidator)) return nil } diff --git a/cmd/tendermint/main.go b/cmd/tendermint/main.go index c24cfe197..17b5a585b 100644 --- a/cmd/tendermint/main.go +++ b/cmd/tendermint/main.go @@ -2,10 +2,12 @@ package main import ( "os" + "path/filepath" "github.com/tendermint/tmlibs/cli" cmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + cfg "github.com/tendermint/tendermint/config" nm "github.com/tendermint/tendermint/node" ) @@ -37,7 +39,7 @@ func main() { // Create & start node rootCmd.AddCommand(cmd.NewRunNodeCmd(nodeFunc)) - cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv("$HOME/.tendermint")) + cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv(filepath.Join("$HOME", cfg.DefaultTendermintDir))) if err := cmd.Execute(); err != nil { panic(err) } diff --git a/config/config.go b/config/config.go index 5d4a8ef65..621847bd0 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,30 @@ import ( "time" ) +// NOTE: Most of the structs & relevant comments + the +// default configuration options were used to manually +// generate the config.toml. Please reflect any changes +// made here in the defaultConfigTemplate constant in +// config/toml.go +// NOTE: tmlibs/cli must know to look in the config dir! +var ( + DefaultTendermintDir = ".tendermint" + defaultConfigDir = "config" + defaultDataDir = "data" + + defaultConfigFileName = "config.toml" + defaultGenesisJSONName = "genesis.json" + defaultPrivValName = "priv_validator.json" + defaultNodeKeyName = "node_key.json" + defaultAddrBookName = "addrbook.json" + + defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName) + defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName) + defaultPrivValPath = filepath.Join(defaultConfigDir, defaultPrivValName) + defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName) + defaultAddrBookPath = filepath.Join(defaultConfigDir, defaultAddrBookName) +) + // Config defines the top level configuration for a Tendermint node type Config struct { // Top level options use an anonymous struct @@ -38,9 +62,9 @@ func TestConfig() *Config { BaseConfig: TestBaseConfig(), RPC: TestRPCConfig(), P2P: TestP2PConfig(), - Mempool: DefaultMempoolConfig(), + Mempool: TestMempoolConfig(), Consensus: TestConsensusConfig(), - TxIndex: DefaultTxIndexConfig(), + TxIndex: TestTxIndexConfig(), } } @@ -59,19 +83,23 @@ func (cfg *Config) SetRoot(root string) *Config { // BaseConfig defines the base configuration for a Tendermint node type BaseConfig struct { + + // chainID is unexposed and immutable but here for convenience + chainID string + // The root directory for all data. // This should be set in viper so it can unmarshal into this struct RootDir string `mapstructure:"home"` - // The ID of the chain to join (should be signed with every transaction and vote) - ChainID string `mapstructure:"chain_id"` - - // A JSON file containing the initial validator set and other meta data + // Path to the JSON file containing the initial validator set and other meta data Genesis string `mapstructure:"genesis_file"` - // A JSON file containing the private key to use as a validator in the consensus protocol + // Path to the JSON file containing the private key to use as a validator in the consensus protocol PrivValidator string `mapstructure:"priv_validator_file"` + // A JSON file containing the private key to use for p2p authenticated encryption + NodeKey string `mapstructure:"node_key_file"` + // A custom human readable name for this node Moniker string `mapstructure:"moniker"` @@ -104,11 +132,16 @@ type BaseConfig struct { DBPath string `mapstructure:"db_dir"` } +func (c BaseConfig) ChainID() string { + return c.chainID +} + // DefaultBaseConfig returns a default base configuration for a Tendermint node func DefaultBaseConfig() BaseConfig { return BaseConfig{ - Genesis: "genesis.json", - PrivValidator: "priv_validator.json", + Genesis: defaultGenesisJSONPath, + PrivValidator: defaultPrivValPath, + NodeKey: defaultNodeKeyPath, Moniker: defaultMoniker, ProxyApp: "tcp://127.0.0.1:46658", ABCI: "socket", @@ -124,7 +157,7 @@ func DefaultBaseConfig() BaseConfig { // TestBaseConfig returns a base configuration for testing a Tendermint node func TestBaseConfig() BaseConfig { conf := DefaultBaseConfig() - conf.ChainID = "tendermint_test" + conf.chainID = "tendermint_test" conf.ProxyApp = "dummy" conf.FastSync = false conf.DBBackend = "memdb" @@ -141,6 +174,11 @@ func (b BaseConfig) PrivValidatorFile() string { return rootify(b.PrivValidator, b.RootDir) } +// NodeKeyFile returns the full path to the node_key.json file +func (b BaseConfig) NodeKeyFile() string { + return rootify(b.NodeKey, b.RootDir) +} + // DBDir returns the full path to the database directory func (b BaseConfig) DBDir() string { return rootify(b.DBPath, b.RootDir) @@ -151,9 +189,10 @@ func DefaultLogLevel() string { return "error" } -// DefaultPackageLogLevels returns a default log level setting so all packages log at "error", while the `state` package logs at "info" +// DefaultPackageLogLevels returns a default log level setting so all packages +// log at "error", while the `state` and `main` packages log at "info" func DefaultPackageLogLevels() string { - return fmt.Sprintf("state:info,*:%s", DefaultLogLevel()) + return fmt.Sprintf("main:info,state:info,*:%s", DefaultLogLevel()) } //----------------------------------------------------------------------------- @@ -170,7 +209,7 @@ type RPCConfig struct { // NOTE: This server only supports /broadcast_tx_commit GRPCListenAddress string `mapstructure:"grpc_laddr"` - // Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool + // Activate unsafe RPC commands like /dial_persistent_peers and /unsafe_flush_mempool Unsafe bool `mapstructure:"unsafe"` } @@ -203,8 +242,13 @@ type P2PConfig struct { ListenAddress string `mapstructure:"laddr"` // Comma separated list of seed nodes to connect to + // We only use these if we can’t connect to peers in the addrbook Seeds string `mapstructure:"seeds"` + // Comma separated list of persistent peers to connect to + // We always connect to these + PersistentPeers string `mapstructure:"persistent_peers"` + // Skip UPNP port forwarding SkipUPNP bool `mapstructure:"skip_upnp"` @@ -214,9 +258,6 @@ type P2PConfig struct { // Set true for strict address routability rules AddrBookStrict bool `mapstructure:"addr_book_strict"` - // Set true to enable the peer-exchange reactor - PexReactor bool `mapstructure:"pex"` - // Maximum number of peers to connect to MaxNumPeers int `mapstructure:"max_num_peers"` @@ -231,13 +272,22 @@ type P2PConfig struct { // Rate at which packets can be received, in bytes/second RecvRate int64 `mapstructure:"recv_rate"` + + // Set true to enable the peer-exchange reactor + PexReactor bool `mapstructure:"pex"` + + // Seed mode, in which node constantly crawls the network and looks for + // peers. If another node asks it for addresses, it responds and disconnects. + // + // Does not work if the peer-exchange reactor is disabled. + SeedMode bool `mapstructure:"seed_mode"` } // DefaultP2PConfig returns a default configuration for the peer-to-peer layer func DefaultP2PConfig() *P2PConfig { return &P2PConfig{ ListenAddress: "tcp://0.0.0.0:46656", - AddrBook: "addrbook.json", + AddrBook: defaultAddrBookPath, AddrBookStrict: true, MaxNumPeers: 50, FlushThrottleTimeout: 100, @@ -245,6 +295,7 @@ func DefaultP2PConfig() *P2PConfig { SendRate: 512000, // 500 kB/s RecvRate: 512000, // 500 kB/s PexReactor: true, + SeedMode: false, } } @@ -253,6 +304,7 @@ func TestP2PConfig() *P2PConfig { conf := DefaultP2PConfig() conf.ListenAddress = "tcp://0.0.0.0:36656" conf.SkipUPNP = true + conf.FlushThrottleTimeout = 10 return conf } @@ -271,6 +323,7 @@ type MempoolConfig struct { RecheckEmpty bool `mapstructure:"recheck_empty"` Broadcast bool `mapstructure:"broadcast"` WalPath string `mapstructure:"wal_dir"` + CacheSize int `mapstructure:"cache_size"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -279,10 +332,18 @@ func DefaultMempoolConfig() *MempoolConfig { Recheck: true, RecheckEmpty: true, Broadcast: true, - WalPath: "data/mempool.wal", + WalPath: filepath.Join(defaultDataDir, "mempool.wal"), + CacheSize: 100000, } } +// TestMempoolConfig returns a configuration for testing the Tendermint mempool +func TestMempoolConfig() *MempoolConfig { + config := DefaultMempoolConfig() + config.CacheSize = 1000 + return config +} + // WalDir returns the full path to the mempool's write-ahead log func (m *MempoolConfig) WalDir() string { return rootify(m.WalPath, m.RootDir) @@ -299,7 +360,7 @@ type ConsensusConfig struct { WalLight bool `mapstructure:"wal_light"` walFile string // overrides WalPath if set - // All timeouts are in ms + // All timeouts are in milliseconds TimeoutPropose int `mapstructure:"timeout_propose"` TimeoutProposeDelta int `mapstructure:"timeout_propose_delta"` TimeoutPrevote int `mapstructure:"timeout_prevote"` @@ -319,7 +380,7 @@ type ConsensusConfig struct { CreateEmptyBlocks bool `mapstructure:"create_empty_blocks"` CreateEmptyBlocksInterval int `mapstructure:"create_empty_blocks_interval"` - // Reactor sleep duration parameters are in ms + // Reactor sleep duration parameters are in milliseconds PeerGossipSleepDuration int `mapstructure:"peer_gossip_sleep_duration"` PeerQueryMaj23SleepDuration int `mapstructure:"peer_query_maj23_sleep_duration"` } @@ -367,7 +428,7 @@ func (cfg *ConsensusConfig) PeerQueryMaj23Sleep() time.Duration { // DefaultConsensusConfig returns a default configuration for the consensus service func DefaultConsensusConfig() *ConsensusConfig { return &ConsensusConfig{ - WalPath: "data/cs.wal/wal", + WalPath: filepath.Join(defaultDataDir, "cs.wal", "wal"), WalLight: false, TimeoutPropose: 3000, TimeoutProposeDelta: 500, @@ -397,6 +458,8 @@ func TestConsensusConfig() *ConsensusConfig { config.TimeoutPrecommitDelta = 1 config.TimeoutCommit = 10 config.SkipTimeoutCommit = true + config.PeerGossipSleepDuration = 5 + config.PeerQueryMaj23SleepDuration = 250 return config } @@ -448,6 +511,11 @@ func DefaultTxIndexConfig() *TxIndexConfig { } } +// TestTxIndexConfig returns a default configuration for the transaction indexer. +func TestTxIndexConfig() *TxIndexConfig { + return DefaultTxIndexConfig() +} + //----------------------------------------------------------------------------- // Utils diff --git a/config/toml.go b/config/toml.go index 735f45c12..e69cf37fe 100644 --- a/config/toml.go +++ b/config/toml.go @@ -1,52 +1,224 @@ package config import ( + "bytes" "os" - "path" "path/filepath" - "strings" + "text/template" cmn "github.com/tendermint/tmlibs/common" ) +var configTemplate *template.Template + +func init() { + var err error + if configTemplate, err = template.New("configFileTemplate").Parse(defaultConfigTemplate); err != nil { + panic(err) + } +} + /****** these are for production settings ***********/ +// EnsureRoot creates the root, config, and data directories if they don't exist, +// and panics if it fails. func EnsureRoot(rootDir string) { if err := cmn.EnsureDir(rootDir, 0700); err != nil { cmn.PanicSanity(err.Error()) } - if err := cmn.EnsureDir(rootDir+"/data", 0700); err != nil { + if err := cmn.EnsureDir(filepath.Join(rootDir, defaultConfigDir), 0700); err != nil { + cmn.PanicSanity(err.Error()) + } + if err := cmn.EnsureDir(filepath.Join(rootDir, defaultDataDir), 0700); err != nil { cmn.PanicSanity(err.Error()) } - configFilePath := path.Join(rootDir, "config.toml") + configFilePath := filepath.Join(rootDir, defaultConfigFilePath) // Write default config file if missing. if !cmn.FileExists(configFilePath) { - cmn.MustWriteFile(configFilePath, []byte(defaultConfig(defaultMoniker)), 0644) + writeConfigFile(configFilePath) } } -var defaultConfigTmpl = `# This is a TOML config file. +// XXX: this func should probably be called by cmd/tendermint/commands/init.go +// alongside the writing of the genesis.json and priv_validator.json +func writeConfigFile(configFilePath string) { + var buffer bytes.Buffer + + if err := configTemplate.Execute(&buffer, DefaultConfig()); err != nil { + panic(err) + } + + cmn.MustWriteFile(configFilePath, buffer.Bytes(), 0644) +} + +// Note: any changes to the comments/variables/mapstructure +// must be reflected in the appropriate struct in config/config.go +const defaultConfigTemplate = `# This is a TOML config file. # For more information, see https://github.com/toml-lang/toml -proxy_app = "tcp://127.0.0.1:46658" -moniker = "__MONIKER__" -fast_sync = true -db_backend = "leveldb" -log_level = "state:info,*:error" +##### main base config options ##### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy_app = "{{ .BaseConfig.ProxyApp }}" + +# A custom human readable name for this node +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 }} + +# Database backend: leveldb | memdb +db_backend = "{{ .BaseConfig.DBBackend }}" + +# Database directory +db_path = "{{ .BaseConfig.DBPath }}" + +# Output level for logging, including package level options +log_level = "{{ .BaseConfig.LogLevel }}" +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis_file = "{{ .BaseConfig.Genesis }}" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_file = "{{ .BaseConfig.PrivValidator }}" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "{{ .BaseConfig.NodeKey}}" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "{{ .BaseConfig.ABCI }}" + +# TCP or UNIX socket address for the profiling server to listen on +prof_laddr = "{{ .BaseConfig.ProfListenAddress }}" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = {{ .BaseConfig.FilterPeers }} + +##### advanced configuration options ##### + +##### rpc server configuration options ##### [rpc] -laddr = "tcp://0.0.0.0:46657" +# TCP or UNIX socket address for the RPC server to listen on +laddr = "{{ .RPC.ListenAddress }}" + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "{{ .RPC.GRPCListenAddress }}" + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = {{ .RPC.Unsafe }} + +##### peer to peer configuration options ##### [p2p] -laddr = "tcp://0.0.0.0:46656" + +# Address to listen for incoming connections +laddr = "{{ .P2P.ListenAddress }}" + +# Comma separated list of seed nodes to connect to seeds = "" -` -func defaultConfig(moniker string) string { - return strings.Replace(defaultConfigTmpl, "__MONIKER__", moniker, -1) -} +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "" + +# Path to address book +addr_book_file = "{{ .P2P.AddrBook }}" + +# Set true for strict address routability rules +addr_book_strict = {{ .P2P.AddrBookStrict }} + +# Time to wait before flushing messages out on the connection, in ms +flush_throttle_timeout = {{ .P2P.FlushThrottleTimeout }} + +# Maximum number of peers to connect to +max_num_peers = {{ .P2P.MaxNumPeers }} + +# Maximum size of a message packet payload, in bytes +max_msg_packet_payload_size = {{ .P2P.MaxMsgPacketPayloadSize }} + +# Rate at which packets can be sent, in bytes/second +send_rate = {{ .P2P.SendRate }} + +# Rate at which packets can be received, in bytes/second +recv_rate = {{ .P2P.RecvRate }} + +# Set true to enable the peer-exchange reactor +pex = {{ .P2P.PexReactor }} + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = {{ .P2P.SeedMode }} + +##### mempool configuration options ##### +[mempool] + +recheck = {{ .Mempool.Recheck }} +recheck_empty = {{ .Mempool.RecheckEmpty }} +broadcast = {{ .Mempool.Broadcast }} +wal_dir = "{{ .Mempool.WalPath }}" + +##### consensus configuration options ##### +[consensus] + +wal_file = "{{ .Consensus.WalPath }}" +wal_light = {{ .Consensus.WalLight }} + +# All timeouts are in milliseconds +timeout_propose = {{ .Consensus.TimeoutPropose }} +timeout_propose_delta = {{ .Consensus.TimeoutProposeDelta }} +timeout_prevote = {{ .Consensus.TimeoutPrevote }} +timeout_prevote_delta = {{ .Consensus.TimeoutPrevoteDelta }} +timeout_precommit = {{ .Consensus.TimeoutPrecommit }} +timeout_precommit_delta = {{ .Consensus.TimeoutPrecommitDelta }} +timeout_commit = {{ .Consensus.TimeoutCommit }} + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = {{ .Consensus.SkipTimeoutCommit }} + +# BlockSize +max_block_size_txs = {{ .Consensus.MaxBlockSizeTxs }} +max_block_size_bytes = {{ .Consensus.MaxBlockSizeBytes }} + +# EmptyBlocks mode and possible interval between empty blocks in seconds +create_empty_blocks = {{ .Consensus.CreateEmptyBlocks }} +create_empty_blocks_interval = {{ .Consensus.CreateEmptyBlocksInterval }} + +# Reactor sleep duration parameters are in milliseconds +peer_gossip_sleep_duration = {{ .Consensus.PeerGossipSleepDuration }} +peer_query_maj23_sleep_duration = {{ .Consensus.PeerQueryMaj23SleepDuration }} + +##### transactions indexer configuration options ##### +[tx_index] + +# What indexer to use for transactions +# +# Options: +# 1) "null" (default) +# 2) "kv" - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +indexer = "{{ .TxIndex.Indexer }}" + +# Comma-separated list of tags to index (by default the only tag is tx hash) +# +# It's recommended to index only a subset of tags due to possible memory +# bloat. This is, of course, depends on the indexer's DB and the volume of +# transactions. +index_tags = "{{ .TxIndex.IndexTags }}" + +# When set to true, tells indexer to index all tags. Note this may be not +# desirable (see the comment above). IndexTags has a precedence over +# IndexAllTags (i.e. when given both, IndexTags will be indexed). +index_all_tags = {{ .TxIndex.IndexAllTags }} +` /****** these are for test settings ***********/ @@ -69,17 +241,21 @@ func ResetTestRoot(testName string) *Config { if err := cmn.EnsureDir(rootDir, 0700); err != nil { cmn.PanicSanity(err.Error()) } - if err := cmn.EnsureDir(rootDir+"/data", 0700); err != nil { + if err := cmn.EnsureDir(filepath.Join(rootDir, defaultConfigDir), 0700); err != nil { + cmn.PanicSanity(err.Error()) + } + if err := cmn.EnsureDir(filepath.Join(rootDir, defaultDataDir), 0700); err != nil { cmn.PanicSanity(err.Error()) } - configFilePath := path.Join(rootDir, "config.toml") - genesisFilePath := path.Join(rootDir, "genesis.json") - privFilePath := path.Join(rootDir, "priv_validator.json") + baseConfig := DefaultBaseConfig() + configFilePath := filepath.Join(rootDir, defaultConfigFilePath) + genesisFilePath := filepath.Join(rootDir, baseConfig.Genesis) + privFilePath := filepath.Join(rootDir, baseConfig.PrivValidator) // Write default config file if missing. if !cmn.FileExists(configFilePath) { - cmn.MustWriteFile(configFilePath, []byte(testConfig(defaultMoniker)), 0644) + writeConfigFile(configFilePath) } if !cmn.FileExists(genesisFilePath) { cmn.MustWriteFile(genesisFilePath, []byte(testGenesis), 0644) @@ -91,28 +267,6 @@ func ResetTestRoot(testName string) *Config { return config } -var testConfigTmpl = `# This is a TOML config file. -# For more information, see https://github.com/toml-lang/toml - -proxy_app = "dummy" -moniker = "__MONIKER__" -fast_sync = false -db_backend = "memdb" -log_level = "info" - -[rpc] -laddr = "tcp://0.0.0.0:36657" - -[p2p] -laddr = "tcp://0.0.0.0:36656" -seeds = "" -` - -func testConfig(moniker string) (testConfig string) { - testConfig = strings.Replace(testConfigTmpl, "__MONIKER__", moniker, -1) - return -} - var testGenesis = `{ "genesis_time": "0001-01-01T00:00:00.000Z", "chain_id": "tendermint_test", diff --git a/config/toml_test.go b/config/toml_test.go index f927a14ca..a1637f671 100644 --- a/config/toml_test.go +++ b/config/toml_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -19,7 +20,7 @@ func ensureFiles(t *testing.T, rootDir string, files ...string) { } func TestEnsureRoot(t *testing.T) { - assert, require := assert.New(t), require.New(t) + require := require.New(t) // setup temp dir for test tmpDir, err := ioutil.TempDir("", "config-test") @@ -30,15 +31,18 @@ func TestEnsureRoot(t *testing.T) { EnsureRoot(tmpDir) // make sure config is set properly - data, err := ioutil.ReadFile(filepath.Join(tmpDir, "config.toml")) + data, err := ioutil.ReadFile(filepath.Join(tmpDir, defaultConfigFilePath)) require.Nil(err) - assert.Equal([]byte(defaultConfig(defaultMoniker)), data) + + if !checkConfig(string(data)) { + t.Fatalf("config file missing some information") + } ensureFiles(t, tmpDir, "data") } func TestEnsureTestRoot(t *testing.T) { - assert, require := assert.New(t), require.New(t) + require := require.New(t) testName := "ensureTestRoot" @@ -47,11 +51,44 @@ func TestEnsureTestRoot(t *testing.T) { rootDir := cfg.RootDir // make sure config is set properly - data, err := ioutil.ReadFile(filepath.Join(rootDir, "config.toml")) + data, err := ioutil.ReadFile(filepath.Join(rootDir, defaultConfigFilePath)) require.Nil(err) - assert.Equal([]byte(testConfig(defaultMoniker)), data) + + if !checkConfig(string(data)) { + t.Fatalf("config file missing some information") + } // TODO: make sure the cfg returned and testconfig are the same! + baseConfig := DefaultBaseConfig() + ensureFiles(t, rootDir, defaultDataDir, baseConfig.Genesis, baseConfig.PrivValidator) +} + +func checkConfig(configFile string) bool { + var valid bool - ensureFiles(t, rootDir, "data", "genesis.json", "priv_validator.json") + // list of words we expect in the config + var elems = []string{ + "moniker", + "seeds", + "proxy_app", + "fast_sync", + "create_empty_blocks", + "peer", + "timeout", + "broadcast", + "send", + "addr", + "wal", + "propose", + "max", + "genesis", + } + for _, e := range elems { + if !strings.Contains(configFile, e) { + valid = false + } else { + valid = true + } + } + return valid } diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index 2f5f3f76c..38bb37474 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" crypto "github.com/tendermint/go-crypto" - data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" @@ -33,7 +32,9 @@ func TestByzantine(t *testing.T) { css := randConsensusNet(N, "consensus_byzantine_test", newMockTickerFunc(false), newCounter) // give the byzantine validator a normal ticker - css[0].SetTimeoutTicker(NewTimeoutTicker()) + ticker := NewTimeoutTicker() + ticker.SetLogger(css[0].Logger) + css[0].SetTimeoutTicker(ticker) switches := make([]*p2p.Switch, N) p2pLogger := logger.With("module", "p2p") @@ -279,7 +280,7 @@ func NewByzantinePrivValidator(pv types.PrivValidator) *ByzantinePrivValidator { } } -func (privVal *ByzantinePrivValidator) GetAddress() data.Bytes { +func (privVal *ByzantinePrivValidator) GetAddress() types.Address { return privVal.pv.GetAddress() } diff --git a/consensus/common_test.go b/consensus/common_test.go index 249e77329..c27b50c4f 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -36,8 +36,8 @@ const ( ) // genesis, chain_id, priv_val -var config *cfg.Config // NOTE: must be reset for each _test.go file -var ensureTimeout = time.Second * 2 +var config *cfg.Config // NOTE: must be reset for each _test.go file +var ensureTimeout = time.Second * 1 // must be in seconds because CreateEmptyBlocksInterval is func ensureDir(dir string, mode os.FileMode) { if err := cmn.EnsureDir(dir, mode); err != nil { @@ -78,7 +78,7 @@ func (vs *validatorStub) signVote(voteType byte, hash []byte, header types.PartS Type: voteType, BlockID: types.BlockID{hash, header}, } - err := vs.PrivValidator.SignVote(config.ChainID, vote) + err := vs.PrivValidator.SignVote(config.ChainID(), vote) return vote, err } @@ -129,7 +129,7 @@ func decideProposal(cs1 *ConsensusState, vs *validatorStub, height int64, round // Make proposal polRound, polBlockID := cs1.Votes.POLInfo() proposal = types.NewProposal(height, round, blockParts.Header(), polRound, polBlockID) - if err := vs.SignProposal(config.ChainID, proposal); err != nil { + if err := vs.SignProposal(cs1.state.ChainID, proposal); err != nil { panic(err) } return @@ -267,7 +267,7 @@ func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.S stateDB := dbm.NewMemDB() blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyAppConnCon, mempool, evpool) cs := NewConsensusState(thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool) - cs.SetLogger(log.TestingLogger()) + cs.SetLogger(log.TestingLogger().With("module", "consensus")) cs.SetPrivValidator(pv) eventBus := types.NewEventBus() @@ -285,14 +285,6 @@ func loadPrivValidator(config *cfg.Config) *types.PrivValidatorFS { return privValidator } -func fixedConsensusStateDummy(config *cfg.Config, logger log.Logger) *ConsensusState { - state, _ := sm.MakeGenesisStateFromFile(config.GenesisFile()) - privValidator := loadPrivValidator(config) - cs := newConsensusState(state, privValidator, dummy.NewDummyApplication()) - cs.SetLogger(logger) - return cs -} - func randConsensusState(nValidators int) (*ConsensusState, []*validatorStub) { // Get State state, privVals := randGenesisState(nValidators, false, 10) @@ -300,7 +292,6 @@ func randConsensusState(nValidators int) (*ConsensusState, []*validatorStub) { vss := make([]*validatorStub, nValidators) cs := newConsensusState(state, privVals[0], counter.NewCounterApplication(true)) - cs.SetLogger(log.TestingLogger()) for i := 0; i < nValidators; i++ { vss[i] = NewValidatorStub(privVals[i], i) @@ -346,7 +337,7 @@ func consensusLogger() log.Logger { } } return term.FgBgColor{} - }) + }).With("module", "consensus") } func randConsensusNet(nValidators int, testName string, tickerFunc func() TimeoutTicker, appFunc func() abci.Application, configOpts ...func(*cfg.Config)) []*ConsensusState { @@ -366,8 +357,8 @@ func randConsensusNet(nValidators int, testName string, tickerFunc func() Timeou app.InitChain(abci.RequestInitChain{Validators: vals}) css[i] = newConsensusStateWithConfig(thisConfig, state, privVals[i], app) - css[i].SetLogger(logger.With("validator", i)) css[i].SetTimeoutTicker(tickerFunc()) + css[i].SetLogger(logger.With("validator", i, "module", "consensus")) } return css } @@ -395,8 +386,8 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF app.InitChain(abci.RequestInitChain{Validators: vals}) css[i] = newConsensusStateWithConfig(thisConfig, state, privVal, app) - css[i].SetLogger(logger.With("validator", i)) css[i].SetTimeoutTicker(tickerFunc()) + css[i].SetLogger(logger.With("validator", i, "module", "consensus")) } return css } @@ -426,9 +417,10 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G privValidators[i] = privVal } sort.Sort(types.PrivValidatorsByAddress(privValidators)) + return &types.GenesisDoc{ GenesisTime: time.Now(), - ChainID: config.ChainID, + ChainID: config.ChainID(), Validators: validators, }, privValidators } diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index 91acce65d..d1714a741 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -19,7 +19,7 @@ func init() { config = ResetConfig("consensus_mempool_test") } -func TestNoProgressUntilTxsAvailable(t *testing.T) { +func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") config.Consensus.CreateEmptyBlocks = false state, privVals := randGenesisState(1, false, 10) @@ -37,7 +37,7 @@ func TestNoProgressUntilTxsAvailable(t *testing.T) { ensureNoNewStep(newBlockCh) } -func TestProgressAfterCreateEmptyBlocksInterval(t *testing.T) { +func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") config.Consensus.CreateEmptyBlocksInterval = int(ensureTimeout.Seconds()) state, privVals := randGenesisState(1, false, 10) @@ -52,7 +52,7 @@ func TestProgressAfterCreateEmptyBlocksInterval(t *testing.T) { ensureNewStep(newBlockCh) // until the CreateEmptyBlocksInterval has passed } -func TestProgressInHigherRound(t *testing.T) { +func TestMempoolProgressInHigherRound(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") config.Consensus.CreateEmptyBlocks = false state, privVals := randGenesisState(1, false, 10) @@ -94,7 +94,7 @@ func deliverTxsRange(cs *ConsensusState, start, end int) { } } -func TestTxConcurrentWithCommit(t *testing.T) { +func TestMempoolTxConcurrentWithCommit(t *testing.T) { state, privVals := randGenesisState(1, false, 10) cs := newConsensusState(state, privVals[0], NewCounterApplication()) height, round := cs.Height, cs.Round @@ -116,7 +116,7 @@ func TestTxConcurrentWithCommit(t *testing.T) { } } -func TestRmBadTx(t *testing.T) { +func TestMempoolRmBadTx(t *testing.T) { state, privVals := randGenesisState(1, false, 10) app := NewCounterApplication() cs := newConsensusState(state, privVals[0], app) @@ -129,7 +129,7 @@ func TestRmBadTx(t *testing.T) { assert.False(t, resDeliver.IsErr(), cmn.Fmt("expected no error. got %v", resDeliver)) resCommit := app.Commit() - assert.False(t, resCommit.IsErr(), cmn.Fmt("expected no error. got %v", resCommit)) + assert.True(t, len(resCommit.Data) > 0) emptyMempoolCh := make(chan struct{}) checkTxRespCh := make(chan struct{}) @@ -223,10 +223,10 @@ func txAsUint64(tx []byte) uint64 { func (app *CounterApplication) Commit() abci.ResponseCommit { app.mempoolTxCount = app.txCount if app.txCount == 0 { - return abci.ResponseCommit{Code: code.CodeTypeOK} + return abci.ResponseCommit{} } else { hash := make([]byte, 8) binary.BigEndian.PutUint64(hash, uint64(app.txCount)) - return abci.ResponseCommit{Code: code.CodeTypeOK, Data: hash} + return abci.ResponseCommit{Data: hash} } } diff --git a/consensus/reactor.go b/consensus/reactor.go index 9b3393e94..b63793670 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -205,7 +205,11 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) return } // Peer claims to have a maj23 for some BlockID at H,R,S, - votes.SetPeerMaj23(msg.Round, msg.Type, ps.Peer.Key(), msg.BlockID) + err := votes.SetPeerMaj23(msg.Round, msg.Type, ps.Peer.ID(), msg.BlockID) + if err != nil { + conR.Switch.StopPeerForError(src, err) + return + } // Respond with a VoteSetBitsMessage showing which votes we have. // (and consequently shows which we don't have) var ourVotes *cmn.BitArray @@ -242,12 +246,12 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) switch msg := msg.(type) { case *ProposalMessage: ps.SetHasProposal(msg.Proposal) - conR.conS.peerMsgQueue <- msgInfo{msg, src.Key()} + conR.conS.peerMsgQueue <- msgInfo{msg, src.ID()} case *ProposalPOLMessage: ps.ApplyProposalPOLMessage(msg) case *BlockPartMessage: ps.SetHasProposalBlockPart(msg.Height, msg.Round, msg.Part.Index) - conR.conS.peerMsgQueue <- msgInfo{msg, src.Key()} + conR.conS.peerMsgQueue <- msgInfo{msg, src.ID()} default: conR.Logger.Error(cmn.Fmt("Unknown message type %v", reflect.TypeOf(msg))) } @@ -267,7 +271,7 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) ps.EnsureVoteBitArrays(height-1, lastCommitSize) ps.SetHasVote(msg.Vote) - cs.peerMsgQueue <- msgInfo{msg, src.Key()} + cs.peerMsgQueue <- msgInfo{msg, src.ID()} default: // don't punish (leave room for soft upgrades) @@ -376,7 +380,7 @@ func (conR *ConsensusReactor) startBroadcastRoutine() error { edph := data.(types.TMEventData).Unwrap().(types.EventDataProposalHeartbeat) conR.broadcastProposalHeartbeatMessage(edph) } - case <-conR.Quit: + case <-conR.Quit(): conR.eventBus.UnsubscribeAll(ctx, subscriber) return } @@ -1200,7 +1204,7 @@ func (ps *PeerState) StringIndented(indent string) string { %s Key %v %s PRS %v %s}`, - indent, ps.Peer.Key(), + indent, ps.Peer.ID(), indent, ps.PeerRoundState.StringIndented(indent+" "), indent) } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index bcf97f94b..f66f4e9e5 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "runtime" "runtime/pprof" "sync" "testing" @@ -31,31 +32,24 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ([]*Consensus reactors := make([]*ConsensusReactor, N) eventChans := make([]chan interface{}, N) eventBuses := make([]*types.EventBus, N) - logger := consensusLogger() for i := 0; i < N; i++ { - /*thisLogger, err := tmflags.ParseLogLevel("consensus:info,*:error", logger, "info") + /*logger, err := tmflags.ParseLogLevel("consensus:info,*:error", logger, "info") if err != nil { t.Fatal(err)}*/ - thisLogger := logger - reactors[i] = NewConsensusReactor(css[i], true) // so we dont start the consensus states - reactors[i].conS.SetLogger(thisLogger.With("validator", i)) - reactors[i].SetLogger(thisLogger.With("validator", i)) - - eventBuses[i] = types.NewEventBus() - eventBuses[i].SetLogger(thisLogger.With("module", "events", "validator", i)) - err := eventBuses[i].Start() - require.NoError(t, err) + reactors[i].SetLogger(css[i].Logger) + // eventBus is already started with the cs + eventBuses[i] = css[i].eventBus reactors[i].SetEventBus(eventBuses[i]) eventChans[i] = make(chan interface{}, 1) - err = eventBuses[i].Subscribe(context.Background(), testSubscriber, types.EventQueryNewBlock, eventChans[i]) + err := eventBuses[i].Subscribe(context.Background(), testSubscriber, types.EventQueryNewBlock, eventChans[i]) require.NoError(t, err) } // make connected switches and start all reactors p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("CONSENSUS", reactors[i]) - s.SetLogger(reactors[i].Logger.With("module", "p2p", "validator", i)) + s.SetLogger(reactors[i].conS.Logger.With("module", "p2p")) return s }, p2p.Connect2Switches) @@ -84,15 +78,14 @@ func stopConsensusNet(logger log.Logger, reactors []*ConsensusReactor, eventBuse } // Ensure a testnet makes blocks -func TestReactor(t *testing.T) { +func TestReactorBasic(t *testing.T) { N := 4 css := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) reactors, eventChans, eventBuses := startConsensusNet(t, css, N) defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) // wait till everyone makes the first new block - timeoutWaitGroup(t, N, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, N, func(j int) { <-eventChans[j] - wg.Done() }, css) } @@ -113,9 +106,8 @@ func TestReactorProposalHeartbeats(t *testing.T) { require.NoError(t, err) } // wait till everyone sends a proposal heartbeat - timeoutWaitGroup(t, N, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, N, func(j int) { <-heartbeatChans[j] - wg.Done() }, css) // send a tx @@ -124,9 +116,8 @@ func TestReactorProposalHeartbeats(t *testing.T) { } // wait till everyone makes the first new block - timeoutWaitGroup(t, N, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, N, func(j int) { <-eventChans[j] - wg.Done() }, css) } @@ -147,9 +138,8 @@ func TestReactorVotingPowerChange(t *testing.T) { } // wait till everyone makes block 1 - timeoutWaitGroup(t, nVals, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, nVals, func(j int) { <-eventChans[j] - wg.Done() }, css) //--------------------------------------------------------------------------- @@ -210,9 +200,8 @@ func TestReactorValidatorSetChanges(t *testing.T) { } // wait till everyone makes block 1 - timeoutWaitGroup(t, nPeers, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, nPeers, func(j int) { <-eventChans[j] - wg.Done() }, css) //--------------------------------------------------------------------------- @@ -300,16 +289,13 @@ func TestReactorWithTimeoutCommit(t *testing.T) { defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) // wait till everyone makes the first new block - timeoutWaitGroup(t, N-1, func(wg *sync.WaitGroup, j int) { + timeoutWaitGroup(t, N-1, func(j int) { <-eventChans[j] - wg.Done() }, css) } func waitForAndValidateBlock(t *testing.T, n int, activeVals map[string]struct{}, eventChans []chan interface{}, css []*ConsensusState, txs ...[]byte) { - timeoutWaitGroup(t, n, func(wg *sync.WaitGroup, j int) { - defer wg.Done() - + timeoutWaitGroup(t, n, func(j int) { css[j].Logger.Debug("waitForAndValidateBlock") newBlockI, ok := <-eventChans[j] if !ok { @@ -327,8 +313,7 @@ func waitForAndValidateBlock(t *testing.T, n int, activeVals map[string]struct{} } func waitForAndValidateBlockWithTx(t *testing.T, n int, activeVals map[string]struct{}, eventChans []chan interface{}, css []*ConsensusState, txs ...[]byte) { - timeoutWaitGroup(t, n, func(wg *sync.WaitGroup, j int) { - defer wg.Done() + timeoutWaitGroup(t, n, func(j int) { ntxs := 0 BLOCK_TX_LOOP: for { @@ -359,8 +344,7 @@ func waitForAndValidateBlockWithTx(t *testing.T, n int, activeVals map[string]st } func waitForBlockWithUpdatedValsAndValidateIt(t *testing.T, n int, updatedVals map[string]struct{}, eventChans []chan interface{}, css []*ConsensusState) { - timeoutWaitGroup(t, n, func(wg *sync.WaitGroup, j int) { - defer wg.Done() + timeoutWaitGroup(t, n, func(j int) { var newBlock *types.Block LOOP: @@ -398,11 +382,14 @@ func validateBlock(block *types.Block, activeVals map[string]struct{}) error { return nil } -func timeoutWaitGroup(t *testing.T, n int, f func(*sync.WaitGroup, int), css []*ConsensusState) { +func timeoutWaitGroup(t *testing.T, n int, f func(int), css []*ConsensusState) { wg := new(sync.WaitGroup) wg.Add(n) for i := 0; i < n; i++ { - go f(wg, i) + go func(j int) { + f(j) + wg.Done() + }(i) } done := make(chan struct{}) @@ -424,7 +411,15 @@ func timeoutWaitGroup(t *testing.T, n int, f func(*sync.WaitGroup, int), css []* t.Log(cs.GetRoundState()) t.Log("") } + os.Stdout.Write([]byte("pprof.Lookup('goroutine'):\n")) pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + capture() panic("Timed out waiting for all validators to commit a block") } } + +func capture() { + trace := make([]byte, 10240000) + count := runtime.Stack(trace, true) + fmt.Printf("Stack of %d bytes: %s\n", count, trace) +} diff --git a/consensus/replay.go b/consensus/replay.go index 784e8bd6e..2e6b36a22 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -61,21 +61,21 @@ func (cs *ConsensusState) readReplayMessage(msg *TimedWALMessage, newStepCh chan } } case msgInfo: - peerKey := m.PeerKey - if peerKey == "" { - peerKey = "local" + peerID := m.PeerID + if peerID == "" { + peerID = "local" } switch msg := m.Msg.(type) { case *ProposalMessage: p := msg.Proposal cs.Logger.Info("Replay: Proposal", "height", p.Height, "round", p.Round, "header", - p.BlockPartsHeader, "pol", p.POLRound, "peer", peerKey) + p.BlockPartsHeader, "pol", p.POLRound, "peer", peerID) case *BlockPartMessage: - cs.Logger.Info("Replay: BlockPart", "height", msg.Height, "round", msg.Round, "peer", peerKey) + cs.Logger.Info("Replay: BlockPart", "height", msg.Height, "round", msg.Round, "peer", peerID) case *VoteMessage: v := msg.Vote cs.Logger.Info("Replay: Vote", "height", v.Height, "round", v.Round, "type", v.Type, - "blockID", v.BlockID, "peer", peerKey) + "blockID", v.BlockID, "peer", peerID) } cs.handleMsg(m) @@ -249,7 +249,9 @@ func (h *Handshaker) ReplayBlocks(state sm.State, appHash []byte, appBlockHeight // If appBlockHeight == 0 it means that we are at genesis and hence should send InitChain if appBlockHeight == 0 { validators := types.TM2PB.Validators(state.Validators) - if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators}); err != nil { + // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) + var genesisBytes []byte + if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { return nil, err } } @@ -423,5 +425,5 @@ func (mock *mockProxyApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlo } func (mock *mockProxyApp) Commit() abci.ResponseCommit { - return abci.ResponseCommit{Code: abci.CodeTypeOK, Data: mock.appHash} + return abci.ResponseCommit{Data: mock.appHash} } diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 26b8baebf..979425f87 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -280,12 +280,13 @@ func (pb *playback) replayConsoleLoop() int { // convenience for replay mode func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusConfig) *ConsensusState { + dbType := dbm.DBBackendType(config.DBBackend) // Get BlockStore - blockStoreDB := dbm.NewDB("blockstore", config.DBBackend, config.DBDir()) + blockStoreDB := dbm.NewDB("blockstore", dbType, config.DBDir()) blockStore := bc.NewBlockStore(blockStoreDB) // Get State - stateDB := dbm.NewDB("state", config.DBBackend, config.DBDir()) + stateDB := dbm.NewDB("state", dbType, config.DBDir()) state, err := sm.MakeGenesisStateFromFile(config.GenesisFile()) if err != nil { cmn.Exit(err.Error()) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 242552627..31fb9908f 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -81,13 +81,13 @@ func startNewConsensusStateAndWaitForBlock(t *testing.T, lastBlockHeight int64, } func sendTxs(cs *ConsensusState, ctx context.Context) { - i := 0 - for { + for i := 0; i < 256; i++ { select { case <-ctx.Done(): return default: - cs.mempool.CheckTx([]byte{byte(i)}, nil) + tx := []byte{byte(i)} + cs.mempool.CheckTx(tx, nil) i++ } } @@ -413,7 +413,9 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, defer proxyApp.Stop() validators := types.TM2PB.Validators(state.Validators) - if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators}); err != nil { + // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) + var genesisBytes []byte + if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { panic(err) } @@ -448,7 +450,9 @@ func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, c defer proxyApp.Stop() validators := types.TM2PB.Validators(state.Validators) - if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators}); err != nil { + // TODO: get the genesis bytes (https://github.com/tendermint/tendermint/issues/1224) + var genesisBytes []byte + if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{validators, genesisBytes}); err != nil { panic(err) } diff --git a/consensus/state.go b/consensus/state.go index 518d81c58..aa334fdd0 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -17,6 +17,7 @@ import ( cfg "github.com/tendermint/tendermint/config" cstypes "github.com/tendermint/tendermint/consensus/types" + "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -46,8 +47,8 @@ var ( // msgs from the reactor which may update the state type msgInfo struct { - Msg ConsensusMessage `json:"msg"` - PeerKey string `json:"peer_key"` + Msg ConsensusMessage `json:"msg"` + PeerID p2p.ID `json:"peer_key"` } // internally generated messages which may update the state @@ -85,7 +86,7 @@ type ConsensusState struct { cstypes.RoundState state sm.State // State until height-1. - // state changes may be triggered by msgs from peers, + // state changes may be triggered by: msgs from peers, // msgs from ourself, or by timeouts peerMsgQueue chan msgInfo internalMsgQueue chan msgInfo @@ -303,17 +304,17 @@ func (cs *ConsensusState) OpenWAL(walFile string) (WAL, error) { //------------------------------------------------------------ // Public interface for passing messages into the consensus state, possibly causing a state transition. -// If peerKey == "", the msg is considered internal. +// If peerID == "", the msg is considered internal. // Messages are added to the appropriate queue (peer or internal). // If the queue is full, the function may block. // TODO: should these return anything or let callers just use events? // AddVote inputs a vote. -func (cs *ConsensusState) AddVote(vote *types.Vote, peerKey string) (added bool, err error) { - if peerKey == "" { +func (cs *ConsensusState) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { + if peerID == "" { cs.internalMsgQueue <- msgInfo{&VoteMessage{vote}, ""} } else { - cs.peerMsgQueue <- msgInfo{&VoteMessage{vote}, peerKey} + cs.peerMsgQueue <- msgInfo{&VoteMessage{vote}, peerID} } // TODO: wait for event?! @@ -321,12 +322,12 @@ func (cs *ConsensusState) AddVote(vote *types.Vote, peerKey string) (added bool, } // SetProposal inputs a proposal. -func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerKey string) error { +func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerID p2p.ID) error { - if peerKey == "" { + if peerID == "" { cs.internalMsgQueue <- msgInfo{&ProposalMessage{proposal}, ""} } else { - cs.peerMsgQueue <- msgInfo{&ProposalMessage{proposal}, peerKey} + cs.peerMsgQueue <- msgInfo{&ProposalMessage{proposal}, peerID} } // TODO: wait for event?! @@ -334,12 +335,12 @@ func (cs *ConsensusState) SetProposal(proposal *types.Proposal, peerKey string) } // AddProposalBlockPart inputs a part of the proposal block. -func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *types.Part, peerKey string) error { +func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *types.Part, peerID p2p.ID) error { - if peerKey == "" { + if peerID == "" { cs.internalMsgQueue <- msgInfo{&BlockPartMessage{height, round, part}, ""} } else { - cs.peerMsgQueue <- msgInfo{&BlockPartMessage{height, round, part}, peerKey} + cs.peerMsgQueue <- msgInfo{&BlockPartMessage{height, round, part}, peerID} } // TODO: wait for event?! @@ -347,13 +348,13 @@ func (cs *ConsensusState) AddProposalBlockPart(height int64, round int, part *ty } // SetProposalAndBlock inputs the proposal and all block parts. -func (cs *ConsensusState) SetProposalAndBlock(proposal *types.Proposal, block *types.Block, parts *types.PartSet, peerKey string) error { - if err := cs.SetProposal(proposal, peerKey); err != nil { +func (cs *ConsensusState) SetProposalAndBlock(proposal *types.Proposal, block *types.Block, parts *types.PartSet, peerID p2p.ID) error { + if err := cs.SetProposal(proposal, peerID); err != nil { return err } for i := 0; i < parts.Total(); i++ { part := parts.GetPart(i) - if err := cs.AddProposalBlockPart(proposal.Height, proposal.Round, part, peerKey); err != nil { + if err := cs.AddProposalBlockPart(proposal.Height, proposal.Round, part, peerID); err != nil { return err } } @@ -540,7 +541,7 @@ func (cs *ConsensusState) receiveRoutine(maxSteps int) { // if the timeout is relevant to the rs // go to the next step cs.handleTimeout(ti, rs) - case <-cs.Quit: + case <-cs.Quit(): // NOTE: the internalMsgQueue may have signed messages from our // priv_val that haven't hit the WAL, but its ok because @@ -561,7 +562,7 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { defer cs.mtx.Unlock() var err error - msg, peerKey := mi.Msg, mi.PeerKey + msg, peerID := mi.Msg, mi.PeerID switch msg := msg.(type) { case *ProposalMessage: // will not cause transition. @@ -569,14 +570,14 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { err = cs.setProposal(msg.Proposal) case *BlockPartMessage: // if the proposal is complete, we'll enterPrevote or tryFinalizeCommit - _, err = cs.addProposalBlockPart(msg.Height, msg.Part, peerKey != "") + _, err = cs.addProposalBlockPart(msg.Height, msg.Part, peerID != "") if err != nil && msg.Round != cs.Round { err = nil } case *VoteMessage: // attempt to add the vote and dupeout the validator if its a duplicate signature // if the vote gives us a 2/3-any or 2/3-one, we transition - err := cs.tryAddVote(msg.Vote, peerKey) + err := cs.tryAddVote(msg.Vote, peerID) if err == ErrAddingVote { // TODO: punish peer } @@ -591,7 +592,7 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { cs.Logger.Error("Unknown msg type", reflect.TypeOf(msg)) } if err != nil { - cs.Logger.Error("Error with msg", "type", reflect.TypeOf(msg), "peer", peerKey, "err", err, "msg", msg) + cs.Logger.Error("Error with msg", "type", reflect.TypeOf(msg), "peer", peerID, "err", err, "msg", msg) } } @@ -770,17 +771,18 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { return } - if !cs.isProposer() { - cs.Logger.Info("enterPropose: Not our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) - if cs.Validators.HasAddress(cs.privValidator.GetAddress()) { - cs.Logger.Debug("This node is a validator") - } else { - cs.Logger.Debug("This node is not a validator") - } - } else { + // if not a validator, we're done + if !cs.Validators.HasAddress(cs.privValidator.GetAddress()) { + cs.Logger.Debug("This node is not a validator") + return + } + cs.Logger.Debug("This node is a validator") + + if cs.isProposer() { cs.Logger.Info("enterPropose: Our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) - cs.Logger.Debug("This node is a validator") cs.decideProposal(height, round) + } else { + cs.Logger.Info("enterPropose: Not our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) } } @@ -1308,8 +1310,8 @@ func (cs *ConsensusState) addProposalBlockPart(height int64, part *types.Part, v } // Attempt to add the vote. if its a duplicate signature, dupeout the validator -func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerKey string) error { - _, err := cs.addVote(vote, peerKey) +func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) error { + _, err := cs.addVote(vote, peerID) if err != nil { // If the vote height is off, we'll just ignore it, // But if it's a conflicting sig, add it to the cs.evpool. @@ -1335,7 +1337,7 @@ func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerKey string) error { //----------------------------------------------------------------------------- -func (cs *ConsensusState) addVote(vote *types.Vote, peerKey string) (added bool, err error) { +func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { cs.Logger.Debug("addVote", "voteHeight", vote.Height, "voteType", vote.Type, "valIndex", vote.ValidatorIndex, "csHeight", cs.Height) // A precommit for the previous height? @@ -1365,7 +1367,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerKey string) (added bool, // A prevote/precommit for this height? if vote.Height == cs.Height { height := cs.Height - added, err = cs.Votes.AddVote(vote, peerKey) + added, err = cs.Votes.AddVote(vote, peerID) if added { cs.eventBus.PublishEventVote(types.EventDataVote{vote}) diff --git a/consensus/state_test.go b/consensus/state_test.go index 6beb7da54..e6b2a1354 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -55,7 +55,7 @@ x * TestHalt1 - if we see +2/3 precommits after timing out into new round, we sh //---------------------------------------------------------------------------------------------------- // ProposeSuite -func TestProposerSelection0(t *testing.T) { +func TestStateProposerSelection0(t *testing.T) { cs1, vss := randConsensusState(4) height, round := cs1.Height, cs1.Round @@ -89,7 +89,7 @@ func TestProposerSelection0(t *testing.T) { } // Now let's do it all again, but starting from round 2 instead of 0 -func TestProposerSelection2(t *testing.T) { +func TestStateProposerSelection2(t *testing.T) { cs1, vss := randConsensusState(4) // test needs more work for more than 3 validators newRoundCh := subscribe(cs1.eventBus, types.EventQueryNewRound) @@ -118,7 +118,7 @@ func TestProposerSelection2(t *testing.T) { } // a non-validator should timeout into the prevote round -func TestEnterProposeNoPrivValidator(t *testing.T) { +func TestStateEnterProposeNoPrivValidator(t *testing.T) { cs, _ := randConsensusState(1) cs.SetPrivValidator(nil) height, round := cs.Height, cs.Round @@ -143,7 +143,7 @@ func TestEnterProposeNoPrivValidator(t *testing.T) { } // a validator should not timeout of the prevote round (TODO: unless the block is really big!) -func TestEnterProposeYesPrivValidator(t *testing.T) { +func TestStateEnterProposeYesPrivValidator(t *testing.T) { cs, _ := randConsensusState(1) height, round := cs.Height, cs.Round @@ -179,7 +179,7 @@ func TestEnterProposeYesPrivValidator(t *testing.T) { } } -func TestBadProposal(t *testing.T) { +func TestStateBadProposal(t *testing.T) { cs1, vss := randConsensusState(2) height, round := cs1.Height, cs1.Round vs2 := vss[1] @@ -204,7 +204,7 @@ func TestBadProposal(t *testing.T) { propBlock.AppHash = stateHash propBlockParts := propBlock.MakePartSet(partSize) proposal := types.NewProposal(vs2.Height, round, propBlockParts.Header(), -1, types.BlockID{}) - if err := vs2.SignProposal(config.ChainID, proposal); err != nil { + if err := vs2.SignProposal(config.ChainID(), proposal); err != nil { t.Fatal("failed to sign bad proposal", err) } @@ -239,7 +239,7 @@ func TestBadProposal(t *testing.T) { // FullRoundSuite // propose, prevote, and precommit a block -func TestFullRound1(t *testing.T) { +func TestStateFullRound1(t *testing.T) { cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round @@ -275,7 +275,7 @@ func TestFullRound1(t *testing.T) { } // nil is proposed, so prevote and precommit nil -func TestFullRoundNil(t *testing.T) { +func TestStateFullRoundNil(t *testing.T) { cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round @@ -293,7 +293,7 @@ func TestFullRoundNil(t *testing.T) { // run through propose, prevote, precommit commit with two validators // where the first validator has to wait for votes from the second -func TestFullRound2(t *testing.T) { +func TestStateFullRound2(t *testing.T) { cs1, vss := randConsensusState(2) vs2 := vss[1] height, round := cs1.Height, cs1.Round @@ -334,7 +334,7 @@ func TestFullRound2(t *testing.T) { // two validators, 4 rounds. // two vals take turns proposing. val1 locks on first one, precommits nil on everything else -func TestLockNoPOL(t *testing.T) { +func TestStateLockNoPOL(t *testing.T) { cs1, vss := randConsensusState(2) vs2 := vss[1] height := cs1.Height @@ -503,7 +503,7 @@ func TestLockNoPOL(t *testing.T) { } // 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka -func TestLockPOLRelock(t *testing.T) { +func TestStateLockPOLRelock(t *testing.T) { cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] @@ -618,7 +618,7 @@ func TestLockPOLRelock(t *testing.T) { } // 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka -func TestLockPOLUnlock(t *testing.T) { +func TestStateLockPOLUnlock(t *testing.T) { cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] @@ -715,7 +715,7 @@ func TestLockPOLUnlock(t *testing.T) { // a polka at round 1 but we miss it // then a polka at round 2 that we lock on // then we see the polka from round 1 but shouldn't unlock -func TestLockPOLSafety1(t *testing.T) { +func TestStateLockPOLSafety1(t *testing.T) { cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] @@ -838,7 +838,7 @@ func TestLockPOLSafety1(t *testing.T) { // What we want: // dont see P0, lock on P1 at R1, dont unlock using P0 at R2 -func TestLockPOLSafety2(t *testing.T) { +func TestStateLockPOLSafety2(t *testing.T) { cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] @@ -900,7 +900,7 @@ func TestLockPOLSafety2(t *testing.T) { // in round 2 we see the polkad block from round 0 newProp := types.NewProposal(height, 2, propBlockParts0.Header(), 0, propBlockID1) - if err := vs3.SignProposal(config.ChainID, newProp); err != nil { + if err := vs3.SignProposal(config.ChainID(), newProp); err != nil { t.Fatal(err) } if err := cs1.SetProposalAndBlock(newProp, propBlock0, propBlockParts0, "some peer"); err != nil { @@ -937,7 +937,7 @@ func TestLockPOLSafety2(t *testing.T) { // TODO: Slashing /* -func TestSlashingPrevotes(t *testing.T) { +func TestStateSlashingPrevotes(t *testing.T) { cs1, vss := randConsensusState(2) vs2 := vss[1] @@ -972,7 +972,7 @@ func TestSlashingPrevotes(t *testing.T) { // XXX: Check for existence of Dupeout info } -func TestSlashingPrecommits(t *testing.T) { +func TestStateSlashingPrecommits(t *testing.T) { cs1, vss := randConsensusState(2) vs2 := vss[1] @@ -1017,7 +1017,7 @@ func TestSlashingPrecommits(t *testing.T) { // 4 vals. // we receive a final precommit after going into next round, but others might have gone to commit already! -func TestHalt1(t *testing.T) { +func TestStateHalt1(t *testing.T) { cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] diff --git a/consensus/ticker.go b/consensus/ticker.go index f66856f91..b37b7c495 100644 --- a/consensus/ticker.go +++ b/consensus/ticker.go @@ -127,7 +127,7 @@ func (t *timeoutTicker) timeoutRoutine() { // We can eliminate it by merging the timeoutRoutine into receiveRoutine // and managing the timeouts ourselves with a millisecond ticker go func(toi timeoutInfo) { t.tockChan <- toi }(ti) - case <-t.Quit: + case <-t.Quit(): return } } diff --git a/consensus/types/height_vote_set.go b/consensus/types/height_vote_set.go index 0a0a25fea..17ef334db 100644 --- a/consensus/types/height_vote_set.go +++ b/consensus/types/height_vote_set.go @@ -1,9 +1,11 @@ package types import ( + "fmt" "strings" "sync" + "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" ) @@ -35,7 +37,7 @@ type HeightVoteSet struct { mtx sync.Mutex round int // max tracked round roundVoteSets map[int]RoundVoteSet // keys: [0...round] - peerCatchupRounds map[string][]int // keys: peer.Key; values: at most 2 rounds + peerCatchupRounds map[p2p.ID][]int // keys: peer.ID; values: at most 2 rounds } func NewHeightVoteSet(chainID string, height int64, valSet *types.ValidatorSet) *HeightVoteSet { @@ -53,7 +55,7 @@ func (hvs *HeightVoteSet) Reset(height int64, valSet *types.ValidatorSet) { hvs.height = height hvs.valSet = valSet hvs.roundVoteSets = make(map[int]RoundVoteSet) - hvs.peerCatchupRounds = make(map[string][]int) + hvs.peerCatchupRounds = make(map[p2p.ID][]int) hvs.addRound(0) hvs.round = 0 @@ -101,8 +103,8 @@ func (hvs *HeightVoteSet) addRound(round int) { } // Duplicate votes return added=false, err=nil. -// By convention, peerKey is "" if origin is self. -func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerKey string) (added bool, err error) { +// By convention, peerID is "" if origin is self. +func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerID p2p.ID) (added bool, err error) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(vote.Type) { @@ -110,10 +112,10 @@ func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerKey string) (added bool, } voteSet := hvs.getVoteSet(vote.Round, vote.Type) if voteSet == nil { - if rndz := hvs.peerCatchupRounds[peerKey]; len(rndz) < 2 { + if rndz := hvs.peerCatchupRounds[peerID]; len(rndz) < 2 { hvs.addRound(vote.Round) voteSet = hvs.getVoteSet(vote.Round, vote.Type) - hvs.peerCatchupRounds[peerKey] = append(rndz, vote.Round) + hvs.peerCatchupRounds[peerID] = append(rndz, vote.Round) } else { // Peer has sent a vote that does not match our round, // for more than one round. Bad peer! @@ -206,15 +208,15 @@ func (hvs *HeightVoteSet) StringIndented(indent string) string { // NOTE: if there are too many peers, or too much peer churn, // this can cause memory issues. // TODO: implement ability to remove peers too -func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID string, blockID types.BlockID) { +func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID p2p.ID, blockID types.BlockID) error { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(type_) { - return + return fmt.Errorf("SetPeerMaj23: Invalid vote type %v", type_) } voteSet := hvs.getVoteSet(round, type_) if voteSet == nil { - return + return nil // something we don't know about yet } - voteSet.SetPeerMaj23(peerID, blockID) + return voteSet.SetPeerMaj23(peerID, blockID) } diff --git a/consensus/types/height_vote_set_test.go b/consensus/types/height_vote_set_test.go index 306592aa2..5719d7eea 100644 --- a/consensus/types/height_vote_set_test.go +++ b/consensus/types/height_vote_set_test.go @@ -18,7 +18,7 @@ func init() { func TestPeerCatchupRounds(t *testing.T) { valSet, privVals := types.RandValidatorSet(10, 1) - hvs := NewHeightVoteSet(config.ChainID, 1, valSet) + hvs := NewHeightVoteSet(config.ChainID(), 1, valSet) vote999_0 := makeVoteHR(t, 1, 999, privVals, 0) added, err := hvs.AddVote(vote999_0, "peer1") @@ -59,7 +59,7 @@ func makeVoteHR(t *testing.T, height int64, round int, privVals []*types.PrivVal Type: types.VoteTypePrecommit, BlockID: types.BlockID{[]byte("fakehash"), types.PartSetHeader{}}, } - chainID := config.ChainID + chainID := config.ChainID() err := privVal.SignVote(chainID, vote) if err != nil { panic(cmn.Fmt("Error signing vote: %v", err)) diff --git a/consensus/wal.go b/consensus/wal.go index dfbef8790..88218940d 100644 --- a/consensus/wal.go +++ b/consensus/wal.go @@ -121,7 +121,7 @@ func (wal *baseWAL) Save(msg WALMessage) { if wal.light { // in light mode we only write new steps, timeouts, and our own votes (no proposals, block parts) if mi, ok := msg.(msgInfo); ok { - if mi.PeerKey != "" { + if mi.PeerID != "" { return } } diff --git a/consensus/wal_test.go b/consensus/wal_test.go index c7f087398..3553591c9 100644 --- a/consensus/wal_test.go +++ b/consensus/wal_test.go @@ -41,7 +41,7 @@ func TestWALEncoderDecoder(t *testing.T) { } } -func TestSearchForEndHeight(t *testing.T) { +func TestWALSearchForEndHeight(t *testing.T) { walBody, err := WALWithNBlocks(6) if err != nil { t.Fatal(err) diff --git a/docs/.python-version b/docs/.python-version new file mode 100644 index 000000000..9bbf49249 --- /dev/null +++ b/docs/.python-version @@ -0,0 +1 @@ +2.7.14 diff --git a/docs/Makefile b/docs/Makefile index f8d1790de..442c9be65 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -12,6 +12,9 @@ BUILDDIR = _build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +install: + @pip install -r requirements.txt + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new diff --git a/docs/abci-cli.rst b/docs/abci-cli.rst index 9a5ba833c..efbeb71be 100644 --- a/docs/abci-cli.rst +++ b/docs/abci-cli.rst @@ -53,7 +53,7 @@ Now run ``abci-cli`` to see the list of commands: -h, --help help for abci-cli -v, --verbose print the command and results as if it were a console session - Use "abci-cli [command] --help" for more information about a command. + Use "abci-cli [command] --help" for more information about a command. Dummy - First Example @@ -66,14 +66,56 @@ The most important messages are ``deliver_tx``, ``check_tx``, and ``commit``, but there are others for convenience, configuration, and information purposes. -Let's start a dummy application, which was installed at the same time as -``abci-cli`` above. The dummy just stores transactions in a merkle tree: +We'll start a dummy application, which was installed at the same time as +``abci-cli`` above. The dummy just stores transactions in a merkle tree. + +Its code can be found `here `__ and looks like: + +.. container:: toggle + + .. container:: header + + **Show/Hide Dummy Example** + + .. code-block:: go + + func cmdDummy(cmd *cobra.Command, args []string) error { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + + // Create the application - in memory or persisted to disk + var app types.Application + if flagPersist == "" { + app = dummy.NewDummyApplication() + } else { + app = dummy.NewPersistentDummyApplication(flagPersist) + app.(*dummy.PersistentDummyApplication).SetLogger(logger.With("module", "dummy")) + } + + // Start the listener + srv, err := server.NewServer(flagAddrD, flagAbci, app) + if err != nil { + return err + } + srv.SetLogger(logger.With("module", "abci-server")) + if err := srv.Start(); err != nil { + return err + } + + // Wait forever + cmn.TrapSignal(func() { + // Cleanup + srv.Stop() + }) + return nil + } + +Start by running: :: abci-cli dummy -In another terminal, run +And in another terminal, run :: @@ -187,6 +229,41 @@ Counter - Another Example Now that we've got the hang of it, let's try another application, the "counter" app. +Like the dummy app, its code can be found `here `__ and looks like: + +.. container:: toggle + + .. container:: header + + **Show/Hide Counter Example** + + .. code-block:: go + + func cmdCounter(cmd *cobra.Command, args []string) error { + + app := counter.NewCounterApplication(flagSerial) + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + + // Start the listener + srv, err := server.NewServer(flagAddrC, flagAbci, app) + if err != nil { + return err + } + srv.SetLogger(logger.With("module", "abci-server")) + if err := srv.Start(); err != nil { + return err + } + + // Wait forever + cmn.TrapSignal(func() { + // Cleanup + srv.Stop() + }) + return nil + } + + The counter app doesn't use a Merkle tree, it just counts how many times we've sent a transaction, asked for a hash, or committed the state. The result of ``commit`` is just the number of transactions sent. @@ -261,7 +338,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). +`example directory `__. For examples of running an ABCI app with Tendermint, see the `getting started -guide <./getting-started.html>`__. +guide <./getting-started.html>`__. Next is the ABCI specification. diff --git a/docs/app-development.rst b/docs/app-development.rst index cbb39fa34..18477eda7 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -409,11 +409,10 @@ to update the validator set. To add a new validator or update an existing one, simply include them in the list returned in the EndBlock response. To remove one, include it in the list with a ``power`` equal to ``0``. Tendermint core will take care of updating the validator set. Note the change in voting power -must be strictly less than 1/3 per block. Otherwise it will be impossible for a -light client to prove the transition externally. See the `light client docs +must be strictly less than 1/3 per block if you want a light client to be able +to prove the transition externally. See the `light client docs `__ -for details on how it tracks validators. Tendermint core will fail with an -error if the change in voting power is more or equal than 1/3. +for details on how it tracks validators. .. container:: toggle diff --git a/docs/conf.py b/docs/conf.py index d5c49355f..92c5e1201 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -171,29 +171,38 @@ texinfo_documents = [ 'Database'), ] -repo = "https://raw.githubusercontent.com/tendermint/tools/" -branch = "master" +# ---- customization ------------------------- -tools = "./tools" -assets = tools + "/assets" +tools_repo = "https://raw.githubusercontent.com/tendermint/tools/" +tools_branch = "master" -if os.path.isdir(tools) != True: - os.mkdir(tools) -if os.path.isdir(assets) != True: - os.mkdir(assets) +tools_dir = "./tools" +assets_dir = tools_dir + "/assets" -urllib.urlretrieve(repo+branch+'/ansible/README.rst', filename=tools+'/ansible.rst') -urllib.urlretrieve(repo+branch+'/ansible/assets/a_plus_t.png', filename=assets+'/a_plus_t.png') +if os.path.isdir(tools_dir) != True: + os.mkdir(tools_dir) +if os.path.isdir(assets_dir) != True: + os.mkdir(assets_dir) -urllib.urlretrieve(repo+branch+'/docker/README.rst', filename=tools+'/docker.rst') +urllib.urlretrieve(tools_repo+tools_branch+'/ansible/README.rst', filename=tools_dir+'/ansible.rst') +urllib.urlretrieve(tools_repo+tools_branch+'/ansible/assets/a_plus_t.png', filename=assets_dir+'/a_plus_t.png') -urllib.urlretrieve(repo+branch+'/mintnet-kubernetes/README.rst', filename=tools+'/mintnet-kubernetes.rst') -urllib.urlretrieve(repo+branch+'/mintnet-kubernetes/assets/gce1.png', filename=assets+'/gce1.png') -urllib.urlretrieve(repo+branch+'/mintnet-kubernetes/assets/gce2.png', filename=assets+'/gce2.png') -urllib.urlretrieve(repo+branch+'/mintnet-kubernetes/assets/statefulset.png', filename=assets+'/statefulset.png') -urllib.urlretrieve(repo+branch+'/mintnet-kubernetes/assets/t_plus_k.png', filename=assets+'/t_plus_k.png') +urllib.urlretrieve(tools_repo+tools_branch+'/docker/README.rst', filename=tools_dir+'/docker.rst') -urllib.urlretrieve(repo+branch+'/terraform-digitalocean/README.rst', filename=tools+'/terraform-digitalocean.rst') -urllib.urlretrieve(repo+branch+'/tm-bench/README.rst', filename=tools+'/benchmarking-and-monitoring.rst') +urllib.urlretrieve(tools_repo+tools_branch+'/mintnet-kubernetes/README.rst', filename=tools_dir+'/mintnet-kubernetes.rst') +urllib.urlretrieve(tools_repo+tools_branch+'/mintnet-kubernetes/assets/gce1.png', filename=assets_dir+'/gce1.png') +urllib.urlretrieve(tools_repo+tools_branch+'/mintnet-kubernetes/assets/gce2.png', filename=assets_dir+'/gce2.png') +urllib.urlretrieve(tools_repo+tools_branch+'/mintnet-kubernetes/assets/statefulset.png', filename=assets_dir+'/statefulset.png') +urllib.urlretrieve(tools_repo+tools_branch+'/mintnet-kubernetes/assets/t_plus_k.png', filename=assets_dir+'/t_plus_k.png') + +urllib.urlretrieve(tools_repo+tools_branch+'/terraform-digitalocean/README.rst', filename=tools_dir+'/terraform-digitalocean.rst') +urllib.urlretrieve(tools_repo+tools_branch+'/tm-bench/README.rst', filename=tools_dir+'/benchmarking-and-monitoring.rst') # the readme for below is included in tm-bench # urllib.urlretrieve('https://raw.githubusercontent.com/tendermint/tools/master/tm-monitor/README.rst', filename='tools/tm-monitor.rst') + +#### abci spec ################################# + +abci_repo = "https://raw.githubusercontent.com/tendermint/abci/" +abci_branch = "spec-docs" + +urllib.urlretrieve(abci_repo+abci_branch+'/specification.rst', filename='abci-spec.rst') diff --git a/docs/deploy-testnets.rst b/docs/deploy-testnets.rst index 89fa4b799..5740ca56f 100644 --- a/docs/deploy-testnets.rst +++ b/docs/deploy-testnets.rst @@ -13,7 +13,7 @@ It's relatively easy to setup a Tendermint cluster manually. The only requirements for a particular Tendermint node are a private key for the validator, stored as ``priv_validator.json``, and a list of the public keys of all validators, stored as ``genesis.json``. These files should -be stored in ``~/.tendermint``, or wherever the ``$TMHOME`` variable +be stored in ``~/.tendermint/config``, or wherever the ``$TMHOME`` variable might be set to. Here are the steps to setting up a testnet manually: @@ -24,15 +24,15 @@ Here are the steps to setting up a testnet manually: ``tendermint gen_validator`` 4) Compile a list of public keys for each validator into a ``genesis.json`` file. -5) Run ``tendermint node --p2p.seeds=< seed addresses >`` on each node, - where ``< seed addresses >`` is a comma separated list of the IP:PORT +5) Run ``tendermint node --p2p.persistent_peers=< peer addresses >`` on each node, + where ``< peer addresses >`` is a comma separated list of the IP:PORT combination for each node. The default port for Tendermint is ``46656``. Thus, if the IP addresses of your nodes were ``192.168.0.1, 192.168.0.2, 192.168.0.3, 192.168.0.4``, the command would look like: - ``tendermint node --p2p.seeds=192.168.0.1:46656,192.168.0.2:46656,192.168.0.3:46656,192.168.0.4:46656``. + ``tendermint node --p2p.persistent_peers=192.168.0.1:46656,192.168.0.2:46656,192.168.0.3:46656,192.168.0.4:46656``. -After a few seconds, all the nodes should connect to eachother and start +After a few seconds, all the nodes should connect to each other and start making blocks! For more information, see the Tendermint Networks section of `the guide to using Tendermint `__. @@ -48,7 +48,7 @@ Automated Deployment using Kubernetes The `mintnet-kubernetes tool `__ allows automating the deployment of a Tendermint network on an already -provisioned kubernetes cluster. For simple provisioning of a kubernetes +provisioned Kubernetes cluster. For simple provisioning of a Kubernetes cluster, check out the `Google Cloud Platform `__. Automated Deployment using Terraform and Ansible diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 30ab9a35d..39e6785ee 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -1,122 +1,15 @@ Tendermint Ecosystem ==================== -Below are the many applications built using various pieces of the Tendermint stack. We thank the community for their contributions thus far and welcome the addition of new projects. Feel free to submit a pull request to add your project! +The growing list of applications built using various pieces of the Tendermint stack can be found at: -ABCI Applications ------------------ +* https://tendermint.com/ecosystem -Burrow -^^^^^^ +We thank the community for their contributions thus far and welcome the addition of new projects. A pull request can be submitted to `this file `__ to include your project. -Ethereum Virtual Machine augmented with native permissioning scheme and global key-value store, written in Go, authored by Monax Industries, and incubated `by Hyperledger `__. - -cb-ledger -^^^^^^^^^ - -Custodian Bank Ledger, integrating central banking with the blockchains of tomorrow, written in C++, and `authored by Block Finance `__. - -Clearchain -^^^^^^^^^^ - -Application to manage a distributed ledger for money transfers that support multi-currency accounts, written in Go, and `authored by Allession Treglia `__. - -Comit -^^^^^ - -Public service reporting and tracking, written in Go, and `authored by Zach Balder `__. - -Cosmos SDK -^^^^^^^^^^ - -A prototypical account based crypto currency state machine supporting plugins, written in Go, and `authored by Cosmos `__. - -Ethermint -^^^^^^^^^ - -The go-ethereum state machine run as a ABCI app, written in Go, `authored by Tendermint `__. - -IAVL -^^^^ - -Immutable AVL+ tree with Merkle proofs, Written in Go, `authored by Tendermint `__. - -Lotion -^^^^^^ - -A Javascript microframework for building blockchain applications with Tendermint, written in Javascript, `authored by Judd Keppel of Tendermint `__. See also `lotion-chat `__ and `lotion-coin `__ apps written using Lotion. - -MerkleTree -^^^^^^^^^^ - -Immutable AVL+ tree with Merkle proofs, Written in Java, `authored by jTendermint `__. - -Passchain -^^^^^^^^^ - -Passchain is a tool to securely store and share passwords, tokens and other short secrets, `authored by trusch `__. - -Passwerk -^^^^^^^^ - -Encrypted storage web-utility backed by Tendermint, written in Go, `authored by Rigel Rozanski `__. - -Py-Tendermint -^^^^^^^^^^^^^ - -A Python microframework for building blockchain applications with Tendermint, written in Python, `authored by Dave Bryson `__. - -Stratumn -^^^^^^^^ - -SDK for "Proof-of-Process" networks, written in Go, `authored by the Stratumn team `__. - -TMChat -^^^^^^ - -P2P chat using Tendermint, written in Java, `authored by wolfposd `__. - - -ABCI Servers ------------- - -+------------------------------------------------------------------+--------------------+--------------+ -| **Name** | **Author** | **Language** | -| | | | -+------------------------------------------------------------------+--------------------+--------------+ -| `abci `__ | Tendermint | Go | -+------------------------------------------------------------------+--------------------+--------------+ -| `js abci `__ | Tendermint | Javascript | -+------------------------------------------------------------------+--------------------+--------------+ -| `cpp-tmsp `__ | Martin Dyring | C++ | -+------------------------------------------------------------------+--------------------+--------------+ -| `c-abci `__ | ChainX | C | -+------------------------------------------------------------------+--------------------+--------------+ -| `jabci `__ | jTendermint | Java | -+------------------------------------------------------------------+--------------------+--------------+ -| `ocaml-tmsp `__ | Zach Balder | Ocaml | -+------------------------------------------------------------------+--------------------+--------------+ -| `abci_server `__ | Krzysztof Jurewicz | Erlang | -+------------------------------------------------------------------+--------------------+--------------+ -| `rust-tsp `__   | Adrian Brink | Rust       | -+------------------------------------------------------------------+--------------------+--------------+ -| `hs-abci `__ | Alberto Gonzalez | Haskell | -+------------------------------------------------------------------+--------------------+--------------+ -| `haskell-abci `__ | Christoper Goes | Haskell | -+------------------------------------------------------------------+--------------------+--------------+ -| `Spearmint `__ | Dennis Mckinnon | Javascript | -+------------------------------------------------------------------+--------------------+--------------+ -| `py-abci `__ | Dave Bryson | Python | -+------------------------------------------------------------------+--------------------+--------------+ - -Deployment Tools ----------------- +Other Tools +----------- See `deploy testnets <./deploy-testnets.html>`__ for information about all the tools built by Tendermint. We have Kubernetes, Ansible, and Terraform integrations. -Cloudsoft built `brooklyn-tendermint `__ for deploying a tendermint testnet in docker continers. It uses Clocker for Apache Brooklyn. - -Dev Tools ---------- - For upgrading from older to newer versions of tendermint and to migrate your chain data, see `tm-migrator `__ written by @hxzqlh. diff --git a/docs/examples/getting-started.md b/docs/examples/getting-started.md new file mode 100644 index 000000000..3ae42e27f --- /dev/null +++ b/docs/examples/getting-started.md @@ -0,0 +1,142 @@ +# Tendermint + +## Overview + +This is a quick start guide. If you have a vague idea about how Tendermint works +and want to get started right away, continue. Otherwise, [review the documentation](http://tendermint.readthedocs.io/en/master/) + +## Install + +### Quick Install + +On a fresh Ubuntu 16.04 machine can be done with [this script](https://git.io/vNLfY), like so: + +``` +curl -L https://git.io/vNLfY | bash +source ~/.profile +``` + +WARNING: do not run the above on your local machine. + +The script is also used to facilitate cluster deployment below. + +### Manual Install + +Requires: +- `go` minimum version 1.9 +- `$GOPATH` environment variable must be set +- `$GOPATH/bin` must be on your `$PATH` (see https://github.com/tendermint/tendermint/wiki/Setting-GOPATH) + +To install Tendermint, run: + +``` +go get github.com/tendermint/tendermint +cd $GOPATH/src/github.com/tendermint/tendermint +make get_tools && make get_vendor_deps +make install +``` + +Note that `go get` may return an error but it can be ignored. + +Confirm installation: + +``` +$ tendermint version +0.15.0-381fe19 +``` + +## Initialization + +Running: + +``` +tendermint init +``` + +will create the required files for a single, local node. + +These files are found in `$HOME/.tendermint`: + +``` +$ ls $HOME/.tendermint + +config.toml data genesis.json priv_validator.json +``` + +For a single, local node, no further configuration is required. +Configuring a cluster is covered further below. + +## Local Node + +Start tendermint with a simple in-process application: + +``` +tendermint node --proxy_app=dummy +``` + +and blocks will start to stream in: + +``` +I[01-06|01:45:15.592] Executed block module=state height=1 validTxs=0 invalidTxs=0 +I[01-06|01:45:15.624] Committed state module=state height=1 txs=0 appHash= +``` + +Check the status with: + +``` +curl -s localhost:46657/status +``` + +### Sending Transactions + +With the dummy app running, we can send transactions: + +``` +curl -s 'localhost:46657/broadcast_tx_commit?tx="abcd"' +``` + +and check that it worked with: + +``` +curl -s 'localhost:46657/abci_query?data="abcd"' +``` + +We can send transactions with a key and value too: + +``` +curl -s 'localhost:46657/broadcast_tx_commit?tx="name=satoshi"' +``` + +and query the key: + +``` +curl -s 'localhost:46657/abci_query?data="name"' +``` + +where the value is returned in hex. + +## Cluster of Nodes + +First create four Ubuntu cloud machines. The following was tested on Digital Ocean Ubuntu 16.04 x64 (3GB/1CPU, 20GB SSD). We'll refer to their respective IP addresses below as IP1, IP2, IP3, IP4. + +Then, `ssh` into each machine, and execute [this script](https://git.io/vNLfY): + +``` +curl -L https://git.io/vNLfY | bash +source ~/.profile +``` + +This will install `go` and other dependencies, get the Tendermint source code, then compile the `tendermint` binary. + +Next, `cd` into `docs/examples`. Each command below should be run from each node, in sequence: + +``` +tendermint node --home ./node1 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node2 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node3 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +tendermint node --home ./node4 --proxy_app=dummy --p2p.seeds IP1:46656,IP2:46656,IP3:46656,IP4:46656 +``` + +Note that after the third node is started, blocks will start to stream in because >2/3 of validators (defined in the `genesis.json`) have come online. Seeds can also be specified in the `config.toml`. See [this PR](https://github.com/tendermint/tendermint/pull/792) for more information about configuration options. + +Transactions can then be sent as covered in the single, local node example above. diff --git a/docs/examples/install_tendermint.sh b/docs/examples/install_tendermint.sh new file mode 100644 index 000000000..ca328dad0 --- /dev/null +++ b/docs/examples/install_tendermint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# XXX: this script is meant to be used only on a fresh Ubuntu 16.04 instance +# and has only been tested on Digital Ocean + +# get and unpack golang +curl -O https://storage.googleapis.com/golang/go1.9.2.linux-amd64.tar.gz +tar -xvf go1.9.2.linux-amd64.tar.gz + +apt install make + +## move go and add binary to path +mv go /usr/local +echo "export PATH=\$PATH:/usr/local/go/bin" >> ~/.profile + +## create the GOPATH directory, set GOPATH and put on PATH +mkdir goApps +echo "export GOPATH=/root/goApps" >> ~/.profile +echo "export PATH=\$PATH:\$GOPATH/bin" >> ~/.profile + +source ~/.profile + +## get the code and move into it +REPO=github.com/tendermint/tendermint +go get $REPO +cd $GOPATH/src/$REPO + +## build +git checkout v0.15.0 +make get_tools +make get_vendor_deps +make install \ No newline at end of file diff --git a/docs/examples/node1/config.toml b/docs/examples/node1/config.toml new file mode 100644 index 000000000..10bbf7105 --- /dev/null +++ b/docs/examples/node1/config.toml @@ -0,0 +1,15 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +proxy_app = "tcp://127.0.0.1:46658" +moniker = "penguin" +fast_sync = true +db_backend = "leveldb" +log_level = "state:info,*:error" + +[rpc] +laddr = "tcp://0.0.0.0:46657" + +[p2p] +laddr = "tcp://0.0.0.0:46656" +seeds = "" diff --git a/docs/examples/node1/genesis.json b/docs/examples/node1/genesis.json new file mode 100644 index 000000000..78ff6ab3b --- /dev/null +++ b/docs/examples/node1/genesis.json @@ -0,0 +1,42 @@ +{ + "genesis_time":"0001-01-01T00:00:00Z", + "chain_id":"test-chain-wt7apy", + "validators":[ + { + "pub_key":{ + "type":"ed25519", + "data":"F08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + }, + "power":10, + "name":"node1" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "A8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + }, + "power":10, + "name":"node2" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + }, + "power":10, + "name":"node3" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + }, + "power":10, + "name":"node4" + } + ], + "app_hash":"" +} diff --git a/docs/examples/node1/priv_validator.json b/docs/examples/node1/priv_validator.json new file mode 100644 index 000000000..f6c5634a1 --- /dev/null +++ b/docs/examples/node1/priv_validator.json @@ -0,0 +1,15 @@ +{ + "address":"4DC2756029CE0D8F8C6C3E4C3CE6EE8C30AF352F", + "pub_key":{ + "type":"ed25519", + "data":"F08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + }, + "last_height":0, + "last_round":0, + "last_step":0, + "last_signature":null, + "priv_key":{ + "type":"ed25519", + "data":"4D3648E1D93C8703E436BFF814728B6BD270CFDFD686DF5385E8ACBEB7BE2D7DF08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + } +} diff --git a/docs/examples/node2/config.toml b/docs/examples/node2/config.toml new file mode 100644 index 000000000..10bbf7105 --- /dev/null +++ b/docs/examples/node2/config.toml @@ -0,0 +1,15 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +proxy_app = "tcp://127.0.0.1:46658" +moniker = "penguin" +fast_sync = true +db_backend = "leveldb" +log_level = "state:info,*:error" + +[rpc] +laddr = "tcp://0.0.0.0:46657" + +[p2p] +laddr = "tcp://0.0.0.0:46656" +seeds = "" diff --git a/docs/examples/node2/genesis.json b/docs/examples/node2/genesis.json new file mode 100644 index 000000000..78ff6ab3b --- /dev/null +++ b/docs/examples/node2/genesis.json @@ -0,0 +1,42 @@ +{ + "genesis_time":"0001-01-01T00:00:00Z", + "chain_id":"test-chain-wt7apy", + "validators":[ + { + "pub_key":{ + "type":"ed25519", + "data":"F08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + }, + "power":10, + "name":"node1" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "A8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + }, + "power":10, + "name":"node2" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + }, + "power":10, + "name":"node3" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + }, + "power":10, + "name":"node4" + } + ], + "app_hash":"" +} diff --git a/docs/examples/node2/priv_validator.json b/docs/examples/node2/priv_validator.json new file mode 100644 index 000000000..7733196e6 --- /dev/null +++ b/docs/examples/node2/priv_validator.json @@ -0,0 +1,15 @@ +{ + "address": "DD6C63A762608A9DDD4A845657743777F63121D6", + "pub_key": { + "type": "ed25519", + "data": "A8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + }, + "last_height": 0, + "last_round": 0, + "last_step": 0, + "last_signature": null, + "priv_key": { + "type": "ed25519", + "data": "7B0DE666FF5E9B437D284BCE767F612381890C018B93B0A105D2E829A568DA6FA8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + } +} diff --git a/docs/examples/node3/config.toml b/docs/examples/node3/config.toml new file mode 100644 index 000000000..10bbf7105 --- /dev/null +++ b/docs/examples/node3/config.toml @@ -0,0 +1,15 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +proxy_app = "tcp://127.0.0.1:46658" +moniker = "penguin" +fast_sync = true +db_backend = "leveldb" +log_level = "state:info,*:error" + +[rpc] +laddr = "tcp://0.0.0.0:46657" + +[p2p] +laddr = "tcp://0.0.0.0:46656" +seeds = "" diff --git a/docs/examples/node3/genesis.json b/docs/examples/node3/genesis.json new file mode 100644 index 000000000..78ff6ab3b --- /dev/null +++ b/docs/examples/node3/genesis.json @@ -0,0 +1,42 @@ +{ + "genesis_time":"0001-01-01T00:00:00Z", + "chain_id":"test-chain-wt7apy", + "validators":[ + { + "pub_key":{ + "type":"ed25519", + "data":"F08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + }, + "power":10, + "name":"node1" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "A8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + }, + "power":10, + "name":"node2" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + }, + "power":10, + "name":"node3" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + }, + "power":10, + "name":"node4" + } + ], + "app_hash":"" +} diff --git a/docs/examples/node3/priv_validator.json b/docs/examples/node3/priv_validator.json new file mode 100644 index 000000000..d570b1279 --- /dev/null +++ b/docs/examples/node3/priv_validator.json @@ -0,0 +1,15 @@ +{ + "address": "6D6A1E313B407B5474106CA8759C976B777AB659", + "pub_key": { + "type": "ed25519", + "data": "E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + }, + "last_height": 0, + "last_round": 0, + "last_step": 0, + "last_signature": null, + "priv_key": { + "type": "ed25519", + "data": "622432A370111A5C25CFE121E163FE709C9D5C95F551EDBD7A2C69A8545C9B76E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + } +} diff --git a/docs/examples/node4/config.toml b/docs/examples/node4/config.toml new file mode 100644 index 000000000..10bbf7105 --- /dev/null +++ b/docs/examples/node4/config.toml @@ -0,0 +1,15 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +proxy_app = "tcp://127.0.0.1:46658" +moniker = "penguin" +fast_sync = true +db_backend = "leveldb" +log_level = "state:info,*:error" + +[rpc] +laddr = "tcp://0.0.0.0:46657" + +[p2p] +laddr = "tcp://0.0.0.0:46656" +seeds = "" diff --git a/docs/examples/node4/genesis.json b/docs/examples/node4/genesis.json new file mode 100644 index 000000000..78ff6ab3b --- /dev/null +++ b/docs/examples/node4/genesis.json @@ -0,0 +1,42 @@ +{ + "genesis_time":"0001-01-01T00:00:00Z", + "chain_id":"test-chain-wt7apy", + "validators":[ + { + "pub_key":{ + "type":"ed25519", + "data":"F08446C80A33E10D620E21450821B58D053778528F2B583D423B3E46EC647D30" + }, + "power":10, + "name":"node1" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "A8423F70A9E512643B4B00F7C3701ECAD1F31B0A1FAA45852C41046353B9A07F" + }, + "power":10, + "name":"node2" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "E52EFFAEDFE1D618ECDA71DE3B23592B3612CAABA0C10826E4C3120B2198C29A" + }, + "power":10, + "name":"node3" + } + , + { + "pub_key":{ + "type":"ed25519", + "data": "2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + }, + "power":10, + "name":"node4" + } + ], + "app_hash":"" +} diff --git a/docs/examples/node4/priv_validator.json b/docs/examples/node4/priv_validator.json new file mode 100644 index 000000000..1ea7831bb --- /dev/null +++ b/docs/examples/node4/priv_validator.json @@ -0,0 +1,15 @@ +{ + "address": "829A9663611D3DD88A3D84EA0249679D650A0755", + "pub_key": { + "type": "ed25519", + "data": "2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + }, + "last_height": 0, + "last_round": 0, + "last_step": 0, + "last_signature": null, + "priv_key": { + "type": "ed25519", + "data": "0A604D1C9AE94A50150BF39E603239092F9392E4773F4D8F4AC1D86E6438E89E2B8FC09C07955A02998DFE5AF1AAD1C44115ECA7635FF51A867CF4265D347C07" + } +} diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 26f6b7897..94f55c128 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -40,7 +40,7 @@ dependencies: Now you should have the ``abci-cli`` installed; you'll see a couple of commands (``counter`` and ``dummy``) that are example applications written in Go. See below for an application -written in Javascript. +written in JavaScript. Now, let's run some apps! @@ -49,7 +49,7 @@ Dummy - A First Example The dummy app is a `Merkle tree `__ that just stores all -transactions. If the transaction contains an ``=``, eg. ``key=value``, +transactions. If the transaction contains an ``=``, e.g. ``key=value``, then the ``value`` is stored under the ``key`` in the Merkle tree. Otherwise, the full transaction bytes are stored as the key and the value. @@ -147,7 +147,7 @@ The result should look like: Note the ``value`` in the result (``61626364``); this is the hex-encoding of the ASCII of ``abcd``. You can verify this in -a python shell by running ``"61626364".decode('hex')``. Stay +a python 2 shell by running ``"61626364".decode('hex')`` or in python 3 shell by running ``import codecs; codecs.decode("61626364", 'hex').decode('ascii')``. Stay tuned for a future release that `makes this output more human-readable `__. Now let's try setting a different key and value: diff --git a/docs/index.rst b/docs/index.rst index 3ad3c4c59..b32ba4841 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,6 +53,7 @@ Tendermint 102 :maxdepth: 2 abci-cli.rst + abci-spec.rst app-architecture.rst app-development.rst how-to-read-logs.rst diff --git a/docs/install.rst b/docs/install.rst index 64fae4cdc..734be1629 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,7 +9,7 @@ To download pre-built binaries, see the `Download page `__) have had a monolithic design. That is, each blockchain stack is a single program that handles all the concerns of a decentralized ledger; this includes P2P connectivity, the "mempool" broadcasting of transactions, consensus on the most recent block, account balances, Turing-complete contracts, user-level permissions, etc. Using a monolithic architecture is typically bad practice in computer science. -It makes it difficult to reuse components of the code, and attempts to do so result in complex maintanence procedures for forks of the codebase. +It makes it difficult to reuse components of the code, and attempts to do so result in complex maintenance procedures for forks of the codebase. This is especially true when the codebase is not modular in design and suffers from "spaghetti code". Another problem with monolithic design is that it limits you to the language of the blockchain stack (or vice versa). In the case of Ethereum which supports a Turing-complete bytecode virtual-machine, it limits you to languages that compile down to that bytecode; today, those are Serpent and Solidity. diff --git a/docs/specification/configuration.rst b/docs/specification/configuration.rst index 74b41d09d..d7c186007 100644 --- a/docs/specification/configuration.rst +++ b/docs/specification/configuration.rst @@ -1,58 +1,182 @@ Configuration ============= -TendermintCore can be configured via a TOML file in -``$TMHOME/config.toml``. Some of these parameters can be overridden by -command-line flags. - -Config parameters -~~~~~~~~~~~~~~~~~ - -The main config parameters are defined -`here `__. - -- ``abci``: ABCI transport (socket \| grpc). *Default*: ``socket`` -- ``db_backend``: Database backend for the blockchain and - TendermintCore state. ``leveldb`` or ``memdb``. *Default*: - ``"leveldb"`` -- ``db_dir``: Database dir. *Default*: ``"$TMHOME/data"`` -- ``fast_sync``: Whether to sync faster from the block pool. *Default*: - ``true`` -- ``genesis_file``: The location of the genesis file. *Default*: - ``"$TMHOME/genesis.json"`` -- ``log_level``: *Default*: ``"state:info,*:error"`` -- ``moniker``: Name of this node. *Default*: the host name or ``"anonymous"`` - if runtime fails to get the host name -- ``priv_validator_file``: Validator private key file. *Default*: - ``"$TMHOME/priv_validator.json"`` -- ``prof_laddr``: Profile listen address. *Default*: ``""`` -- ``proxy_app``: The ABCI app endpoint. *Default*: - ``"tcp://127.0.0.1:46658"`` - -- ``consensus.max_block_size_txs``: Maximum number of block txs. - *Default*: ``10000`` -- ``consensus.create_empty_blocks``: Create empty blocks w/o txs. - *Default*: ``true`` -- ``consensus.create_empty_blocks_interval``: Block creation interval, even if empty. -- ``consensus.timeout_*``: Various consensus timeout parameters -- ``consensus.wal_file``: Consensus state WAL. *Default*: - ``"$TMHOME/data/cs.wal/wal"`` -- ``consensus.wal_light``: Whether to use light-mode for Consensus - state WAL. *Default*: ``false`` - -- ``mempool.*``: Various mempool parameters - -- ``p2p.addr_book_file``: Peer address book. *Default*: - ``"$TMHOME/addrbook.json"``. **NOT USED** -- ``p2p.laddr``: Node listen address. (0.0.0.0:0 means any interface, - any port). *Default*: ``"0.0.0.0:46656"`` -- ``p2p.pex``: Enable Peer-Exchange (dev feature). *Default*: ``false`` -- ``p2p.seeds``: Comma delimited host:port seed nodes. *Default*: - ``""`` -- ``p2p.skip_upnp``: Skip UPNP detection. *Default*: ``false`` - -- ``rpc.grpc_laddr``: GRPC listen address (BroadcastTx only). Port - required. *Default*: ``""`` -- ``rpc.laddr``: RPC listen address. Port required. *Default*: - ``"0.0.0.0:46657"`` -- ``rpc.unsafe``: Enabled unsafe rpc methods. *Default*: ``true`` +Tendermint Core can be configured via a TOML file in +``$TMHOME/config/config.toml``. Some of these parameters can be overridden by +command-line flags. For most users, the options in the ``##### main +base configuration options #####`` are intended to be modified while +config options further below are intended for advance power users. + +Config options +~~~~~~~~~~~~~~ + +The default configuration file create by ``tendermint init`` has all +the parameters set with their default values. It will look something +like the file below, however, double check by inspecting the +``config.toml`` created with your version of ``tendermint`` installed: + +:: + + # This is a TOML config file. + # For more information, see https://github.com/toml-lang/toml + + ##### main base config options ##### + + # TCP or UNIX socket address of the ABCI application, + # or the name of an ABCI application compiled in with the Tendermint binary + proxy_app = "tcp://127.0.0.1:46658" + + # A custom human readable name for this node + moniker = "anonymous" + + # 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 = true + + # Database backend: leveldb | memdb + db_backend = "leveldb" + + # Database directory + db_path = "data" + + # Output level for logging + log_level = "state:info,*:error" + + ##### additional base config options ##### + + # The ID of the chain to join (should be signed with every transaction and vote) + chain_id = "" + + # Path to the JSON file containing the initial validator set and other meta data + genesis_file = "genesis.json" + + # Path to the JSON file containing the private key to use as a validator in the consensus protocol + priv_validator_file = "priv_validator.json" + + # Mechanism to connect to the ABCI application: socket | grpc + abci = "socket" + + # TCP or UNIX socket address for the profiling server to listen on + prof_laddr = "" + + # If true, query the ABCI app on connecting to a new peer + # so the app can decide if we should keep the connection or not + filter_peers = false + + ##### advanced configuration options ##### + + ##### rpc server configuration options ##### + [rpc] + + # TCP or UNIX socket address for the RPC server to listen on + laddr = "tcp://0.0.0.0:46657" + + # TCP or UNIX socket address for the gRPC server to listen on + # NOTE: This server only supports /broadcast_tx_commit + grpc_laddr = "" + + # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool + unsafe = false + + ##### peer to peer configuration options ##### + [p2p] + + # Address to listen for incoming connections + laddr = "tcp://0.0.0.0:46656" + + # Comma separated list of seed nodes to connect to + seeds = "" + + # Comma separated list of nodes to keep persistent connections to + persistent_peers = "" + + # Path to address book + addr_book_file = "addrbook.json" + + # Set true for strict address routability rules + addr_book_strict = true + + # Time to wait before flushing messages out on the connection, in ms + flush_throttle_timeout = 100 + + # Maximum number of peers to connect to + max_num_peers = 50 + + # Maximum size of a message packet payload, in bytes + max_msg_packet_payload_size = 1024 + + # Rate at which packets can be sent, in bytes/second + send_rate = 512000 + + # Rate at which packets can be received, in bytes/second + recv_rate = 512000 + + # Set true to enable the peer-exchange reactor + pex = true + + # Seed mode, in which node constantly crawls the network and looks for + # peers. If another node asks it for addresses, it responds and disconnects. + # + # Does not work if the peer-exchange reactor is disabled. + seed_mode = false + + ##### mempool configuration options ##### + [mempool] + + recheck = true + recheck_empty = true + broadcast = true + wal_dir = "data/mempool.wal" + + ##### consensus configuration options ##### + [consensus] + + wal_file = "data/cs.wal/wal" + wal_light = false + + # All timeouts are in milliseconds + timeout_propose = 3000 + timeout_propose_delta = 500 + timeout_prevote = 1000 + timeout_prevote_delta = 500 + timeout_precommit = 1000 + timeout_precommit_delta = 500 + timeout_commit = 1000 + + # Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) + skip_timeout_commit = false + + # BlockSize + max_block_size_txs = 10000 + max_block_size_bytes = 1 + + # EmptyBlocks mode and possible interval between empty blocks in seconds + create_empty_blocks = true + create_empty_blocks_interval = 0 + + # Reactor sleep duration parameters are in milliseconds + peer_gossip_sleep_duration = 100 + peer_query_maj23_sleep_duration = 2000 + + ##### transactions indexer configuration options ##### + [tx_index] + + # What indexer to use for transactions + # + # Options: + # 1) "null" (default) + # 2) "kv" - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). + indexer = "{{ .TxIndex.Indexer }}" + + # Comma-separated list of tags to index (by default the only tag is tx hash) + # + # It's recommended to index only a subset of tags due to possible memory + # bloat. This is, of course, depends on the indexer's DB and the volume of + # transactions. + index_tags = "{{ .TxIndex.IndexTags }}" + + # When set to true, tells indexer to index all tags. Note this may be not + # desirable (see the comment above). IndexTags has a precedence over + # IndexAllTags (i.e. when given both, IndexTags will be indexed). + index_all_tags = {{ .TxIndex.IndexAllTags }} diff --git a/docs/specification/genesis.rst b/docs/specification/genesis.rst index a7ec7a268..7e36c1315 100644 --- a/docs/specification/genesis.rst +++ b/docs/specification/genesis.rst @@ -1,7 +1,7 @@ Genesis ======= -The genesis.json file in ``$TMHOME`` defines the initial TendermintCore +The genesis.json file in ``$TMHOME/config`` defines the initial TendermintCore state upon genesis of the blockchain (`see definition `__). diff --git a/docs/specification/new-spec/README.md b/docs/specification/new-spec/README.md index a5061e62e..f3b9e3c29 100644 --- a/docs/specification/new-spec/README.md +++ b/docs/specification/new-spec/README.md @@ -1,20 +1,47 @@ # Tendermint Specification This is a markdown specification of the Tendermint blockchain. +It defines the base data structures, how they are validated, +and how they are communicated over the network. -It defines the base data structures used in the blockchain and how they are validated. +XXX: this spec is a work in progress and not yet complete - see github +[issues](https://github.com/tendermint/tendermint/issues) and +[pull requests](https://github.com/tendermint/tendermint/pulls) +for more details. -It contains the following components: +If you find discrepancies between the spec and the code that +do not have an associated issue or pull request on github, +please submit them to our [bug bounty](https://tendermint.com/security)! +## Contents + +### Data Structures + +- [Overview](#overview) - [Encoding and Digests](encoding.md) - [Blockchain](blockchain.md) - [State](state.md) +### P2P and Network Protocols + +TODO: update links + +- [The Base P2P Layer](p2p/README.md): multiplex the protocols ("reactors") on authenticated and encrypted TCP connections +- [Peer Exchange (PEX)](pex/README.md): gossip known peer addresses so peers can find each other +- [Block Sync](block_sync/README.md): gossip blocks so peers can catch up quickly +- [Consensus](consensus/README.md): gossip votes and block parts so new blocks can be committed +- [Mempool](mempool/README.md): gossip transactions so they get included in blocks +- [Evidence](evidence/README.md): TODO + +### More +- [Light Client](light_client/README.md): TODO +- [Persistence](persistence/README.md): TODO + ## Overview Tendermint provides Byzantine Fault Tolerant State Machine Replication using hash-linked batches of transactions. Such transaction batches are called "blocks". -Hence Tendermint defines a "blockchain". +Hence, Tendermint defines a "blockchain". Each block in Tendermint has a unique index - its Height. A block at `Height == H` can only be committed *after* the @@ -23,7 +50,7 @@ Each block is committed by a known set of weighted Validators. Membership and weighting within this set may change over time. Tendermint guarantees the safety and liveness of the blockchain so long as less than 1/3 of the total weight of the Validator set -is malicious. +is malicious or faulty. A commit in Tendermint is a set of signed messages from more than 2/3 of the total weight of the current Validator set. Validators take turns proposing @@ -49,9 +76,3 @@ Also note that information like the transaction results and the validator set ar directly included in the block - only their cryptographic digests (Merkle roots) are. Hence, verification of a block requires a separate data structure to store this information. We call this the `State`. Block verification also requires access to the previous block. - -## TODO - -- Light Client -- P2P -- Reactor protocols (consensus, mempool, blockchain, pex) diff --git a/docs/specification/new-spec/bft-time.md b/docs/specification/new-spec/bft-time.md new file mode 100644 index 000000000..cbb1b65d4 --- /dev/null +++ b/docs/specification/new-spec/bft-time.md @@ -0,0 +1,42 @@ +# BFT time in Tendermint + +Tendermint provides a deterministic, Byzantine fault-tolerant, source of time. +Time in Tendermint is defined with the Time field of the block header. + +It satisfies the following properties: + +- Time Monotonicity: Time is monotonically increasing, i.e., given +a header H1 for height h1 and a header H2 for height `h2 = h1 + 1`, `H1.Time < H2.Time`. +- Time Validity: Given a set of Commit votes that forms the `block.LastCommit` field, a range of +valid values for the Time field of the block header is defined only by +Precommit messages (from the LastCommit field) sent by correct processes, i.e., +a faulty process cannot arbitrarily increase the Time value. + +In the context of Tendermint, time is of type int64 and denotes UNIX time in milliseconds, i.e., +corresponds to the number of milliseconds since January 1, 1970. Before defining rules that need to be enforced by the +Tendermint consensus protocol, so the properties above holds, we introduce the following definition: + +- median of a set of `Vote` messages is equal to the median of `Vote.Time` fields of the corresponding `Vote` messages + +We ensure Time Monotonicity and Time Validity properties by the following rules: + +- let rs denotes `RoundState` (consensus internal state) of some process. Then +`rs.ProposalBlock.Header.Time == median(rs.LastCommit) && +rs.Proposal.Timestamp == rs.ProposalBlock.Header.Time`. + +- Furthermore, when creating the `vote` message, the following rules for determining `vote.Time` field should hold: + + - if `rs.Proposal` is defined then + `vote.Time = max(rs.Proposal.Timestamp + 1, time.Now())`, where `time.Now()` + denotes local Unix time in milliseconds. + + - if `rs.Proposal` is not defined and `rs.Votes` contains +2/3 of the corresponding vote messages (votes for the + current height and round, and with the corresponding type (`Prevote` or `Precommit`)), then + + `vote.Time = max(median(getVotes(rs.Votes, vote.Height, vote.Round, vote.Type)), time.Now())`, + + where `getVotes` function returns the votes for particular `Height`, `Round` and `Type`. + The second rule is relevant for the case when a process jumps to a higher round upon receiving +2/3 votes for a higher + round, but the corresponding `Proposal` message for the higher round hasn't been received yet. + + diff --git a/docs/specification/new-spec/blockchain.md b/docs/specification/new-spec/blockchain.md index ce2529f80..d95d5d330 100644 --- a/docs/specification/new-spec/blockchain.md +++ b/docs/specification/new-spec/blockchain.md @@ -2,17 +2,23 @@ Here we describe the data structures in the Tendermint blockchain and the rules for validating them. -# Data Structures +## Data Structures The Tendermint blockchains consists of a short list of basic data types: -`Block`, `Header`, `Vote`, `BlockID`, `Signature`, and `Evidence`. + +- `Block` +- `Header` +- `Vote` +- `BlockID` +- `Signature` +- `Evidence` ## Block A block consists of a header, a list of transactions, a list of votes (the commit), -and a list of evidence if malfeasance (ie. signing conflicting votes). +and a list of evidence of malfeasance (ie. signing conflicting votes). -``` +```go type Block struct { Header Header Txs [][]byte @@ -26,12 +32,12 @@ type Block struct { A block header contains metadata about the block and about the consensus, as well as commitments to the data in the current block, the previous block, and the results returned by the application: -``` +```go type Header struct { // block metadata Version string // Version string ChainID string // ID of the chain - Height int64 // current block height + Height int64 // Current block height Time int64 // UNIX time, in millisconds // current block @@ -55,7 +61,7 @@ type Header struct { } ``` -Further details on each of this fields is taken up below. +Further details on each of these fields is described below. ## BlockID @@ -66,7 +72,7 @@ the block during consensus, is the Merkle root of the complete serialized block cut into parts. The `BlockID` includes these two hashes, as well as the number of parts. -``` +```go type BlockID struct { Hash []byte Parts PartsHeader @@ -83,7 +89,7 @@ type PartsHeader struct { A vote is a signed message from a validator for a particular block. The vote includes information about the validator signing it. -``` +```go type Vote struct { Timestamp int64 Address []byte @@ -96,10 +102,9 @@ type Vote struct { } ``` - There are two types of votes: -a prevote has `vote.Type == 1` and -a precommit has `vote.Type == 2`. +a *prevote* has `vote.Type == 1` and +a *precommit* has `vote.Type == 2`. ## Signature @@ -111,7 +116,7 @@ Currently, Tendermint supports Ed25519 and Secp256k1. An ED25519 signature has `Type == 0x1`. It looks like: -``` +```go // Implements Signature type Ed25519Signature struct { Type int8 = 0x1 @@ -125,7 +130,7 @@ where `Signature` is the 64 byte signature. A `Secp256k1` signature has `Type == 0x2`. It looks like: -``` +```go // Implements Signature type Secp256k1Signature struct { Type int8 = 0x2 @@ -135,7 +140,7 @@ type Secp256k1Signature struct { where `Signature` is the DER encoded signature, ie: -``` +```hex 0x30 <0x02> 0x2 . ``` @@ -143,7 +148,7 @@ where `Signature` is the DER encoded signature, ie: TODO -# Validation +## Validation Here we describe the validation rules for every element in a block. Blocks which do not satisfy these rules are considered invalid. @@ -159,7 +164,7 @@ and other results from the application. Elements of an object are accessed as expected, ie. `block.Header`. See [here](state.md) for the definition of `state`. -## Header +### Header A Header is valid if its corresponding fields are valid. @@ -173,7 +178,7 @@ Arbitrary constant string. ### Height -``` +```go block.Header.Height > 0 block.Header.Height == prevBlock.Header.Height + 1 ``` @@ -185,12 +190,12 @@ The height is an incrementing integer. The first block has `block.Header.Height The median of the timestamps of the valid votes in the block.LastCommit. Corresponds to the number of nanoseconds, with millisecond resolution, since January 1, 1970. -Note the timestamp in a vote must be greater by at least one millisecond than that of the +Note: the timestamp of a vote must be greater by at least one millisecond than that of the block being voted on. ### NumTxs -``` +```go block.Header.NumTxs == len(block.Txs) ``` @@ -198,7 +203,7 @@ Number of transactions included in the block. ### TxHash -``` +```go block.Header.TxHash == SimpleMerkleRoot(block.Txs) ``` @@ -206,7 +211,7 @@ Simple Merkle root of the transactions in the block. ### LastCommitHash -``` +```go block.Header.LastCommitHash == SimpleMerkleRoot(block.LastCommit) ``` @@ -217,7 +222,7 @@ The first block has `block.Header.LastCommitHash == []byte{}` ### TotalTxs -``` +```go block.Header.TotalTxs == prevBlock.Header.TotalTxs + block.Header.NumTxs ``` @@ -227,7 +232,9 @@ The first block has `block.Header.TotalTxs = block.Header.NumberTxs`. ### LastBlockID -``` +LastBlockID is the previous block's BlockID: + +```go prevBlockParts := MakeParts(prevBlock, state.LastConsensusParams.BlockGossip.BlockPartSize) block.Header.LastBlockID == BlockID { Hash: SimpleMerkleRoot(prevBlock.Header), @@ -238,14 +245,14 @@ block.Header.LastBlockID == BlockID { } ``` -Previous block's BlockID. Note it depends on the ConsensusParams, +Note: it depends on the ConsensusParams, which are held in the `state` and may be updated by the application. The first block has `block.Header.LastBlockID == BlockID{}`. ### ResultsHash -``` +```go block.ResultsHash == SimpleMerkleRoot(state.LastResults) ``` @@ -255,7 +262,7 @@ The first block has `block.Header.ResultsHash == []byte{}`. ### AppHash -``` +```go block.AppHash == state.AppHash ``` @@ -265,7 +272,7 @@ The first block has `block.Header.AppHash == []byte{}`. ### ValidatorsHash -``` +```go block.ValidatorsHash == SimpleMerkleRoot(state.Validators) ``` @@ -275,7 +282,7 @@ May be updated by the application. ### ConsensusParamsHash -``` +```go block.ConsensusParamsHash == SimpleMerkleRoot(state.ConsensusParams) ``` @@ -284,19 +291,17 @@ May be updated by the application. ### Proposer -``` +```go block.Header.Proposer in state.Validators ``` Original proposer of the block. Must be a current validator. -NOTE: this field can only be further verified by real-time participants in the consensus. -This is because the same block can be proposed in multiple rounds for the same height -and we do not track the initial round the block was proposed. +NOTE: we also need to track the round. -### EvidenceHash +## EvidenceHash -``` +```go block.EvidenceHash == SimpleMerkleRoot(block.Evidence) ``` @@ -310,7 +315,7 @@ Arbitrary length array of arbitrary length byte-arrays. The first height is an exception - it requires the LastCommit to be empty: -``` +```go if block.Header.Height == 1 { len(b.LastCommit) == 0 } @@ -318,7 +323,7 @@ if block.Header.Height == 1 { Otherwise, we require: -``` +```go len(block.LastCommit) == len(state.LastValidators) talliedVotingPower := 0 for i, vote := range block.LastCommit{ @@ -356,21 +361,21 @@ For signing, votes are encoded in JSON, and the ChainID is included, in the form We define a method `Verify` that returns `true` if the signature verifies against the pubkey for the CanonicalSignBytes using the given ChainID: -``` +```go func (v Vote) Verify(chainID string, pubKey PubKey) bool { return pubKey.Verify(v.Signature, CanonicalSignBytes(chainID, v)) } ``` -where `pubKey.Verify` performs the approprioate digital signature verification of the `pubKey` +where `pubKey.Verify` performs the appropriate digital signature verification of the `pubKey` against the given signature and message bytes. ## Evidence +TODO ``` - - +TODO ``` Every piece of evidence contains two conflicting votes from a single validator that @@ -382,10 +387,38 @@ The votes must not be too old. Once a block is validated, it can be executed against the state. -The state follows the recursive equation: +The state follows this recursive equation: -``` -app = NewABCIApp +```go state(1) = InitialState -state(h+1) <- Execute(state(h), app, block(h)) +state(h+1) <- Execute(state(h), ABCIApp, block(h)) ``` + +where `InitialState` includes the initial consensus parameters and validator set, +and `ABCIApp` is an ABCI application that can return results and changes to the validator +set (TODO). Execute is defined as: + +```go +Execute(s State, app ABCIApp, block Block) State { + TODO: just spell out ApplyBlock here + and remove ABCIResponses struct. + abciResponses := app.ApplyBlock(block) + + return State{ + LastResults: abciResponses.DeliverTxResults, + AppHash: abciResponses.AppHash, + Validators: UpdateValidators(state.Validators, abciResponses.ValidatorChanges), + LastValidators: state.Validators, + ConsensusParams: UpdateConsensusParams(state.ConsensusParams, abci.Responses.ConsensusParamChanges), + } +} + +type ABCIResponses struct { + DeliverTxResults []Result + ValidatorChanges []Validator + ConsensusParamChanges ConsensusParams + AppHash []byte +} +``` + + diff --git a/docs/specification/new-spec/encoding.md b/docs/specification/new-spec/encoding.md index a7482e6cb..e0317b7ef 100644 --- a/docs/specification/new-spec/encoding.md +++ b/docs/specification/new-spec/encoding.md @@ -2,9 +2,13 @@ ## Binary Serialization (TMBIN) -Tendermint aims to encode data structures in a manner similar to how the corresponding Go structs are laid out in memory. +Tendermint aims to encode data structures in a manner similar to how the corresponding Go structs +are laid out in memory. Variable length items are length-prefixed. -While the encoding was inspired by Go, it is easily implemented in other languages as well given its intuitive design. +While the encoding was inspired by Go, it is easily implemented in other languages as well, given its intuitive design. + +XXX: This is changing to use real varints and 4-byte-prefixes. +See https://github.com/tendermint/go-wire/tree/sdk2. ### Fixed Length Integers @@ -16,7 +20,7 @@ Negative integers are encoded via twos-complement. Examples: -``` +```go encode(uint8(6)) == [0x06] encode(uint32(6)) == [0x00, 0x00, 0x00, 0x06] @@ -33,10 +37,9 @@ Negative integers are encoded by flipping the leading bit of the length-prefix t Zero is encoded as `0x00`. It is not length-prefixed. - Examples: -``` +```go encode(uint(6)) == [0x01, 0x06] encode(uint(70000)) == [0x03, 0x01, 0x11, 0x70] @@ -48,14 +51,14 @@ encode(int(0)) == [0x00] ### Strings -An encoded string is a length prefix followed by the underlying bytes of the string. +An encoded string is length-prefixed followed by the underlying bytes of the string. The length-prefix is itself encoded as an `int`. The empty string is encoded as `0x00`. It is not length-prefixed. Examples: -``` +```go encode("") == [0x00] encode("a") == [0x01, 0x01, 0x61] encode("hello") == [0x01, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F] @@ -69,7 +72,7 @@ There is no length-prefix. Examples: -``` +```go encode([4]int8{1, 2, 3, 4}) == [0x01, 0x02, 0x03, 0x04] encode([4]int16{1, 2, 3, 4}) == [0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04] encode([4]int{1, 2, 3, 4}) == [0x01, 0x01, 0x01, 0x02, 0x01, 0x03, 0x01, 0x04] @@ -78,14 +81,15 @@ encode([2]string{"abc", "efg"}) == [0x01, 0x03, 0x61, 0x62, 0x63, 0x01, 0x03, 0x ### Slices (variable length) -An encoded variable-length array is a length prefix followed by the concatenation of the encoding of its elements. +An encoded variable-length array is length-prefixed followed by the concatenation of the encoding of +its elements. The length-prefix is itself encoded as an `int`. An empty slice is encoded as `0x00`. It is not length-prefixed. Examples: -``` +```go encode([]int8{}) == [0x00] encode([]int8{1, 2, 3, 4}) == [0x01, 0x04, 0x01, 0x02, 0x03, 0x04] encode([]int16{1, 2, 3, 4}) == [0x01, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04] @@ -93,6 +97,18 @@ encode([]int{1, 2, 3, 4}) == [0x01, 0x04, 0x01, 0x01, 0x01, 0x02, 0x01, 0x encode([]string{"abc", "efg"}) == [0x01, 0x02, 0x01, 0x03, 0x61, 0x62, 0x63, 0x01, 0x03, 0x65, 0x66, 0x67] ``` +### BitArray + +BitArray is encoded as an `int` of the number of bits, and with an array of `uint64` to encode +value of each array element. + +```go +type BitArray struct { + Bits int + Elems []uint64 +} +``` + ### Time Time is encoded as an `int64` of the number of nanoseconds since January 1, 1970, @@ -102,7 +118,7 @@ Times before then are invalid. Examples: -``` +```go encode(time.Time("Jan 1 00:00:00 UTC 1970")) == [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] encode(time.Time("Jan 1 00:00:01 UTC 1970")) == [0x00, 0x00, 0x00, 0x00, 0x3B, 0x9A, 0xCA, 0x00] // 1,000,000,000 ns encode(time.Time("Mon Jan 2 15:04:05 -0700 MST 2006")) == [0x0F, 0xC4, 0xBB, 0xC1, 0x53, 0x03, 0x12, 0x00] @@ -115,7 +131,7 @@ There is no length-prefix. Examples: -``` +```go type MyStruct struct{ A int B string @@ -125,7 +141,6 @@ encode(MyStruct{4, "hello", time.Time("Mon Jan 2 15:04:05 -0700 MST 2006")}) == [0x01, 0x04, 0x01, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x0F, 0xC4, 0xBB, 0xC1, 0x53, 0x03, 0x12, 0x00] ``` - ## Merkle Trees Simple Merkle trees are used in numerous places in Tendermint to compute a cryptographic digest of a data structure. @@ -134,23 +149,24 @@ RIPEMD160 is always used as the hashing function. The function `SimpleMerkleRoot` is a simple recursive function defined as follows: -``` +```go func SimpleMerkleRoot(hashes [][]byte) []byte{ - switch len(hashes) { - case 0: - return nil - case 1: - return hashes[0] - default: - left := SimpleMerkleRoot(hashes[:(len(hashes)+1)/2]) - right := SimpleMerkleRoot(hashes[(len(hashes)+1)/2:]) - return RIPEMD160(append(left, right)) - } + switch len(hashes) { + case 0: + return nil + case 1: + return hashes[0] + default: + left := SimpleMerkleRoot(hashes[:(len(hashes)+1)/2]) + right := SimpleMerkleRoot(hashes[(len(hashes)+1)/2:]) + return RIPEMD160(append(left, right)) + } } ``` -Note we abuse notion and call `SimpleMerkleRoot` with arguments of type `struct` or type `[]struct`. -For `struct` arguments, we compute a `[][]byte` by sorting elements of the `struct` according to field name and then hashing them. +Note: we abuse notion and call `SimpleMerkleRoot` with arguments of type `struct` or type `[]struct`. +For `struct` arguments, we compute a `[][]byte` by sorting elements of the `struct` according to +field name and then hashing them. For `[]struct` arguments, we compute a `[][]byte` by hashing the individual `struct` elements. ## JSON (TMJSON) @@ -158,10 +174,12 @@ For `[]struct` arguments, we compute a `[][]byte` by hashing the individual `str Signed messages (eg. votes, proposals) in the consensus are encoded in TMJSON, rather than TMBIN. TMJSON is JSON where `[]byte` are encoded as uppercase hex, rather than base64. -When signing, the elements of a message are sorted by key and the sorted message is embedded in an outer JSON that includes a `chain_id` field. -We call this encoding the CanonicalSignBytes. For instance, CanonicalSignBytes for a vote would look like: +When signing, the elements of a message are sorted by key and the sorted message is embedded in an +outer JSON that includes a `chain_id` field. +We call this encoding the CanonicalSignBytes. For instance, CanonicalSignBytes for a vote would look +like: -``` +```json {"chain_id":"my-chain-id","vote":{"block_id":{"hash":DEADBEEF,"parts":{"hash":BEEFDEAD,"total":3}},"height":3,"round":2,"timestamp":1234567890, "type":2} ``` @@ -171,8 +189,18 @@ Note how the fields within each level are sorted. ### MakeParts -TMBIN encode an object and slice it into parts. +Encode an object using TMBIN and slice it into parts. -``` +```go MakeParts(object, partSize) ``` + +### Part + +```go +type Part struct { + Index int + Bytes byte[] + Proof byte[] +} +``` diff --git a/docs/specification/new-spec/p2p/config.md b/docs/specification/new-spec/p2p/config.md new file mode 100644 index 000000000..4382b6ac4 --- /dev/null +++ b/docs/specification/new-spec/p2p/config.md @@ -0,0 +1,38 @@ +# P2P Config + +Here we describe configuration options around the Peer Exchange. +These can be set using flags or via the `$TMHOME/config/config.toml` file. + +## Seed Mode + +`--p2p.seed_mode` + +The node operates in seed mode. In seed mode, a node continuously crawls the network for peers, +and upon incoming connection shares some peers and disconnects. + +## Seeds + +`--p2p.seeds “1.2.3.4:466656,2.3.4.5:4444”` + +Dials these seeds when we need more peers. They should return a list of peers and then disconnect. +If we already have enough peers in the address book, we may never need to dial them. + +## Persistent Peers + +`--p2p.persistent_peers “1.2.3.4:46656,2.3.4.5:466656”` + +Dial these peers and auto-redial them if the connection fails. +These are intended to be trusted persistent peers that can help +anchor us in the p2p network. The auto-redial uses exponential +backoff and will give up after a day of trying to connect. + +**Note:** If `seeds` and `persistent_peers` intersect, +the user will be warned that seeds may auto-close connections +and that the node may not be able to keep the connection persistent. + +## Private Persistent Peers + +`--p2p.private_persistent_peers “1.2.3.4:46656,2.3.4.5:466656”` + +These are persistent peers that we do not add to the address book or +gossip to other peers. They stay private to us. diff --git a/docs/specification/new-spec/p2p/connection.md b/docs/specification/new-spec/p2p/connection.md new file mode 100644 index 000000000..9b5e49675 --- /dev/null +++ b/docs/specification/new-spec/p2p/connection.md @@ -0,0 +1,110 @@ +# P2P Multiplex Connection + +## MConnection + +`MConnection` is a multiplex connection that supports multiple independent streams +with distinct quality of service guarantees atop a single TCP connection. +Each stream is known as a `Channel` and each `Channel` has a globally unique *byte id*. +Each `Channel` also has a relative priority that determines the quality of service +of the `Channel` compared to other `Channel`s. +The *byte id* and the relative priorities of each `Channel` are configured upon +initialization of the connection. + +The `MConnection` supports three packet types: + +- Ping +- Pong +- Msg + +### Ping and Pong + +The ping and pong messages consist of writing a single byte to the connection; 0x1 and 0x2, respectively. + +When we haven't received any messages on an `MConnection` in time `pingTimeout`, we send a ping message. +When a ping is received on the `MConnection`, a pong is sent in response only if there are no other messages +to send and the peer has not sent us too many pings (TODO). + +If a pong or message is not received in sufficient time after a ping, the peer is disconnected from. + +### Msg + +Messages in channels are chopped into smaller `msgPacket`s for multiplexing. + +``` +type msgPacket struct { + ChannelID byte + EOF byte // 1 means message ends here. + Bytes []byte +} +``` + +The `msgPacket` is serialized using [go-wire](https://github.com/tendermint/go-wire) and prefixed with 0x3. +The received `Bytes` of a sequential set of packets are appended together +until a packet with `EOF=1` is received, then the complete serialized message +is returned for processing by the `onReceive` function of the corresponding channel. + +### Multiplexing + +Messages are sent from a single `sendRoutine`, which loops over a select statement and results in the sending +of a ping, a pong, or a batch of data messages. The batch of data messages may include messages from multiple channels. +Message bytes are queued for sending in their respective channel, with each channel holding one unsent message at a time. +Messages are chosen for a batch one at a time from the channel with the lowest ratio of recently sent bytes to channel priority. + +## Sending Messages + +There are two methods for sending messages: +```go +func (m MConnection) Send(chID byte, msg interface{}) bool {} +func (m MConnection) TrySend(chID byte, msg interface{}) bool {} +``` + +`Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued +for the channel with the given id byte `chID`. The message `msg` is serialized +using the `tendermint/wire` submodule's `WriteBinary()` reflection routine. + +`TrySend(chID, msg)` is a nonblocking call that queues the message msg in the channel +with the given id byte chID if the queue is not full; otherwise it returns false immediately. + +`Send()` and `TrySend()` are also exposed for each `Peer`. + +## Peer + +Each peer has one `MConnection` instance, and includes other information such as whether the connection +was outbound, whether the connection should be recreated if it closes, various identity information about the node, +and other higher level thread-safe data used by the reactors. + +## Switch/Reactor + +The `Switch` handles peer connections and exposes an API to receive incoming messages +on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one +or more `Channels`. So while sending outgoing messages is typically performed on the peer, +incoming messages are received on the reactor. + +```go +// Declare a MyReactor reactor that handles messages on MyChannelID. +type MyReactor struct{} + +func (reactor MyReactor) GetChannels() []*ChannelDescriptor { + return []*ChannelDescriptor{ChannelDescriptor{ID:MyChannelID, Priority: 1}} +} + +func (reactor MyReactor) Receive(chID byte, peer *Peer, msgBytes []byte) { + r, n, err := bytes.NewBuffer(msgBytes), new(int64), new(error) + msgString := ReadString(r, n, err) + fmt.Println(msgString) +} + +// Other Reactor methods omitted for brevity +... + +switch := NewSwitch([]Reactor{MyReactor{}}) + +... + +// Send a random message to all outbound connections +for _, peer := range switch.Peers().List() { + if peer.IsOutbound() { + peer.Send(MyChannelID, "Here's a random message") + } +} +``` diff --git a/docs/specification/new-spec/p2p/node.md b/docs/specification/new-spec/p2p/node.md new file mode 100644 index 000000000..589e3b436 --- /dev/null +++ b/docs/specification/new-spec/p2p/node.md @@ -0,0 +1,65 @@ +# Tendermint Peer Discovery + +A Tendermint P2P network has different kinds of nodes with different requirements for connectivity to one another. +This document describes what kind of nodes Tendermint should enable and how they should work. + +## Seeds + +Seeds are the first point of contact for a new node. +They return a list of known active peers and then disconnect. + +Seeds should operate full nodes with the PEX reactor in a "crawler" mode +that continuously explores to validate the availability of peers. + +Seeds should only respond with some top percentile of the best peers it knows about. +See [reputation](TODO) for details on peer quality. + +## New Full Node + +A new node needs a few things to connect to the network: +- a list of seeds, which can be provided to Tendermint via config file or flags, +or hardcoded into the software by in-process apps +- a `ChainID`, also called `Network` at the p2p layer +- a recent block height, H, and hash, HASH for the blockchain. + +The values `H` and `HASH` must be received and corroborated by means external to Tendermint, and specific to the user - ie. via the user's trusted social consensus. +This requirement to validate `H` and `HASH` out-of-band and via social consensus +is the essential difference in security models between Proof-of-Work and Proof-of-Stake blockchains. + +With the above, the node then queries some seeds for peers for its chain, +dials those peers, and runs the Tendermint protocols with those it successfully connects to. + +When the peer catches up to height H, it ensures the block hash matches HASH. +If not, Tendermint will exit, and the user must try again - either they are connected +to bad peers or their social consensus is invalid. + +## Restarted Full Node + +A node checks its address book on startup and attempts to connect to peers from there. +If it can't connect to any peers after some time, it falls back to the seeds to find more. + +Restarted full nodes can run the `blockchain` or `consensus` reactor protocols to sync up +to the latest state of the blockchain from wherever they were last. +In a Proof-of-Stake context, if they are sufficiently far behind (greater than the length +of the unbonding period), they will need to validate a recent `H` and `HASH` out-of-band again +so they know they have synced the correct chain. + +## Validator Node + +A validator node is a node that interfaces with a validator signing key. +These nodes require the highest security, and should not accept incoming connections. +They should maintain outgoing connections to a controlled set of "Sentry Nodes" that serve +as their proxy shield to the rest of the network. + +Validators that know and trust each other can accept incoming connections from one another and maintain direct private connectivity via VPN. + +## Sentry Node + +Sentry nodes are guardians of a validator node and provide it access to the rest of the network. +They should be well connected to other full nodes on the network. +Sentry nodes may be dynamic, but should maintain persistent connections to some evolving random subset of each other. +They should always expect to have direct incoming connections from the validator node and its backup(s). +They do not report the validator node's address in the PEX and +they may be more strict about the quality of peers they keep. + +Sentry nodes belonging to validators that trust each other may wish to maintain persistent connections via VPN with one another, but only report each other sparingly in the PEX. diff --git a/docs/specification/new-spec/p2p/peer.md b/docs/specification/new-spec/p2p/peer.md new file mode 100644 index 000000000..68615c498 --- /dev/null +++ b/docs/specification/new-spec/p2p/peer.md @@ -0,0 +1,114 @@ +# Tendermint Peers + +This document explains how Tendermint Peers are identified and how they connect to one another. + +For details on peer discovery, see the [peer exchange (PEX) reactor doc](pex.md). + +## Peer Identity + +Tendermint peers are expected to maintain long-term persistent identities in the form of a public key. +Each peer has an ID defined as `peer.ID == peer.PubKey.Address()`, where `Address` uses the scheme defined in go-crypto. + +A single peer ID can have multiple IP addresses associated with it. +TODO: define how to deal with this. + +When attempting to connect to a peer, we use the PeerURL: `@:`. +We will attempt to connect to the peer at IP:PORT, and verify, +via authenticated encryption, that it is in possession of the private key +corresponding to ``. This prevents man-in-the-middle attacks on the peer layer. + +Peers can also be connected to without specifying an ID, ie. just `:`. +In this case, the peer must be authenticated out-of-band of Tendermint, +for instance via VPN. + +## Connections + +All p2p connections use TCP. +Upon establishing a successful TCP connection with a peer, +two handhsakes are performed: one for authenticated encryption, and one for Tendermint versioning. +Both handshakes have configurable timeouts (they should complete quickly). + +### Authenticated Encryption Handshake + +Tendermint implements the Station-to-Station protocol +using ED25519 keys for Diffie-Helman key-exchange and NACL SecretBox for encryption. +It goes as follows: +- generate an emphemeral ED25519 keypair +- send the ephemeral public key to the peer +- wait to receive the peer's ephemeral public key +- compute the Diffie-Hellman shared secret using the peers ephemeral public key and our ephemeral private key +- generate two nonces to use for encryption (sending and receiving) as follows: + - sort the ephemeral public keys in ascending order and concatenate them + - RIPEMD160 the result + - append 4 empty bytes (extending the hash to 24-bytes) + - the result is nonce1 + - flip the last bit of nonce1 to get nonce2 + - if we had the smaller ephemeral pubkey, use nonce1 for receiving, nonce2 for sending; + else the opposite +- all communications from now on are encrypted using the shared secret and the nonces, where each nonce +increments by 2 every time it is used +- we now have an encrypted channel, but still need to authenticate +- generate a common challenge to sign: + - SHA256 of the sorted (lowest first) and concatenated ephemeral pub keys +- sign the common challenge with our persistent private key +- send the go-wire encoded persistent pubkey and signature to the peer +- wait to receive the persistent public key and signature from the peer +- verify the signature on the challenge using the peer's persistent public key + + +If this is an outgoing connection (we dialed the peer) and we used a peer ID, +then finally verify that the peer's persistent public key corresponds to the peer ID we dialed, +ie. `peer.PubKey.Address() == `. + +The connection has now been authenticated. All traffic is encrypted. + +Note: only the dialer can authenticate the identity of the peer, +but this is what we care about since when we join the network we wish to +ensure we have reached the intended peer (and are not being MITMd). + +### Peer Filter + +Before continuing, we check if the new peer has the same ID as ourselves or +an existing peer. If so, we disconnect. + +We also check the peer's address and public key against +an optional whitelist which can be managed through the ABCI app - +if the whitelist is enabled and the peer does not qualify, the connection is +terminated. + + +### Tendermint Version Handshake + +The Tendermint Version Handshake allows the peers to exchange their NodeInfo: + +```golang +type NodeInfo struct { + PubKey crypto.PubKey + Moniker string + Network string + RemoteAddr string + ListenAddr string + Version string + Channels []int8 + Other []string +} +``` + +The connection is disconnected if: +- `peer.NodeInfo.PubKey != peer.PubKey` +- `peer.NodeInfo.Version` is not formatted as `X.X.X` where X are integers known as Major, Minor, and Revision +- `peer.NodeInfo.Version` Major is not the same as ours +- `peer.NodeInfo.Version` Minor is not the same as ours +- `peer.NodeInfo.Network` is not the same as ours +- `peer.Channels` does not intersect with our known Channels. + + +At this point, if we have not disconnected, the peer is valid. +It is added to the switch and hence all reactors via the `AddPeer` method. +Note that each reactor may handle multiple channels. + +## Connection Activity + +Once a peer is added, incoming messages for a given reactor are handled through +that reactor's `Receive` method, and output messages are sent directly by the Reactors +on each peer. A typical reactor maintains per-peer go-routine(s) that handle this. diff --git a/docs/specification/new-spec/reactors/block_sync/impl.md b/docs/specification/new-spec/reactors/block_sync/impl.md new file mode 100644 index 000000000..a96f83b32 --- /dev/null +++ b/docs/specification/new-spec/reactors/block_sync/impl.md @@ -0,0 +1,46 @@ +## Blockchain Reactor + +* coordinates the pool for syncing +* coordinates the store for persistence +* coordinates the playing of blocks towards the app using a sm.BlockExecutor +* handles switching between fastsync and consensus +* it is a p2p.BaseReactor +* starts the pool.Start() and its poolRoutine() +* registers all the concrete types and interfaces for serialisation + +### poolRoutine + +* listens to these channels: + * pool requests blocks from a specific peer by posting to requestsCh, block reactor then sends + a &bcBlockRequestMessage for a specific height + * pool signals timeout of a specific peer by posting to timeoutsCh + * switchToConsensusTicker to periodically try and switch to consensus + * trySyncTicker to periodically check if we have fallen behind and then catch-up sync + * if there aren't any new blocks available on the pool it skips syncing +* tries to sync the app by taking downloaded blocks from the pool, gives them to the app and stores + them on disk +* 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 + +* responsible for downloading blocks from peers +* makeRequestersRoutine() + * removes timeout peers + * starts new requesters by calling makeNextRequester() +* requestRoutine(): + * picks a peer and sends the request, then blocks until: + * pool is stopped by listening to pool.Quit + * requester is stopped by listening to Quit + * request is redone + * we receive a block + * gotBlockCh is strange + +## Block Store + +* persists blocks to disk + +# TODO + +* How does the switch from bcR to conR happen? Does conR persist blocks to disk too? +* What is the interaction between the consensus and blockchain reactors? diff --git a/docs/specification/new-spec/reactors/block_sync/reactor.md b/docs/specification/new-spec/reactors/block_sync/reactor.md new file mode 100644 index 000000000..c00ea96f3 --- /dev/null +++ b/docs/specification/new-spec/reactors/block_sync/reactor.md @@ -0,0 +1,49 @@ +# Blockchain Reactor + +The Blockchain Reactor's high level responsibility is to enable peers who are +far behind the current state of the consensus to quickly catch up by downloading +many blocks in parallel, verifying their commits, and executing them against the +ABCI application. + +Tendermint full nodes run the Blockchain Reactor as a service to provide blocks +to new nodes. New nodes run the Blockchain Reactor in "fast_sync" mode, +where they actively make requests for more blocks until they sync up. +Once caught up, "fast_sync" mode is disabled and the node switches to +using (and turns on) the Consensus Reactor. + +## Message Types + +```go +const ( + msgTypeBlockRequest = byte(0x10) + msgTypeBlockResponse = byte(0x11) + msgTypeNoBlockResponse = byte(0x12) + msgTypeStatusResponse = byte(0x20) + msgTypeStatusRequest = byte(0x21) +) +``` + +```go +type bcBlockRequestMessage struct { + Height int64 +} + +type bcNoBlockResponseMessage struct { + Height int64 +} + +type bcBlockResponseMessage struct { + Block Block +} + +type bcStatusRequestMessage struct { + Height int64 + +type bcStatusResponseMessage struct { + Height int64 +} +``` + +## Protocol + +TODO diff --git a/docs/specification/new-spec/reactors/consensus/consensus-reactor.md b/docs/specification/new-spec/reactors/consensus/consensus-reactor.md new file mode 100644 index 000000000..69ca409b4 --- /dev/null +++ b/docs/specification/new-spec/reactors/consensus/consensus-reactor.md @@ -0,0 +1,344 @@ +# Consensus Reactor + +Consensus Reactor defines a reactor for the consensus service. It contains the ConsensusState service that +manages the state of the Tendermint consensus internal state machine. +When Consensus Reactor is started, it starts Broadcast Routine which starts ConsensusState service. +Furthermore, for each peer that is added to the Consensus Reactor, it creates (and manages) the known peer state +(that is used extensively in gossip routines) and starts the following three routines for the peer p: +Gossip Data Routine, Gossip Votes Routine and QueryMaj23Routine. Finally, Consensus Reactor is responsible +for decoding messages received from a peer and for adequate processing of the message depending on its type and content. +The processing normally consists of updating the known peer state and for some messages +(`ProposalMessage`, `BlockPartMessage` and `VoteMessage`) also forwarding message to ConsensusState module +for further processing. In the following text we specify the core functionality of those separate unit of executions +that are part of the Consensus Reactor. + +## ConsensusState service + +Consensus State handles execution of the Tendermint BFT consensus algorithm. It processes votes and proposals, +and upon reaching agreement, commits blocks to the chain and executes them against the application. +The internal state machine receives input from peers, the internal validator and from a timer. + +Inside Consensus State we have the following units of execution: Timeout Ticker and Receive Routine. +Timeout Ticker is a timer that schedules timeouts conditional on the height/round/step that are processed +by the Receive Routine. + + +### Receive Routine of the ConsensusState service + +Receive Routine of the ConsensusState handles messages which may cause internal consensus state transitions. +It is the only routine that updates RoundState that contains internal consensus state. +Updates (state transitions) happen on timeouts, complete proposals, and 2/3 majorities. +It receives messages from peers, internal validators and from Timeout Ticker +and invokes the corresponding handlers, potentially updating the RoundState. +The details of the protocol (together with formal proofs of correctness) implemented by the Receive Routine are +discussed in separate document (see [spec](https://github.com/tendermint/spec)). For understanding of this document +it is sufficient to understand that the Receive Routine manages and updates RoundState data structure that is +then extensively used by the gossip routines to determine what information should be sent to peer processes. + +## Round State + +RoundState defines the internal consensus state. It contains height, round, round step, a current validator set, +a proposal and proposal block for the current round, locked round and block (if some block is being locked), set of +received votes and last commit and last validators set. + +```golang +type RoundState struct { + Height int64 + Round int + Step RoundStepType + Validators ValidatorSet + Proposal Proposal + ProposalBlock Block + ProposalBlockParts PartSet + LockedRound int + LockedBlock Block + LockedBlockParts PartSet + Votes HeightVoteSet + LastCommit VoteSet + LastValidators ValidatorSet +} +``` + +Internally, consensus will run as a state machine with the following states: + +- RoundStepNewHeight +- RoundStepNewRound +- RoundStepPropose +- RoundStepProposeWait +- RoundStepPrevote +- RoundStepPrevoteWait +- RoundStepPrecommit +- RoundStepPrecommitWait +- RoundStepCommit + +## Peer Round State + +Peer round state contains the known state of a peer. It is being updated by the Receive routine of +Consensus Reactor and by the gossip routines upon sending a message to the peer. + +```golang +type PeerRoundState struct { + Height int64 // Height peer is at + Round int // Round peer is at, -1 if unknown. + Step RoundStepType // Step peer is at + Proposal bool // True if peer has proposal for this round + ProposalBlockPartsHeader PartSetHeader + ProposalBlockParts BitArray + ProposalPOLRound int // Proposal's POL round. -1 if none. + ProposalPOL BitArray // nil until ProposalPOLMessage received. + Prevotes BitArray // All votes peer has for this round + Precommits BitArray // All precommits peer has for this round + LastCommitRound int // Round of commit for last height. -1 if none. + LastCommit BitArray // All commit precommits of commit for last height. + CatchupCommitRound int // Round that we have commit for. Not necessarily unique. -1 if none. + CatchupCommit BitArray // All commit precommits peer has for this height & CatchupCommitRound +} +``` + +## Receive method of Consensus reactor + +The entry point of the Consensus reactor is a receive method. When a message is received from a peer p, +normally the peer round state is updated correspondingly, and some messages +are passed for further processing, for example to ConsensusState service. We now specify the processing of messages +in the receive method of Consensus reactor for each message type. In the following message handler, `rs` and `prs` denote +`RoundState` and `PeerRoundState`, respectively. + +### NewRoundStepMessage handler + +``` +handleMessage(msg): + if msg is from smaller height/round/step then return + // Just remember these values. + prsHeight = prs.Height + prsRound = prs.Round + prsCatchupCommitRound = prs.CatchupCommitRound + prsCatchupCommit = prs.CatchupCommit + + Update prs with values from msg + if prs.Height or prs.Round has been updated then + reset Proposal related fields of the peer state + if prs.Round has been updated and msg.Round == prsCatchupCommitRound then + prs.Precommits = psCatchupCommit + if prs.Height has been updated then + if prsHeight+1 == msg.Height && prsRound == msg.LastCommitRound then + prs.LastCommitRound = msg.LastCommitRound + prs.LastCommit = prs.Precommits + } else { + prs.LastCommitRound = msg.LastCommitRound + prs.LastCommit = nil + } + Reset prs.CatchupCommitRound and prs.CatchupCommit +``` + +### CommitStepMessage handler + +``` +handleMessage(msg): + if prs.Height == msg.Height then + prs.ProposalBlockPartsHeader = msg.BlockPartsHeader + prs.ProposalBlockParts = msg.BlockParts +``` + +### HasVoteMessage handler + +``` +handleMessage(msg): + if prs.Height == msg.Height then + prs.setHasVote(msg.Height, msg.Round, msg.Type, msg.Index) +``` + +### VoteSetMaj23Message handler + +``` +handleMessage(msg): + if prs.Height == msg.Height then + Record in rs that a peer claim to have ⅔ majority for msg.BlockID + Send VoteSetBitsMessage showing votes node has for that BlockId +``` + +### ProposalMessage handler + +``` +handleMessage(msg): + if prs.Height != msg.Height || prs.Round != msg.Round || prs.Proposal then return + prs.Proposal = true + prs.ProposalBlockPartsHeader = msg.BlockPartsHeader + prs.ProposalBlockParts = empty set + prs.ProposalPOLRound = msg.POLRound + prs.ProposalPOL = nil + Send msg through internal peerMsgQueue to ConsensusState service +``` + +### ProposalPOLMessage handler + +``` +handleMessage(msg): + if prs.Height != msg.Height or prs.ProposalPOLRound != msg.ProposalPOLRound then return + prs.ProposalPOL = msg.ProposalPOL +``` + +### BlockPartMessage handler + +``` +handleMessage(msg): + if prs.Height != msg.Height || prs.Round != msg.Round then return + Record in prs that peer has block part msg.Part.Index + Send msg trough internal peerMsgQueue to ConsensusState service +``` + +### VoteMessage handler + +``` +handleMessage(msg): + Record in prs that a peer knows vote with index msg.vote.ValidatorIndex for particular height and round + Send msg trough internal peerMsgQueue to ConsensusState service +``` + +### VoteSetBitsMessage handler + +``` +handleMessage(msg): + Update prs for the bit-array of votes peer claims to have for the msg.BlockID +``` + +## Gossip Data Routine + +It is used to send the following messages to the peer: `BlockPartMessage`, `ProposalMessage` and +`ProposalPOLMessage` on the DataChannel. The gossip data routine is based on the local RoundState (`rs`) +and the known PeerRoundState (`prs`). The routine repeats forever the logic shown below: + +``` +1a) if rs.ProposalBlockPartsHeader == prs.ProposalBlockPartsHeader and the peer does not have all the proposal parts then + Part = pick a random proposal block part the peer does not have + Send BlockPartMessage(rs.Height, rs.Round, Part) to the peer on the DataChannel + if send returns true, record that the peer knows the corresponding block Part + Continue + +1b) if (0 < prs.Height) and (prs.Height < rs.Height) then + help peer catch up using gossipDataForCatchup function + Continue + +1c) if (rs.Height != prs.Height) or (rs.Round != prs.Round) then + Sleep PeerGossipSleepDuration + Continue + +// at this point rs.Height == prs.Height and rs.Round == prs.Round +1d) if (rs.Proposal != nil and !prs.Proposal) then + Send ProposalMessage(rs.Proposal) to the peer + if send returns true, record that the peer knows Proposal + if 0 <= rs.Proposal.POLRound then + polRound = rs.Proposal.POLRound + prevotesBitArray = rs.Votes.Prevotes(polRound).BitArray() + Send ProposalPOLMessage(rs.Height, polRound, prevotesBitArray) + Continue + +2) Sleep PeerGossipSleepDuration +``` + +### Gossip Data For Catchup + +This function is responsible for helping peer catch up if it is at the smaller height (prs.Height < rs.Height). +The function executes the following logic: + + if peer does not have all block parts for prs.ProposalBlockPart then + blockMeta = Load Block Metadata for height prs.Height from blockStore + if (!blockMeta.BlockID.PartsHeader == prs.ProposalBlockPartsHeader) then + Sleep PeerGossipSleepDuration + return + Part = pick a random proposal block part the peer does not have + Send BlockPartMessage(prs.Height, prs.Round, Part) to the peer on the DataChannel + if send returns true, record that the peer knows the corresponding block Part + return + else Sleep PeerGossipSleepDuration + +## Gossip Votes Routine + +It is used to send the following message: `VoteMessage` on the VoteChannel. +The gossip votes routine is based on the local RoundState (`rs`) +and the known PeerRoundState (`prs`). The routine repeats forever the logic shown below: + +``` +1a) if rs.Height == prs.Height then + if prs.Step == RoundStepNewHeight then + vote = random vote from rs.LastCommit the peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + + if prs.Step <= RoundStepPrevote and prs.Round != -1 and prs.Round <= rs.Round then + Prevotes = rs.Votes.Prevotes(prs.Round) + vote = random vote from Prevotes the peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + + if prs.Step <= RoundStepPrecommit and prs.Round != -1 and prs.Round <= rs.Round then + Precommits = rs.Votes.Precommits(prs.Round) + vote = random vote from Precommits the peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + + if prs.ProposalPOLRound != -1 then + PolPrevotes = rs.Votes.Prevotes(prs.ProposalPOLRound) + vote = random vote from PolPrevotes the peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + +1b) if prs.Height != 0 and rs.Height == prs.Height+1 then + vote = random vote from rs.LastCommit peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + +1c) if prs.Height != 0 and rs.Height >= prs.Height+2 then + Commit = get commit from BlockStore for prs.Height + vote = random vote from Commit the peer does not have + Send VoteMessage(vote) to the peer + if send returns true, continue + +2) Sleep PeerGossipSleepDuration +``` + +## QueryMaj23Routine + +It is used to send the following message: `VoteSetMaj23Message`. `VoteSetMaj23Message` is sent to indicate that a given +BlockID has seen +2/3 votes. This routine is based on the local RoundState (`rs`) and the known PeerRoundState +(`prs`). The routine repeats forever the logic shown below. + +``` +1a) if rs.Height == prs.Height then + Prevotes = rs.Votes.Prevotes(prs.Round) + if there is a ⅔ majority for some blockId in Prevotes then + m = VoteSetMaj23Message(prs.Height, prs.Round, Prevote, blockId) + Send m to peer + Sleep PeerQueryMaj23SleepDuration + +1b) if rs.Height == prs.Height then + Precommits = rs.Votes.Precommits(prs.Round) + if there is a ⅔ majority for some blockId in Precommits then + m = VoteSetMaj23Message(prs.Height,prs.Round,Precommit,blockId) + Send m to peer + Sleep PeerQueryMaj23SleepDuration + +1c) if rs.Height == prs.Height and prs.ProposalPOLRound >= 0 then + Prevotes = rs.Votes.Prevotes(prs.ProposalPOLRound) + if there is a ⅔ majority for some blockId in Prevotes then + m = VoteSetMaj23Message(prs.Height,prs.ProposalPOLRound,Prevotes,blockId) + Send m to peer + Sleep PeerQueryMaj23SleepDuration + +1d) if prs.CatchupCommitRound != -1 and 0 < prs.Height and + prs.Height <= blockStore.Height() then + Commit = LoadCommit(prs.Height) + m = VoteSetMaj23Message(prs.Height,Commit.Round,Precommit,Commit.blockId) + Send m to peer + Sleep PeerQueryMaj23SleepDuration + +2) Sleep PeerQueryMaj23SleepDuration +``` + +## Broadcast routine + +The Broadcast routine subscribes to an internal event bus to receive new round steps, votes messages and proposal +heartbeat messages, and broadcasts messages to peers upon receiving those events. +It broadcasts `NewRoundStepMessage` or `CommitStepMessage` upon new round state event. Note that +broadcasting these messages does not depend on the PeerRoundState; it is sent on the StateChannel. +Upon receiving VoteMessage it broadcasts `HasVoteMessage` message to its peers on the StateChannel. +`ProposalHeartbeatMessage` is sent the same way on the StateChannel. diff --git a/docs/specification/new-spec/reactors/consensus/consensus.md b/docs/specification/new-spec/reactors/consensus/consensus.md new file mode 100644 index 000000000..1f311c44e --- /dev/null +++ b/docs/specification/new-spec/reactors/consensus/consensus.md @@ -0,0 +1,212 @@ +# Tendermint Consensus Reactor + +Tendermint Consensus is a distributed protocol executed by validator processes to agree on +the next block to be added to the Tendermint blockchain. The protocol proceeds in rounds, where +each round is a try to reach agreement on the next block. A round starts by having a dedicated +process (called proposer) suggesting to other processes what should be the next block with +the `ProposalMessage`. +The processes respond by voting for a block with `VoteMessage` (there are two kinds of vote +messages, prevote and precommit votes). Note that a proposal message is just a suggestion what the +next block should be; a validator might vote with a `VoteMessage` for a different block. If in some +round, enough number of processes vote for the same block, then this block is committed and later +added to the blockchain. `ProposalMessage` and `VoteMessage` are signed by the private key of the +validator. The internals of the protocol and how it ensures safety and liveness properties are +explained [here](https://github.com/tendermint/spec). + +For efficiency reasons, validators in Tendermint consensus protocol do not agree directly on the +block as the block size is big, i.e., they don't embed the block inside `Proposal` and +`VoteMessage`. Instead, they reach agreement on the `BlockID` (see `BlockID` definition in +[Blockchain](blockchain.md) section) that uniquely identifies each block. The block itself is +disseminated to validator processes using peer-to-peer gossiping protocol. It starts by having a +proposer first splitting a block into a number of block parts, that are then gossiped between +processes using `BlockPartMessage`. + +Validators in Tendermint communicate by peer-to-peer gossiping protocol. Each validator is connected +only to a subset of processes called peers. By the gossiping protocol, a validator send to its peers +all needed information (`ProposalMessage`, `VoteMessage` and `BlockPartMessage`) so they can +reach agreement on some block, and also obtain the content of the chosen block (block parts). As +part of the gossiping protocol, processes also send auxiliary messages that inform peers about the +executed steps of the core consensus algorithm (`NewRoundStepMessage` and `CommitStepMessage`), and +also messages that inform peers what votes the process has seen (`HasVoteMessage`, +`VoteSetMaj23Message` and `VoteSetBitsMessage`). These messages are then used in the gossiping +protocol to determine what messages a process should send to its peers. + +We now describe the content of each message exchanged during Tendermint consensus protocol. + +## ProposalMessage + +ProposalMessage is sent when a new block is proposed. It is a suggestion of what the +next block in the blockchain should be. + +```go +type ProposalMessage struct { + Proposal Proposal +} +``` + +### Proposal + +Proposal contains height and round for which this proposal is made, BlockID as a unique identifier +of proposed block, timestamp, and two fields (POLRound and POLBlockID) that are needed for +termination of the consensus. The message is signed by the validator private key. + +```go +type Proposal struct { + Height int64 + Round int + Timestamp Time + BlockID BlockID + POLRound int + POLBlockID BlockID + Signature Signature +} +``` + +NOTE: In the current version of the Tendermint, the consensus value in proposal is represented with +PartSetHeader, and with BlockID in vote message. It should be aligned as suggested in this spec as +BlockID contains PartSetHeader. + +## VoteMessage + +VoteMessage is sent to vote for some block (or to inform others that a process does not vote in the +current round). Vote is defined in [Blockchain](blockchain.md) section and contains validator's +information (validator address and index), height and round for which the vote is sent, vote type, +blockID if process vote for some block (`nil` otherwise) and a timestamp when the vote is sent. The +message is signed by the validator private key. + +```go +type VoteMessage struct { + Vote Vote +} +``` + +## BlockPartMessage + +BlockPartMessage is sent when gossipping a piece of the proposed block. It contains height, round +and the block part. + +```go +type BlockPartMessage struct { + Height int64 + Round int + Part Part +} +``` + +## ProposalHeartbeatMessage + +ProposalHeartbeatMessage is sent to signal that a node is alive and waiting for transactions +to be able to create a next block proposal. + +```go +type ProposalHeartbeatMessage struct { + Heartbeat Heartbeat +} +``` + +### Heartbeat + +Heartbeat contains validator information (address and index), +height, round and sequence number. It is signed by the private key of the validator. + +```go +type Heartbeat struct { + ValidatorAddress []byte + ValidatorIndex int + Height int64 + Round int + Sequence int + Signature Signature +} +``` + +## NewRoundStepMessage + +NewRoundStepMessage is sent for every step transition during the core consensus algorithm execution. +It is used in the gossip part of the Tendermint protocol to inform peers about a current +height/round/step a process is in. + +```go +type NewRoundStepMessage struct { + Height int64 + Round int + Step RoundStepType + SecondsSinceStartTime int + LastCommitRound int +} +``` + +## CommitStepMessage + +CommitStepMessage is sent when an agreement on some block is reached. It contains height for which +agreement is reached, block parts header that describes the decided block and is used to obtain all +block parts, and a bit array of the block parts a process currently has, so its peers can know what +parts it is missing so they can send them. + +```go +type CommitStepMessage struct { + Height int64 + BlockID BlockID + BlockParts BitArray +} +``` + +TODO: We use BlockID instead of BlockPartsHeader (in current implementation) for symmetry. + +## ProposalPOLMessage + +ProposalPOLMessage is sent when a previous block is re-proposed. +It is used to inform peers in what round the process learned for this block (ProposalPOLRound), +and what prevotes for the re-proposed block the process has. + +```go +type ProposalPOLMessage struct { + Height int64 + ProposalPOLRound int + ProposalPOL BitArray +} +``` + +## HasVoteMessage + +HasVoteMessage is sent to indicate that a particular vote has been received. It contains height, +round, vote type and the index of the validator that is the originator of the corresponding vote. + +```go +type HasVoteMessage struct { + Height int64 + Round int + Type byte + Index int +} +``` + +## VoteSetMaj23Message + +VoteSetMaj23Message is sent to indicate that a process has seen +2/3 votes for some BlockID. +It contains height, round, vote type and the BlockID. + +```go +type VoteSetMaj23Message struct { + Height int64 + Round int + Type byte + BlockID BlockID +} +``` + +## VoteSetBitsMessage + +VoteSetBitsMessage is sent to communicate the bit-array of votes a process has seen for a given +BlockID. It contains height, round, vote type, BlockID and a bit array of +the votes a process has. + +```go +type VoteSetBitsMessage struct { + Height int64 + Round int + Type byte + BlockID BlockID + Votes BitArray +} +``` diff --git a/docs/specification/new-spec/reactors/consensus/proposer-selection.md b/docs/specification/new-spec/reactors/consensus/proposer-selection.md new file mode 100644 index 000000000..01fa95b8a --- /dev/null +++ b/docs/specification/new-spec/reactors/consensus/proposer-selection.md @@ -0,0 +1,47 @@ +# Proposer selection procedure in Tendermint + +This document specifies the Proposer Selection Procedure that is used in Tendermint to choose a round proposer. +As Tendermint is “leader-based protocol”, the proposer selection is critical for its correct functioning. +Let denote with `proposer_p(h,r)` a process returned by the Proposer Selection Procedure at the process p, at height h +and round r. Then the Proposer Selection procedure should fulfill the following properties: + +`Agreement`: Given a validator set V, and two honest validators, +p and q, for each height h, and each round r, +proposer_p(h,r) = proposer_q(h,r) + +`Liveness`: In every consecutive sequence of rounds of size K (K is system parameter), at least a +single round has an honest proposer. + +`Fairness`: The proposer selection is proportional to the validator voting power, i.e., a validator with more +voting power is selected more frequently, proportional to its power. More precisely, given a set of processes +with the total voting power N, during a sequence of rounds of size N, every process is proposer in a number of rounds +equal to its voting power. + +We now look at a few particular cases to understand better how fairness should be implemented. +If we have 4 processes with the following voting power distribution (p0,4), (p1, 2), (p2, 2), (p3, 2) at some round r, +we have the following sequence of proposer selections in the following rounds: + +`p0, p1, p2, p3, p0, p0, p1, p2, p3, p0, p0, p1, p2, p3, p0, p0, p1, p2, p3, p0, etc` + +Let consider now the following scenario where a total voting power of faulty processes is aggregated in a single process +p0: (p0,3), (p1, 1), (p2, 1), (p3, 1), (p4, 1), (p5, 1), (p6, 1), (p7, 1). +In this case the sequence of proposer selections looks like this: + +`p0, p1, p2, p3, p0, p4, p5, p6, p7, p0, p0, p1, p2, p3, p0, p4, p5, p6, p7, p0, etc` + +In this case, we see that a number of rounds coordinated by a faulty process is proportional to its voting power. +We consider also the case where we have voting power uniformly distributed among processes, i.e., we have 10 processes +each with voting power of 1. And let consider that there are 3 faulty processes with consecutive addresses, +for example the first 3 processes are faulty. Then the sequence looks like this: + +`p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, etc` + +In this case, we have 3 consecutive rounds with a faulty proposer. +One special case we consider is the case where a single honest process p0 has most of the voting power, for example: +(p0,100), (p1, 2), (p2, 3), (p3, 4). Then the sequence of proposer selection looks like this: + +p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p0, p1, p0, p0, p0, p0, p0, etc + +This basically means that almost all rounds have the same proposer. But in this case, the process p0 has anyway enough +voting power to decide whatever he wants, so the fact that he coordinates almost all rounds seems correct. + diff --git a/docs/specification/new-spec/reactors/mempool/README.md b/docs/specification/new-spec/reactors/mempool/README.md new file mode 100644 index 000000000..138b287a5 --- /dev/null +++ b/docs/specification/new-spec/reactors/mempool/README.md @@ -0,0 +1,11 @@ +# Mempool Specification + +This package contains documents specifying the functionality +of the mempool module. + +Components: + +* [Config](./config.md) - how to configure it +* [External Messages](./messages.md) - The messages we accept over p2p and rpc interfaces +* [Functionality](./functionality.md) - high-level description of the functionality it provides +* [Concurrency Model](./concurrency.md) - What guarantees we provide, what locks we require. diff --git a/docs/specification/new-spec/reactors/mempool/concurrency.md b/docs/specification/new-spec/reactors/mempool/concurrency.md new file mode 100644 index 000000000..991113e6d --- /dev/null +++ b/docs/specification/new-spec/reactors/mempool/concurrency.md @@ -0,0 +1,8 @@ +# Mempool Concurrency + +Look at the concurrency model this uses... + +* Receiving CheckTx +* Broadcasting new tx +* Interfaces with consensus engine, reap/update while checking +* Calling the ABCI app (ordering. callbacks. how proxy works alongside the blockchain proxy which actually writes blocks) diff --git a/docs/specification/new-spec/reactors/mempool/config.md b/docs/specification/new-spec/reactors/mempool/config.md new file mode 100644 index 000000000..776149ba0 --- /dev/null +++ b/docs/specification/new-spec/reactors/mempool/config.md @@ -0,0 +1,59 @@ +# Mempool Configuration + +Here we describe configuration options around mempool. +For the purposes of this document, they are described +as command-line flags, but they can also be passed in as +environmental variables or in the config.toml file. The +following are all equivalent: + +Flag: `--mempool.recheck_empty=false` + +Environment: `TM_MEMPOOL_RECHECK_EMPTY=false` + +Config: +``` +[mempool] +recheck_empty = false +``` + + +## Recheck + +`--mempool.recheck=false` (default: true) + +`--mempool.recheck_empty=false` (default: true) + +Recheck determines if the mempool rechecks all pending +transactions after a block was committed. Once a block +is committed, the mempool removes all valid transactions +that were successfully included in the block. + +If `recheck` is true, then it will rerun CheckTx on +all remaining transactions with the new block state. + +If the block contained no transactions, it will skip the +recheck unless `recheck_empty` is true. + +## Broadcast + +`--mempool.broadcast=false` (default: true) + +Determines whether this node gossips any valid transactions +that arrive in mempool. Default is to gossip anything that +passes checktx. If this is disabled, transactions are not +gossiped, but instead stored locally and added to the next +block this node is the proposer. + +## WalDir + +`--mempool.wal_dir=/tmp/gaia/mempool.wal` (default: $TM_HOME/data/mempool.wal) + +This defines the directory where mempool writes the write-ahead +logs. These files can be used to reload unbroadcasted +transactions if the node crashes. + +If the directory passed in is an absolute path, the wal file is +created there. If the directory is a relative path, the path is +appended to home directory of the tendermint process to +generate an absolute path to the wal directory +(default `$HOME/.tendermint` or set via `TM_HOME` or `--home``) diff --git a/docs/specification/new-spec/reactors/mempool/functionality.md b/docs/specification/new-spec/reactors/mempool/functionality.md new file mode 100644 index 000000000..85c3dc58d --- /dev/null +++ b/docs/specification/new-spec/reactors/mempool/functionality.md @@ -0,0 +1,37 @@ +# Mempool Functionality + +The mempool maintains a list of potentially valid transactions, +both to broadcast to other nodes, as well as to provide to the +consensus reactor when it is selected as the block proposer. + +There are two sides to the mempool state: + +* External: get, check, and broadcast new transactions +* Internal: return valid transaction, update list after block commit + + +## External functionality + +External functionality is exposed via network interfaces +to potentially untrusted actors. + +* CheckTx - triggered via RPC or P2P +* Broadcast - gossip messages after a successful check + +## Internal functionality + +Internal functionality is exposed via method calls to other +code compiled into the tendermint binary. + +* Reap - get tx to propose in next block +* Update - remove tx that were included in last block +* ABCI.CheckTx - call ABCI app to validate the tx + +What does it provide the consensus reactor? +What guarantees does it need from the ABCI app? +(talk about interleaving processes in concurrency) + +## Optimizations + +Talk about the LRU cache to make sure we don't process any +tx that we have seen before diff --git a/docs/specification/new-spec/reactors/mempool/messages.md b/docs/specification/new-spec/reactors/mempool/messages.md new file mode 100644 index 000000000..5bd1d1e55 --- /dev/null +++ b/docs/specification/new-spec/reactors/mempool/messages.md @@ -0,0 +1,60 @@ +# Mempool Messages + +## P2P Messages + +There is currently only one message that Mempool broadcasts +and receives over the p2p gossip network (via the reactor): +`TxMessage` + +```go +// TxMessage is a MempoolMessage containing a transaction. +type TxMessage struct { + Tx types.Tx +} +``` + +TxMessage is go-wire encoded and prepended with `0x1` as a +"type byte". This is followed by a go-wire encoded byte-slice. +Prefix of 40=0x28 byte tx is: `0x010128...` followed by +the actual 40-byte tx. Prefix of 350=0x015e byte tx is: +`0x0102015e...` followed by the actual 350 byte tx. + +(Please see the [go-wire repo](https://github.com/tendermint/go-wire#an-interface-example) for more information) + +## RPC Messages + +Mempool exposes `CheckTx([]byte)` over the RPC interface. + +It can be posted via `broadcast_commit`, `broadcast_sync` or +`broadcast_async`. They all parse a message with one argument, +`"tx": "HEX_ENCODED_BINARY"` and differ in only how long they +wait before returning (sync makes sure CheckTx passes, commit +makes sure it was included in a signed block). + +Request (`POST http://gaia.zone:46657/`): +```json +{ + "id": "", + "jsonrpc": "2.0", + "method": "broadcast_sync", + "params": { + "tx": "F012A4BC68..." + } +} +``` + + +Response: +```json +{ + "error": "", + "result": { + "hash": "E39AAB7A537ABAA237831742DCE1117F187C3C52", + "log": "", + "data": "", + "code": 0 + }, + "id": "", + "jsonrpc": "2.0" +} +``` diff --git a/docs/specification/new-spec/reactors/pex/pex.md b/docs/specification/new-spec/reactors/pex/pex.md new file mode 100644 index 000000000..43d6f80d7 --- /dev/null +++ b/docs/specification/new-spec/reactors/pex/pex.md @@ -0,0 +1,98 @@ +# Peer Strategy and Exchange + +Here we outline the design of the AddressBook +and how it used by the Peer Exchange Reactor (PEX) to ensure we are connected +to good peers and to gossip peers to others. + +## Peer Types + +Certain peers are special in that they are specified by the user as `persistent`, +which means we auto-redial them if the connection fails. +Some peers can be marked as `private`, which means +we will not put them in the address book or gossip them to others. + +All peers except private peers are tracked using the address book. + +## Discovery + +Peer discovery begins with a list of seeds. +When we have no peers, or have been unable to find enough peers from existing ones, +we dial a randomly selected seed to get a list of peers to dial. + +So long as we have less than `MaxPeers`, we periodically request additional peers +from each of our own. If sufficient time goes by and we still can't find enough peers, +we try the seeds again. + +## Address Book + +Peers are tracked via their ID (their PubKey.Address()). +For each ID, the address book keeps the most recent IP:PORT. +Peers are added to the address book from the PEX when they first connect to us or +when we hear about them from other peers. + +The address book is arranged in sets of buckets, and distinguishes between +vetted (old) and unvetted (new) peers. It keeps different sets of buckets for vetted and +unvetted peers. Buckets provide randomization over peer selection. + +A vetted peer can only be in one bucket. An unvetted peer can be in multiple buckets. + +## Vetting + +When a peer is first added, it is unvetted. +Marking a peer as vetted is outside the scope of the `p2p` package. +For Tendermint, a Peer becomes vetted once it has contributed sufficiently +at the consensus layer; ie. once it has sent us valid and not-yet-known +votes and/or block parts for `NumBlocksForVetted` blocks. +Other users of the p2p package can determine their own conditions for when a peer is marked vetted. + +If a peer becomes vetted but there are already too many vetted peers, +a randomly selected one of the vetted peers becomes unvetted. + +If a peer becomes unvetted (either a new peer, or one that was previously vetted), +a randomly selected one of the unvetted peers is removed from the address book. + +More fine-grained tracking of peer behaviour can be done using +a trust metric (see below), but it's best to start with something simple. + +## Select Peers to Dial + +When we need more peers, we pick them randomly from the addrbook with some +configurable bias for unvetted peers. The bias should be lower when we have fewer peers, +and can increase as we obtain more, ensuring that our first peers are more trustworthy, +but always giving us the chance to discover new good peers. + +## Select Peers to Exchange + +When we’re asked for peers, we select them as follows: +- select at most `maxGetSelection` peers +- try to select at least `minGetSelection` peers - if we have less than that, select them all. +- select a random, unbiased `getSelectionPercent` of the peers + +Send the selected peers. Note we select peers for sending without bias for vetted/unvetted. + +## Preventing Spam + +There are various cases where we decide a peer has misbehaved and we disconnect from them. +When this happens, the peer is removed from the address book and black listed for +some amount of time. We call this "Disconnect and Mark". +Note that the bad behaviour may be detected outside the PEX reactor itself +(for instance, in the mconnection, or another reactor), but it must be communicated to the PEX reactor +so it can remove and mark the peer. + +In the PEX, if a peer sends us unsolicited lists of peers, +or if the peer sends too many requests for more peers in a given amount of time, +we Disconnect and Mark. + +## Trust Metric + +The quality of peers can be tracked in more fine-grained detail using a +Proportional-Integral-Derivative (PID) controller that incorporates +current, past, and rate-of-change data to inform peer quality. + +While a PID trust metric has been implemented, it remains for future work +to use it in the PEX. + +See the [trustmetric](../../../architecture/adr-006-trust-metric.md ) +and [trustmetric useage](../../../architecture/adr-007-trust-metric-usage.md ) +architecture docs for more details. + diff --git a/docs/specification/new-spec/spec-notes.md b/docs/specification/new-spec/spec-notes.md deleted file mode 100644 index fbffac741..000000000 --- a/docs/specification/new-spec/spec-notes.md +++ /dev/null @@ -1,3 +0,0 @@ -- Remove BlockID from Commit -- Actually validate the ValidatorsHash -- Move blockHeight=1 exception for LastCommit to ValidateBasic diff --git a/docs/specification/new-spec/state.md b/docs/specification/new-spec/state.md index 1d7900273..abd32edba 100644 --- a/docs/specification/new-spec/state.md +++ b/docs/specification/new-spec/state.md @@ -2,13 +2,18 @@ ## State -The state contains information whose cryptographic digest is included in block headers, -and thus is necessary for validating new blocks. -For instance, the Merkle root of the results from executing the previous block, or the Merkle root of the current validators. -While neither the results of transactions now the validators are ever included in the blockchain itself, -the Merkle roots are, and hence we need a separate data structure to track them. +The state contains information whose cryptographic digest is included in block headers, and thus is +necessary for validating new blocks. For instance, the set of validators and the results of +transactions are never included in blocks, but their Merkle roots are - the state keeps track of them. -``` +Note that the `State` object itself is an implementation detail, since it is never +included in a block or gossipped over the network, and we never compute +its hash. However, the types it contains are part of the specification, since +their Merkle roots are included in blocks. + +For details on an implementation of `State` with persistence, see TODO + +```go type State struct { LastResults []Result AppHash []byte @@ -22,7 +27,7 @@ type State struct { ### Result -``` +```go type Result struct { Code uint32 Data []byte @@ -46,7 +51,7 @@ represented in the tags. A validator is an active participant in the consensus with a public key and a voting power. Validator's also contain an address which is derived from the PubKey: -``` +```go type Validator struct { Address []byte PubKey PubKey @@ -59,7 +64,7 @@ so that there is a canonical order for computing the SimpleMerkleRoot. We also define a `TotalVotingPower` function, to return the total voting power: -``` +```go func TotalVotingPower(vals []Validators) int64{ sum := 0 for v := range vals{ @@ -77,28 +82,3 @@ TODO: TODO: -## Execution - -We define an `Execute` function that takes a state and a block, -executes the block against the application, and returns an updated state. - -``` -Execute(s State, app ABCIApp, block Block) State { - abciResponses := app.ApplyBlock(block) - - return State{ - LastResults: abciResponses.DeliverTxResults, - AppHash: abciResponses.AppHash, - Validators: UpdateValidators(state.Validators, abciResponses.ValidatorChanges), - LastValidators: state.Validators, - ConsensusParams: UpdateConsensusParams(state.ConsensusParams, abci.Responses.ConsensusParamChanges), - } -} - -type ABCIResponses struct { - DeliverTxResults []Result - ValidatorChanges []Validator - ConsensusParamChanges ConsensusParams - AppHash []byte -} -``` diff --git a/docs/specification/rpc.rst b/docs/specification/rpc.rst index 33173d196..7df394d77 100644 --- a/docs/specification/rpc.rst +++ b/docs/specification/rpc.rst @@ -18,7 +18,7 @@ Configuration ~~~~~~~~~~~~~ Set the ``laddr`` config parameter under ``[rpc]`` table in the -$TMHOME/config.toml file or the ``--rpc.laddr`` command-line flag to the +$TMHOME/config/config.toml file or the ``--rpc.laddr`` command-line flag to the desired protocol://host:port setting. Default: ``tcp://0.0.0.0:46657``. Arguments @@ -112,6 +112,7 @@ An HTTP Get request to the root RPC endpoint (e.g. http://localhost:46657/broadcast_tx_sync?tx=_ http://localhost:46657/commit?height=_ http://localhost:46657/dial_seeds?seeds=_ + http://localhost:46657/dial_peers?peers=_&persistent=_ http://localhost:46657/subscribe?event=_ http://localhost:46657/tx?hash=_&prove=_ http://localhost:46657/unsafe_start_cpu_profiler?filename=_ diff --git a/docs/using-tendermint.rst b/docs/using-tendermint.rst index 9076230ea..bf6571f73 100644 --- a/docs/using-tendermint.rst +++ b/docs/using-tendermint.rst @@ -24,7 +24,8 @@ Initialize the root directory by running: tendermint init This will create a new private key (``priv_validator.json``), and a -genesis file (``genesis.json``) containing the associated public key. +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. For more elaborate initialization, see our `testnet deployment @@ -33,7 +34,7 @@ tool `__. Run --- -To run a tendermint node, use +To run a Tendermint node, use :: @@ -41,7 +42,7 @@ To run a tendermint node, use By default, Tendermint will try to connect to an ABCI application on `127.0.0.1:46658 <127.0.0.1:46658>`__. If you have the ``dummy`` ABCI -app installed, run it in another window. If you don't, kill tendermint +app installed, run it in another window. If you don't, kill Tendermint and run an in-process version with :: @@ -54,7 +55,7 @@ blocks are produced regularly, even if there are no transactions. See *No Empty Tendermint supports in-process versions of the dummy, counter, and nil apps that ship as examples in the `ABCI repository `__. It's easy to compile -your own app in-process with tendermint if it's written in Go. If your +your own app in-process with Tendermint if it's written in Go. If your app is not written in Go, simply run it in another process, and use the ``--proxy_app`` flag to specify the address of the socket it is listening on, for instance: @@ -67,7 +68,7 @@ Transactions ------------ To send a transaction, use ``curl`` to make requests to the Tendermint -RPC server: +RPC server, for example: :: @@ -92,6 +93,57 @@ Visit http://localhost:46657 in your browser to see the list of other endpoints. Some take no arguments (like ``/status``), while others specify the argument name and use ``_`` as a placeholder. +Formatting +~~~~~~~~~~ + +The following nuances when sending/formatting transactions should +be taken into account: + +With ``GET``: + +To send a UTF8 string byte array, quote the value of the tx pramater: + +:: + + curl 'http://localhost:46657/broadcast_tx_commit?tx="hello"' + +which sends a 5 byte transaction: "h e l l o" [68 65 6c 6c 6f]. + +Note the URL must be wrapped with single quoes, else bash will ignore the double quotes. +To avoid the single quotes, escape the double quotes: + +:: + + curl http://localhost:46657/broadcast_tx_commit?tx=\"hello\" + + + +Using a special character: + +:: + + curl 'http://localhost:46657/broadcast_tx_commit?tx="€5"' + +sends a 4 byte transaction: "€5" (UTF8) [e2 82 ac 35]. + +To send as raw hex, omit quotes AND prefix the hex string with ``0x``: + +:: + + curl http://localhost:46657/broadcast_tx_commit?tx=0x01020304 + +which sends a 4 byte transaction: [01 02 03 04]. + +With ``POST`` (using ``json``), the raw hex must be ``base64`` encoded: + +:: + + curl --data-binary '{"jsonrpc":"2.0","id":"anything","method":"broadcast_tx_commit","params": {"tx": "AQIDBA=="}}' -H 'content-type:text/plain;' http://localhost:46657 + +which sends the same 4 byte transaction: [01 02 03 04]. + +Note that raw hex cannot be used in ``POST`` transactions. + Reset ----- @@ -118,8 +170,8 @@ Tendermint uses a ``config.toml`` for configuration. For details, see `the config specification <./specification/configuration.html>`__. Notable options include the socket address of the application -(``proxy_app``), the listenting address of the tendermint peer -(``p2p.laddr``), and the listening address of the rpc server +(``proxy_app``), the listening address of the Tendermint peer +(``p2p.laddr``), and the listening address of the RPC server (``rpc.laddr``). Some fields from the config file can be overwritten with flags. @@ -127,10 +179,14 @@ Some fields from the config file can be overwritten with flags. No Empty Blocks --------------- -This much requested feature was implemented in version 0.10.3. While the default behaviour of ``tendermint`` is still to create blocks approximately once per second, it is possible to disable empty blocks or set a block creation interval. In the former case, blocks will be created when there are new transactions or when the AppHash changes. +This much requested feature was implemented in version 0.10.3. While the +default behaviour of ``tendermint`` is still to create blocks approximately +once per second, it is possible to disable empty blocks or set a block creation +interval. In the former case, blocks will be created when there are new +transactions or when the AppHash changes. -To configure tendermint to not produce empty blocks unless there are txs or the app hash changes, -run tendermint with this additional flag: +To configure Tendermint to not produce empty blocks unless there are +transactions or the app hash changes, run Tendermint with this additional flag: :: @@ -153,14 +209,13 @@ The block interval setting allows for a delay (in seconds) between the creation create_empty_blocks_interval = 5 With this setting, empty blocks will be produced every 5s if no block has been produced otherwise, -regardless of the value of `create_empty_blocks`. - +regardless of the value of ``create_empty_blocks``. Broadcast API ------------- Earlier, we used the ``broadcast_tx_commit`` endpoint to send a -transaction. When a transaction is sent to a tendermint node, it will +transaction. When a transaction is sent to a Tendermint node, it will run via ``CheckTx`` against the application. If it passes ``CheckTx``, it will be included in the mempool, broadcast to other peers, and eventually included in a block. @@ -187,16 +242,19 @@ value for ``broadcast_tx_commit`` includes two fields, ``check_tx`` and through those ABCI messages. The benefit of using ``broadcast_tx_commit`` is that the request returns -after the transaction is committed (ie. included in a block), but that +after the transaction is committed (i.e. included in a block), but that can take on the order of a second. For a quick result, use ``broadcast_tx_sync``, but the transaction will not be committed until later, and by that point its effect on the state may change. +Note: see the Transactions => Formatting section for details about +transaction formating. + Tendermint Networks ------------------- When ``tendermint init`` is run, both a ``genesis.json`` and -``priv_validator.json`` are created in ``~/.tendermint``. The +``priv_validator.json`` are created in ``~/.tendermint/config``. The ``genesis.json`` might look like: :: @@ -246,13 +304,17 @@ conflicting messages. Note also that the ``pub_key`` (the public key) in the ``priv_validator.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 (ie. 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 -root directory will be able to make new blocks, as we've already seen. +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 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 recommend having voting powers greater than 10^12 (ie. 1 trillion) +(see `Proposals section of Byzantine Consensus Algorithm +<./specification/byzantine-consensus-algorithm.html#proposals>`__ for details). If we want to add more nodes to the network, we have two choices: we can add a new validator node, who will also participate in the consensus by @@ -263,8 +325,10 @@ with the consensus protocol. Peers ~~~~~ -To connect to peers on start-up, specify them in the ``config.toml`` or -on the command line. +To connect to peers on start-up, specify them in the ``$TMHOME/config/config.toml`` or +on the command line. Use `seeds` to specify seed nodes from which you can get many other +peer addresses, and ``persistent_peers`` to specify peers that your node will maintain +persistent connections with. For instance, @@ -273,26 +337,35 @@ For instance, tendermint node --p2p.seeds "1.2.3.4:46656,5.6.7.8:46656" Alternatively, you can use the ``/dial_seeds`` endpoint of the RPC to -specify peers for a running node to connect to: +specify seeds for a running node to connect to: :: - curl --data-urlencode "seeds=[\"1.2.3.4:46656\",\"5.6.7.8:46656\"]" localhost:46657/dial_seeds + curl 'localhost:46657/dial_seeds?seeds=\["1.2.3.4:46656","5.6.7.8:46656"\]' + +Note, if the peer-exchange protocol (PEX) is enabled (default), you should not +normally need seeds after the first start. Peers will be gossipping about known +peers and forming a network, storing peer addresses in the addrbook. + +If you want Tendermint to connect to specific set of addresses and maintain a +persistent connection with each, you can use the ``--p2p.persistent_peers`` +flag or the corresponding setting in the ``config.toml`` or the +``/dial_peers`` RPC endpoint to do it without stopping Tendermint +core instance. + +:: -Additionally, the peer-exchange protocol can be enabled using the -``--pex`` flag, though this feature is `still under -development `__. If -``--pex`` is enabled, peers will gossip about known peers and form a -more resilient network. + tendermint node --p2p.persistent_peers "10.11.12.13:46656,10.11.12.14:46656" + curl 'localhost:46657/dial_peers?persistent=true&peers=\["1.2.3.4:46656","5.6.7.8:46656"\]' Adding a Non-Validator ~~~~~~~~~~~~~~~~~~~~~~ Adding a non-validator is simple. Just copy the original -``genesis.json`` to ``~/.tendermint`` on the new machine and start the -node, specifying seeds as necessary. If no seeds are specified, the node -won't make any blocks, because it's not a validator, and it won't hear -about any blocks, because it's not connected to the other peer. +``genesis.json`` to ``~/.tendermint/config`` on the new machine and start the +node, specifying seeds or persistent peers as necessary. If no seeds or persistent +peers are specified, the node won't make any blocks, because it's not a validator, +and it won't hear about any blocks, because it's not connected to the other peer. Adding a Validator ~~~~~~~~~~~~~~~~~~ @@ -358,12 +431,12 @@ then the new ``genesis.json`` will be: ] } -Update the ``genesis.json`` in ``~/.tendermint``. Copy the genesis file -and the new ``priv_validator.json`` to the ``~/.tendermint`` on a new +Update the ``genesis.json`` in ``~/.tendermint/config``. Copy the genesis file +and the new ``priv_validator.json`` to the ``~/.tendermint/config`` on a new machine. Now run ``tendermint node`` on both machines, and use either -``--p2p.seeds`` or the ``/dial_seeds`` to get them to peer up. They +``--p2p.persistent_peers`` or the ``/dial_peers`` to get them to peer up. They should start making blocks, and will only continue to do so as long as both of them are online. @@ -388,7 +461,7 @@ connections to peers with the same IP address. Upgrading ~~~~~~~~~ -The tendermint development cycle includes a lot of breaking changes. Upgrading from +The Tendermint development cycle includes a lot of breaking changes. Upgrading from an old version to a new version usually means throwing away the chain data. Try out the `tm-migrate `__ tool written by @hxqlh if you are keen to preserve the state of your chain when upgrading to newer versions. diff --git a/evidence/reactor.go b/evidence/reactor.go index cb9706a34..169a274d3 100644 --- a/evidence/reactor.go +++ b/evidence/reactor.go @@ -126,7 +126,7 @@ func (evR *EvidenceReactor) broadcastRoutine() { // broadcast all pending evidence msg := &EvidenceListMessage{evR.evpool.PendingEvidence()} evR.Switch.Broadcast(EvidenceChannel, struct{ EvidenceMessage }{msg}) - case <-evR.Quit: + case <-evR.Quit(): return } } diff --git a/evidence/store.go b/evidence/store.go index fd40b5338..7c8becd02 100644 --- a/evidence/store.go +++ b/evidence/store.go @@ -99,8 +99,8 @@ func (store *EvidenceStore) PendingEvidence() (evidence []types.Evidence) { // ListEvidence lists the evidence for the given prefix key. // It is wrapped by PriorityEvidence and PendingEvidence for convenience. func (store *EvidenceStore) ListEvidence(prefixKey string) (evidence []types.Evidence) { - iter := store.db.IteratorPrefix([]byte(prefixKey)) - for iter.Next() { + iter := dbm.IteratePrefix(store.db, []byte(prefixKey)) + for ; iter.Valid(); iter.Next() { val := iter.Value() var ei EvidenceInfo diff --git a/glide.lock b/glide.lock index b83358864..2e864cee3 100644 --- a/glide.lock +++ b/glide.lock @@ -1,32 +1,26 @@ -hash: 072c8e685dd519c1f509da67379b70451a681bf3ef6cbd82900a1f68c55bbe16 -updated: 2017-12-29T11:08:17.355999228-05:00 +hash: 322a0d4b9be08c59bf65df0e17e3be8d60762eaf9516f0c4126b50f9fd676f26 +updated: 2018-02-21T03:31:35.382568482Z imports: - name: github.com/btcsuite/btcd - version: 2e60448ffcc6bf78332d1fe590260095f554dd78 + version: 50de9da05b50eb15658bb350f6ea24368a111ab7 subpackages: - btcec - name: github.com/ebuchman/fail-test version: 95f809107225be108efcf10a3509e4ea6ceef3c4 - name: github.com/fsnotify/fsnotify - version: 4da3e2cfbabc9f751898f250b49f2439785783a1 + version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 - name: github.com/go-kit/kit - version: 953e747656a7bbb5e1f998608b460458958b70cc + version: 4dc7be5d2d12881735283bcab7352178e190fc71 subpackages: - log - log/level - log/term - name: github.com/go-logfmt/logfmt version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 -- name: github.com/go-playground/locales - version: e4cbcb5d0652150d40ad0646651076b6bd2be4f6 - subpackages: - - currency -- name: github.com/go-playground/universal-translator - version: 71201497bace774495daed26a3874fd339e0b538 - name: github.com/go-stack/stack version: 259ab82a6cad3992b4e21ff5cac294ccb06474bc - name: github.com/gogo/protobuf - version: 342cbe0a04158f6dcb03ca0079991a51a4248c02 + version: 1adfc126b41513cc696b209667c8656ea7aac67c subpackages: - gogoproto - jsonpb @@ -35,7 +29,7 @@ imports: - sortkeys - types - name: github.com/golang/protobuf - version: 1e59b77b52bf8e4b449a57e6f79f21226d571845 + version: 925541529c1fa6821df4e44ce2723319eb2be768 subpackages: - proto - ptypes @@ -66,15 +60,15 @@ imports: - name: github.com/magiconair/properties version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934 - name: github.com/mitchellh/mapstructure - version: 06020f85339e21b2478f756a78e295255ffa4d6a + version: b4575eea38cca1123ec2dc90c26529b5c5acfcff - name: github.com/pelletier/go-toml - version: 0131db6d737cfbbfb678f8b7d92e55e27ce46224 + version: acdc4509485b587f5e675510c4f2c63e90ff68a8 - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/rcrowley/go-metrics - version: e181e095bae94582363434144c61a9653aff6e50 + version: 8732c616f52954686704c8645fe1a9d59e9df7c1 - name: github.com/spf13/afero - version: 57afd63c68602b63ed976de00dd066ccb3c319db + version: bb8f1927f2a9d3ab41c9340aa034f6b803f4359c subpackages: - mem - name: github.com/spf13/cast @@ -82,9 +76,9 @@ imports: - name: github.com/spf13/cobra version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b - name: github.com/spf13/jwalterweatherman - version: 12bd96e66386c1960ab0f74ced1362f66f552f7b + version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394 - name: github.com/spf13/pflag - version: 4c012f6dcd9546820e378d0bdda4d8fc772cdfea + version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 - name: github.com/spf13/viper version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 - name: github.com/syndtr/goleveldb @@ -103,7 +97,7 @@ imports: - leveldb/table - leveldb/util - name: github.com/tendermint/abci - version: 5d5ea6869b91cadb55dbc4211ad7b326f053a33e + version: 68592f4d8ee34e97db94b7a7976b1309efdb7eb9 subpackages: - client - example/code @@ -122,14 +116,8 @@ imports: version: b6fc872b42d41158a60307db4da051dd6f179415 subpackages: - data - - data/base58 - - nowriter/tmlegacy -- name: github.com/tendermint/iavl - version: 594cc0c062a7174475f0ab654384038d77067917 - subpackages: - - iavl - name: github.com/tendermint/tmlibs - version: 91b4b534ad78e442192c8175db92a06a51064064 + version: 1b9b5652a199ab0be2e781393fb275b66377309d subpackages: - autofile - cli @@ -144,7 +132,7 @@ imports: - pubsub/query - test - name: golang.org/x/crypto - version: 95a4943f35d008beabde8c11e5075a1b714e6419 + version: 1875d0a70c90e57f11972aefd42276df65e895b9 subpackages: - curve25519 - nacl/box @@ -155,7 +143,7 @@ imports: - ripemd160 - salsa20/salsa - name: golang.org/x/net - version: d866cfc389cec985d6fda2859936a575a55a3ab6 + version: 2fb46b16b8dda405028c50f7c7f0f9dd1fa6bfb1 subpackages: - context - http2 @@ -165,7 +153,7 @@ imports: - lex/httplex - trace - name: golang.org/x/sys - version: 83801418e1b59fb1880e363299581ee543af32ca + version: 37707fdb30a5b38865cfb95e5aab41707daec7fd subpackages: - unix - name: golang.org/x/text @@ -176,7 +164,7 @@ imports: - unicode/bidi - unicode/norm - name: google.golang.org/genproto - version: a8101f21cf983e773d0c1133ebc5424792003214 + version: 4eb30f4778eed4c258ba66527a0d4f9ec8a36c45 subpackages: - googleapis/rpc/status - name: google.golang.org/grpc @@ -198,21 +186,21 @@ imports: - status - tap - transport -- name: gopkg.in/go-playground/validator.v9 - version: b1f51f36f1c98cc97f777d6fc9d4b05eaa0cabb5 - name: gopkg.in/yaml.v2 - version: 287cf08546ab5e7e37d55a84f7ed3fd1db036de5 + version: d670f9405373e636a5a2765eea47fac0c9bc91a4 testImports: - name: github.com/davecgh/go-spew - version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 + version: 346938d642f2ec3594ed81d874461961cd0faa76 subpackages: - spew +- name: github.com/fortytw2/leaktest + version: 3b724c3d7b8729a35bf4e577f71653aec6e53513 - name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + version: 792786c7400a136282c1664665ae0a8db921c6c2 subpackages: - difflib - name: github.com/stretchr/testify - version: 2aa2c176b9dab406a6970f6a55f513e8a8c8b18f + version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 subpackages: - assert - require diff --git a/glide.yaml b/glide.yaml index b7846d641..c1b0aef55 100644 --- a/glide.yaml +++ b/glide.yaml @@ -2,10 +2,11 @@ package: github.com/tendermint/tendermint import: - package: github.com/ebuchman/fail-test - package: github.com/gogo/protobuf - version: v0.5 + version: ^1.0.0 subpackages: - proto - package: github.com/golang/protobuf + version: ^1.0.0 subpackages: - proto - package: github.com/gorilla/websocket @@ -18,23 +19,19 @@ import: - package: github.com/spf13/viper version: v1.0.0 - package: github.com/tendermint/abci - version: v0.9.0 + version: 0.10.0 subpackages: - client - example/dummy - types - package: github.com/tendermint/go-crypto - version: ~0.4.1 + version: 0.4.1 - package: github.com/tendermint/go-wire - version: ~0.7.2 + version: 0.7.2 subpackages: - data -- package: github.com/tendermint/iavl - version: ~0.2.0 - subpackages: - - iavl - package: github.com/tendermint/tmlibs - version: v0.6.0 + version: 0.7.0 subpackages: - autofile - cli @@ -46,18 +43,18 @@ import: - log - merkle - pubsub + - pubsub/query - package: golang.org/x/crypto subpackages: - nacl/box - nacl/secretbox - ripemd160 -- package: golang.org/x/net - subpackages: - - context - package: google.golang.org/grpc version: v1.7.3 testImport: +- package: github.com/fortytw2/leaktest - package: github.com/go-kit/kit + version: ^0.6.0 subpackages: - log/term - package: github.com/stretchr/testify diff --git a/lite/client/provider_test.go b/lite/client/provider_test.go index 0bebfced0..94d47da3f 100644 --- a/lite/client/provider_test.go +++ b/lite/client/provider_test.go @@ -10,6 +10,7 @@ import ( liteErr "github.com/tendermint/tendermint/lite/errors" rpcclient "github.com/tendermint/tendermint/rpc/client" rpctest "github.com/tendermint/tendermint/rpc/test" + "github.com/tendermint/tendermint/types" ) func TestProvider(t *testing.T) { @@ -17,7 +18,8 @@ func TestProvider(t *testing.T) { cfg := rpctest.GetConfig() rpcAddr := cfg.RPC.ListenAddress - chainID := cfg.ChainID + genDoc, _ := types.GenesisDocFromFile(cfg.GenesisFile()) + chainID := genDoc.ChainID p := NewHTTPProvider(rpcAddr) require.NotNil(t, p) @@ -35,7 +37,7 @@ func TestProvider(t *testing.T) { // let's check this is valid somehow assert.Nil(seed.ValidateBasic(chainID)) - cert := lite.NewStatic(chainID, seed.Validators) + cert := lite.NewStaticCertifier(chainID, seed.Validators) // historical queries now work :) lower := sh - 5 diff --git a/lite/dynamic.go b/lite/dynamic_certifier.go similarity index 60% rename from lite/dynamic.go rename to lite/dynamic_certifier.go index 231aed7a4..0ddace8b6 100644 --- a/lite/dynamic.go +++ b/lite/dynamic_certifier.go @@ -6,9 +6,9 @@ import ( liteErr "github.com/tendermint/tendermint/lite/errors" ) -var _ Certifier = &Dynamic{} +var _ Certifier = (*DynamicCertifier)(nil) -// Dynamic uses a Static for Certify, but adds an +// DynamicCertifier uses a StaticCertifier for Certify, but adds an // Update method to allow for a change of validators. // // You can pass in a FullCommit with another validator set, @@ -17,46 +17,48 @@ var _ Certifier = &Dynamic{} // validator set for the next Certify call. // For security, it will only follow validator set changes // going forward. -type Dynamic struct { - cert *Static +type DynamicCertifier struct { + cert *StaticCertifier lastHeight int64 } // NewDynamic returns a new dynamic certifier. -func NewDynamic(chainID string, vals *types.ValidatorSet, height int64) *Dynamic { - return &Dynamic{ - cert: NewStatic(chainID, vals), +func NewDynamicCertifier(chainID string, vals *types.ValidatorSet, height int64) *DynamicCertifier { + return &DynamicCertifier{ + cert: NewStaticCertifier(chainID, vals), lastHeight: height, } } // ChainID returns the chain id of this certifier. -func (c *Dynamic) ChainID() string { - return c.cert.ChainID() +// Implements Certifier. +func (dc *DynamicCertifier) ChainID() string { + return dc.cert.ChainID() } // Validators returns the validators of this certifier. -func (c *Dynamic) Validators() *types.ValidatorSet { - return c.cert.vSet +func (dc *DynamicCertifier) Validators() *types.ValidatorSet { + return dc.cert.vSet } // Hash returns the hash of this certifier. -func (c *Dynamic) Hash() []byte { - return c.cert.Hash() +func (dc *DynamicCertifier) Hash() []byte { + return dc.cert.Hash() } // LastHeight returns the last height of this certifier. -func (c *Dynamic) LastHeight() int64 { - return c.lastHeight +func (dc *DynamicCertifier) LastHeight() int64 { + return dc.lastHeight } // Certify will verify whether the commit is valid and will update the height if it is or return an // error if it is not. -func (c *Dynamic) Certify(check Commit) error { - err := c.cert.Certify(check) +// Implements Certifier. +func (dc *DynamicCertifier) Certify(check Commit) error { + err := dc.cert.Certify(check) if err == nil { // update last seen height if input is valid - c.lastHeight = check.Height() + dc.lastHeight = check.Height() } return err } @@ -65,15 +67,15 @@ func (c *Dynamic) Certify(check Commit) error { // the certifying validator set if safe to do so. // // Returns an error if update is impossible (invalid proof or IsTooMuchChangeErr) -func (c *Dynamic) Update(fc FullCommit) error { +func (dc *DynamicCertifier) Update(fc FullCommit) error { // ignore all checkpoints in the past -> only to the future h := fc.Height() - if h <= c.lastHeight { + if h <= dc.lastHeight { return liteErr.ErrPastTime() } // first, verify if the input is self-consistent.... - err := fc.ValidateBasic(c.ChainID()) + err := fc.ValidateBasic(dc.ChainID()) if err != nil { return err } @@ -82,14 +84,13 @@ func (c *Dynamic) Update(fc FullCommit) error { // would be approved by the currently known validator set // as well as the new set commit := fc.Commit.Commit - err = c.Validators().VerifyCommitAny(fc.Validators, c.ChainID(), - commit.BlockID, h, commit) + err = dc.Validators().VerifyCommitAny(fc.Validators, dc.ChainID(), commit.BlockID, h, commit) if err != nil { return liteErr.ErrTooMuchChange() } // looks good, we can update - c.cert = NewStatic(c.ChainID(), fc.Validators) - c.lastHeight = h + dc.cert = NewStaticCertifier(dc.ChainID(), fc.Validators) + dc.lastHeight = h return nil } diff --git a/lite/dynamic_test.go b/lite/dynamic_certifier_test.go similarity index 97% rename from lite/dynamic_test.go rename to lite/dynamic_certifier_test.go index c45371acd..88c145f95 100644 --- a/lite/dynamic_test.go +++ b/lite/dynamic_certifier_test.go @@ -23,7 +23,7 @@ func TestDynamicCert(t *testing.T) { vals := keys.ToValidators(20, 10) // and a certifier based on our known set chainID := "test-dyno" - cert := lite.NewDynamic(chainID, vals, 0) + cert := lite.NewDynamicCertifier(chainID, vals, 0) cases := []struct { keys lite.ValKeys @@ -67,7 +67,7 @@ func TestDynamicUpdate(t *testing.T) { chainID := "test-dyno-up" keys := lite.GenValKeys(5) vals := keys.ToValidators(20, 0) - cert := lite.NewDynamic(chainID, vals, 40) + cert := lite.NewDynamicCertifier(chainID, vals, 40) // one valid block to give us a sense of time h := int64(100) diff --git a/lite/inquirer.go b/lite/inquirer.go deleted file mode 100644 index 5d6ce60c7..000000000 --- a/lite/inquirer.go +++ /dev/null @@ -1,155 +0,0 @@ -package lite - -import ( - "github.com/tendermint/tendermint/types" - - liteErr "github.com/tendermint/tendermint/lite/errors" -) - -// Inquiring wraps a dynamic certifier and implements an auto-update strategy. If a call to Certify -// fails due to a change it validator set, Inquiring will try and find a previous FullCommit which -// it can use to safely update the validator set. It uses a source provider to obtain the needed -// FullCommits. It stores properly validated data on the local system. -type Inquiring struct { - cert *Dynamic - // These are only properly validated data, from local system - trusted Provider - // This is a source of new info, like a node rpc, or other import method - Source Provider -} - -// NewInquiring returns a new Inquiring object. It uses the trusted provider to store validated -// data and the source provider to obtain missing FullCommits. -// -// Example: The trusted provider should a CacheProvider, MemProvider or files.Provider. The source -// provider should be a client.HTTPProvider. -func NewInquiring(chainID string, fc FullCommit, trusted Provider, source Provider) *Inquiring { - // store the data in trusted - // TODO: StoredCommit() can return an error and we need to handle this. - trusted.StoreCommit(fc) - - return &Inquiring{ - cert: NewDynamic(chainID, fc.Validators, fc.Height()), - trusted: trusted, - Source: source, - } -} - -// ChainID returns the chain id. -func (c *Inquiring) ChainID() string { - return c.cert.ChainID() -} - -// Validators returns the validator set. -func (c *Inquiring) Validators() *types.ValidatorSet { - return c.cert.cert.vSet -} - -// LastHeight returns the last height. -func (c *Inquiring) LastHeight() int64 { - return c.cert.lastHeight -} - -// Certify makes sure this is checkpoint is valid. -// -// If the validators have changed since the last know time, it looks -// for a path to prove the new validators. -// -// On success, it will store the checkpoint in the store for later viewing -func (c *Inquiring) Certify(commit Commit) error { - err := c.useClosestTrust(commit.Height()) - if err != nil { - return err - } - - err = c.cert.Certify(commit) - if !liteErr.IsValidatorsChangedErr(err) { - return err - } - err = c.updateToHash(commit.Header.ValidatorsHash) - if err != nil { - return err - } - - err = c.cert.Certify(commit) - if err != nil { - return err - } - - // store the new checkpoint - return c.trusted.StoreCommit(NewFullCommit(commit, c.Validators())) -} - -// Update will verify if this is a valid change and update -// the certifying validator set if safe to do so. -func (c *Inquiring) Update(fc FullCommit) error { - err := c.useClosestTrust(fc.Height()) - if err != nil { - return err - } - - err = c.cert.Update(fc) - if err == nil { - err = c.trusted.StoreCommit(fc) - } - return err -} - -func (c *Inquiring) useClosestTrust(h int64) error { - closest, err := c.trusted.GetByHeight(h) - if err != nil { - return err - } - - // if the best seed is not the one we currently use, - // let's just reset the dynamic validator - if closest.Height() != c.LastHeight() { - c.cert = NewDynamic(c.ChainID(), closest.Validators, closest.Height()) - } - return nil -} - -// updateToHash gets the validator hash we want to update to -// if IsTooMuchChangeErr, we try to find a path by binary search over height -func (c *Inquiring) updateToHash(vhash []byte) error { - // try to get the match, and update - fc, err := c.Source.GetByHash(vhash) - if err != nil { - return err - } - err = c.cert.Update(fc) - // handle IsTooMuchChangeErr by using divide and conquer - if liteErr.IsTooMuchChangeErr(err) { - err = c.updateToHeight(fc.Height()) - } - return err -} - -// updateToHeight will use divide-and-conquer to find a path to h -func (c *Inquiring) updateToHeight(h int64) error { - // try to update to this height (with checks) - fc, err := c.Source.GetByHeight(h) - if err != nil { - return err - } - start, end := c.LastHeight(), fc.Height() - if end <= start { - return liteErr.ErrNoPathFound() - } - err = c.Update(fc) - - // we can handle IsTooMuchChangeErr specially - if !liteErr.IsTooMuchChangeErr(err) { - return err - } - - // try to update to mid - mid := (start + end) / 2 - err = c.updateToHeight(mid) - if err != nil { - return err - } - - // if we made it to mid, we recurse - return c.updateToHeight(h) -} diff --git a/lite/inquiring_certifier.go b/lite/inquiring_certifier.go new file mode 100644 index 000000000..042bd08e3 --- /dev/null +++ b/lite/inquiring_certifier.go @@ -0,0 +1,163 @@ +package lite + +import ( + "github.com/tendermint/tendermint/types" + + liteErr "github.com/tendermint/tendermint/lite/errors" +) + +var _ Certifier = (*InquiringCertifier)(nil) + +// InquiringCertifier wraps a dynamic certifier and implements an auto-update strategy. If a call +// to Certify fails due to a change it validator set, InquiringCertifier will try and find a +// previous FullCommit which it can use to safely update the validator set. It uses a source +// provider to obtain the needed FullCommits. It stores properly validated data on the local system. +type InquiringCertifier struct { + cert *DynamicCertifier + // These are only properly validated data, from local system + trusted Provider + // This is a source of new info, like a node rpc, or other import method + Source Provider +} + +// NewInquiringCertifier returns a new Inquiring object. It uses the trusted provider to store +// validated data and the source provider to obtain missing FullCommits. +// +// Example: The trusted provider should a CacheProvider, MemProvider or files.Provider. The source +// provider should be a client.HTTPProvider. +func NewInquiringCertifier(chainID string, fc FullCommit, trusted Provider, + source Provider) (*InquiringCertifier, error) { + + // store the data in trusted + err := trusted.StoreCommit(fc) + if err != nil { + return nil, err + } + + return &InquiringCertifier{ + cert: NewDynamicCertifier(chainID, fc.Validators, fc.Height()), + trusted: trusted, + Source: source, + }, nil +} + +// ChainID returns the chain id. +// Implements Certifier. +func (ic *InquiringCertifier) ChainID() string { + return ic.cert.ChainID() +} + +// Validators returns the validator set. +func (ic *InquiringCertifier) Validators() *types.ValidatorSet { + return ic.cert.cert.vSet +} + +// LastHeight returns the last height. +func (ic *InquiringCertifier) LastHeight() int64 { + return ic.cert.lastHeight +} + +// Certify makes sure this is checkpoint is valid. +// +// If the validators have changed since the last know time, it looks +// for a path to prove the new validators. +// +// On success, it will store the checkpoint in the store for later viewing +// Implements Certifier. +func (ic *InquiringCertifier) Certify(commit Commit) error { + err := ic.useClosestTrust(commit.Height()) + if err != nil { + return err + } + + err = ic.cert.Certify(commit) + if !liteErr.IsValidatorsChangedErr(err) { + return err + } + err = ic.updateToHash(commit.Header.ValidatorsHash) + if err != nil { + return err + } + + err = ic.cert.Certify(commit) + if err != nil { + return err + } + + // store the new checkpoint + return ic.trusted.StoreCommit(NewFullCommit(commit, ic.Validators())) +} + +// Update will verify if this is a valid change and update +// the certifying validator set if safe to do so. +func (ic *InquiringCertifier) Update(fc FullCommit) error { + err := ic.useClosestTrust(fc.Height()) + if err != nil { + return err + } + + err = ic.cert.Update(fc) + if err == nil { + err = ic.trusted.StoreCommit(fc) + } + return err +} + +func (ic *InquiringCertifier) useClosestTrust(h int64) error { + closest, err := ic.trusted.GetByHeight(h) + if err != nil { + return err + } + + // if the best seed is not the one we currently use, + // let's just reset the dynamic validator + if closest.Height() != ic.LastHeight() { + ic.cert = NewDynamicCertifier(ic.ChainID(), closest.Validators, closest.Height()) + } + return nil +} + +// updateToHash gets the validator hash we want to update to +// if IsTooMuchChangeErr, we try to find a path by binary search over height +func (ic *InquiringCertifier) updateToHash(vhash []byte) error { + // try to get the match, and update + fc, err := ic.Source.GetByHash(vhash) + if err != nil { + return err + } + err = ic.cert.Update(fc) + // handle IsTooMuchChangeErr by using divide and conquer + if liteErr.IsTooMuchChangeErr(err) { + err = ic.updateToHeight(fc.Height()) + } + return err +} + +// updateToHeight will use divide-and-conquer to find a path to h +func (ic *InquiringCertifier) updateToHeight(h int64) error { + // try to update to this height (with checks) + fc, err := ic.Source.GetByHeight(h) + if err != nil { + return err + } + start, end := ic.LastHeight(), fc.Height() + if end <= start { + return liteErr.ErrNoPathFound() + } + err = ic.Update(fc) + + // we can handle IsTooMuchChangeErr specially + if !liteErr.IsTooMuchChangeErr(err) { + return err + } + + // try to update to mid + mid := (start + end) / 2 + err = ic.updateToHeight(mid) + if err != nil { + return err + } + + // if we made it to mid, we recurse + return ic.updateToHeight(h) +} diff --git a/lite/inquirer_test.go b/lite/inquiring_certifier_test.go similarity index 90% rename from lite/inquirer_test.go rename to lite/inquiring_certifier_test.go index ce4317543..db8160bdc 100644 --- a/lite/inquirer_test.go +++ b/lite/inquiring_certifier_test.go @@ -32,18 +32,20 @@ func TestInquirerValidPath(t *testing.T) { vals := keys.ToValidators(vote, 0) h := int64(20 + 10*i) appHash := []byte(fmt.Sprintf("h=%d", h)) - commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, len(keys)) + commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, + len(keys)) } // initialize a certifier with the initial state - cert := lite.NewInquiring(chainID, commits[0], trust, source) + cert, err := lite.NewInquiringCertifier(chainID, commits[0], trust, source) + require.Nil(err) // this should fail validation.... commit := commits[count-1].Commit - err := cert.Certify(commit) + err = cert.Certify(commit) require.NotNil(err) - // add a few seed in the middle should be insufficient + // adding a few commits in the middle should be insufficient for i := 10; i < 13; i++ { err := source.StoreCommit(commits[i]) require.Nil(err) @@ -81,11 +83,12 @@ func TestInquirerMinimalPath(t *testing.T) { h := int64(5 + 10*i) appHash := []byte(fmt.Sprintf("h=%d", h)) resHash := []byte(fmt.Sprintf("res=%d", h)) - commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, len(keys)) + commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, + len(keys)) } // initialize a certifier with the initial state - cert := lite.NewInquiring(chainID, commits[0], trust, source) + cert, _ := lite.NewInquiringCertifier(chainID, commits[0], trust, source) // this should fail validation.... commit := commits[count-1].Commit @@ -130,11 +133,12 @@ func TestInquirerVerifyHistorical(t *testing.T) { h := int64(20 + 10*i) appHash := []byte(fmt.Sprintf("h=%d", h)) resHash := []byte(fmt.Sprintf("res=%d", h)) - commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, len(keys)) + commits[i] = keys.GenFullCommit(chainID, h, nil, vals, appHash, consHash, resHash, 0, + len(keys)) } // initialize a certifier with the initial state - cert := lite.NewInquiring(chainID, commits[0], trust, source) + cert, _ := lite.NewInquiringCertifier(chainID, commits[0], trust, source) // store a few commits as trust for _, i := range []int{2, 5} { diff --git a/lite/performance_test.go b/lite/performance_test.go index 835e52f91..28c73bb08 100644 --- a/lite/performance_test.go +++ b/lite/performance_test.go @@ -105,7 +105,7 @@ func BenchmarkCertifyCommitSec100(b *testing.B) { func benchmarkCertifyCommit(b *testing.B, keys lite.ValKeys) { chainID := "bench-certify" vals := keys.ToValidators(20, 10) - cert := lite.NewStatic(chainID, vals) + cert := lite.NewStaticCertifier(chainID, vals) check := keys.GenCommit(chainID, 123, nil, vals, []byte("foo"), []byte("params"), []byte("res"), 0, len(keys)) for i := 0; i < b.N; i++ { err := cert.Certify(check) diff --git a/lite/proxy/certifier.go b/lite/proxy/certifier.go index 1d7284f2c..6e319dc0d 100644 --- a/lite/proxy/certifier.go +++ b/lite/proxy/certifier.go @@ -6,7 +6,7 @@ import ( "github.com/tendermint/tendermint/lite/files" ) -func GetCertifier(chainID, rootDir, nodeAddr string) (*lite.Inquiring, error) { +func GetCertifier(chainID, rootDir, nodeAddr string) (*lite.InquiringCertifier, error) { trust := lite.NewCacheProvider( lite.NewMemStoreProvider(), files.NewProvider(rootDir), @@ -25,6 +25,11 @@ func GetCertifier(chainID, rootDir, nodeAddr string) (*lite.Inquiring, error) { if err != nil { return nil, err } - cert := lite.NewInquiring(chainID, fc, trust, source) + + cert, err := lite.NewInquiringCertifier(chainID, fc, trust, source) + if err != nil { + return nil, err + } + return cert, nil } diff --git a/lite/proxy/proxy.go b/lite/proxy/proxy.go index 21db13ed4..34aa99fa0 100644 --- a/lite/proxy/proxy.go +++ b/lite/proxy/proxy.go @@ -18,7 +18,11 @@ const ( // set up the rpc routes to proxy via the given client, // and start up an http/rpc server on the location given by bind (eg. :1234) func StartProxy(c rpcclient.Client, listenAddr string, logger log.Logger) error { - c.Start() + err := c.Start() + if err != nil { + return err + } + r := RPCRoutes(c) // build the handler... @@ -30,7 +34,7 @@ func StartProxy(c rpcclient.Client, listenAddr string, logger log.Logger) error core.SetLogger(logger) mux.HandleFunc(wsEndpoint, wm.WebsocketHandler) - _, err := rpc.StartHTTPServer(listenAddr, mux, logger) + _, err = rpc.StartHTTPServer(listenAddr, mux, logger) return err } diff --git a/lite/proxy/query.go b/lite/proxy/query.go index 0a9d86a0e..9c9557f8f 100644 --- a/lite/proxy/query.go +++ b/lite/proxy/query.go @@ -3,8 +3,7 @@ package proxy import ( "github.com/pkg/errors" - "github.com/tendermint/go-wire/data" - "github.com/tendermint/iavl" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/lite" "github.com/tendermint/tendermint/lite/client" @@ -13,6 +12,20 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" ) +// KeyProof represents a proof of existence or absence of a single key. +// Copied from iavl repo. TODO +type KeyProof interface { + // Verify verfies the proof is valid. To verify absence, + // the value should be nil. + Verify(key, value, root []byte) error + + // Root returns the root hash of the proof. + Root() []byte + + // Serialize itself + Bytes() []byte +} + // GetWithProof will query the key on the given node, and verify it has // a valid proof, as defined by the certifier. // @@ -21,7 +34,7 @@ import ( // If val is empty, proof should be KeyMissingProof func GetWithProof(key []byte, reqHeight int64, node rpcclient.Client, cert lite.Certifier) ( - val data.Bytes, height int64, proof iavl.KeyProof, err error) { + val cmn.HexBytes, height int64, proof KeyProof, err error) { if reqHeight < 0 { err = errors.Errorf("Height cannot be negative") @@ -41,7 +54,7 @@ func GetWithProof(key []byte, reqHeight int64, node rpcclient.Client, // GetWithProofOptions is useful if you want full access to the ABCIQueryOptions func GetWithProofOptions(path string, key []byte, opts rpcclient.ABCIQueryOptions, node rpcclient.Client, cert lite.Certifier) ( - *ctypes.ResultABCIQuery, iavl.KeyProof, error) { + *ctypes.ResultABCIQuery, KeyProof, error) { _resp, err := node.ABCIQueryWithOptions(path, key, opts) if err != nil { @@ -51,7 +64,7 @@ func GetWithProofOptions(path string, key []byte, opts rpcclient.ABCIQueryOption // make sure the proof is the proper height if resp.IsErr() { - err = errors.Errorf("Query error %d: %d", resp.Code) + err = errors.Errorf("Query error for key %d: %d", key, resp.Code) return nil, nil, err } if len(resp.Key) == 0 || len(resp.Proof) == 0 { @@ -67,39 +80,54 @@ func GetWithProofOptions(path string, key []byte, opts rpcclient.ABCIQueryOption return nil, nil, err } + _ = commit + return &ctypes.ResultABCIQuery{Response: resp}, nil, nil + + /* // TODO refactor so iavl stuff is not in tendermint core + // https://github.com/tendermint/tendermint/issues/1183 if len(resp.Value) > 0 { // The key was found, construct a proof of existence. - eproof, err := iavl.ReadKeyExistsProof(resp.Proof) + proof, err := iavl.ReadKeyProof(resp.Proof) if err != nil { return nil, nil, errors.Wrap(err, "Error reading proof") } + eproof, ok := proof.(*iavl.KeyExistsProof) + if !ok { + return nil, nil, errors.New("Expected KeyExistsProof for non-empty value") + } + // Validate the proof against the certified header to ensure data integrity. err = eproof.Verify(resp.Key, resp.Value, commit.Header.AppHash) if err != nil { return nil, nil, errors.Wrap(err, "Couldn't verify proof") } - return &ctypes.ResultABCIQuery{resp}, eproof, nil + return &ctypes.ResultABCIQuery{Response: resp}, eproof, nil } // The key wasn't found, construct a proof of non-existence. - var aproof *iavl.KeyAbsentProof - aproof, err = iavl.ReadKeyAbsentProof(resp.Proof) + proof, err := iavl.ReadKeyProof(resp.Proof) if err != nil { return nil, nil, errors.Wrap(err, "Error reading proof") } + + aproof, ok := proof.(*iavl.KeyAbsentProof) + if !ok { + return nil, nil, errors.New("Expected KeyAbsentProof for empty Value") + } + // Validate the proof against the certified header to ensure data integrity. err = aproof.Verify(resp.Key, nil, commit.Header.AppHash) if err != nil { return nil, nil, errors.Wrap(err, "Couldn't verify proof") } - return &ctypes.ResultABCIQuery{resp}, aproof, ErrNoData() + return &ctypes.ResultABCIQuery{Response: resp}, aproof, ErrNoData() + */ } // GetCertifiedCommit gets the signed header for a given height // and certifies it. Returns error if unable to get a proven header. -func GetCertifiedCommit(h int64, node rpcclient.Client, - cert lite.Certifier) (empty lite.Commit, err error) { +func GetCertifiedCommit(h int64, node rpcclient.Client, cert lite.Certifier) (lite.Commit, error) { // FIXME: cannot use cert.GetByHeight for now, as it also requires // Validators and will fail on querying tendermint for non-current height. @@ -107,14 +135,18 @@ func GetCertifiedCommit(h int64, node rpcclient.Client, rpcclient.WaitForHeight(node, h, nil) cresp, err := node.Commit(&h) if err != nil { - return + return lite.Commit{}, err } - commit := client.CommitFromResult(cresp) + commit := client.CommitFromResult(cresp) // validate downloaded checkpoint with our request and trust store. if commit.Height() != h { - return empty, certerr.ErrHeightMismatch(h, commit.Height()) + return lite.Commit{}, certerr.ErrHeightMismatch(h, commit.Height()) + } + + if err = cert.Certify(commit); err != nil { + return lite.Commit{}, err } - err = cert.Certify(commit) + return commit, nil } diff --git a/lite/proxy/query_test.go b/lite/proxy/query_test.go index 234f65e55..6fc4b9730 100644 --- a/lite/proxy/query_test.go +++ b/lite/proxy/query_test.go @@ -58,7 +58,7 @@ func _TestAppProofs(t *testing.T) { source := certclient.NewProvider(cl) seed, err := source.GetByHeight(brh - 2) require.NoError(err, "%+v", err) - cert := lite.NewStatic("my-chain", seed.Validators) + cert := lite.NewStaticCertifier("my-chain", seed.Validators) client.WaitForHeight(cl, 3, nil) latest, err := source.LatestCommit() @@ -117,7 +117,7 @@ func _TestTxProofs(t *testing.T) { source := certclient.NewProvider(cl) seed, err := source.GetByHeight(brh - 2) require.NoError(err, "%+v", err) - cert := lite.NewStatic("my-chain", seed.Validators) + cert := lite.NewStaticCertifier("my-chain", seed.Validators) // First let's make sure a bogus transaction hash returns a valid non-existence proof. key := types.Tx([]byte("bogus")).Hash() @@ -136,5 +136,4 @@ func _TestTxProofs(t *testing.T) { commit, err := GetCertifiedCommit(br.Height, cl, cert) require.Nil(err, "%+v", err) require.Equal(res.Proof.RootHash, commit.Header.DataHash) - } diff --git a/lite/proxy/wrapper.go b/lite/proxy/wrapper.go index a76c29426..e8aa011e1 100644 --- a/lite/proxy/wrapper.go +++ b/lite/proxy/wrapper.go @@ -1,7 +1,7 @@ package proxy import ( - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tendermint/lite" certclient "github.com/tendermint/tendermint/lite/client" @@ -15,14 +15,14 @@ var _ rpcclient.Client = Wrapper{} // provable before passing it along. Allows you to make any rpcclient fully secure. type Wrapper struct { rpcclient.Client - cert *lite.Inquiring + cert *lite.InquiringCertifier } // SecureClient uses a given certifier to wrap an connection to an untrusted // host and return a cryptographically secure rpc client. // // If it is wrapping an HTTP rpcclient, it will also wrap the websocket interface -func SecureClient(c rpcclient.Client, cert *lite.Inquiring) Wrapper { +func SecureClient(c rpcclient.Client, cert *lite.InquiringCertifier) Wrapper { wrap := Wrapper{c, cert} // TODO: no longer possible as no more such interface exposed.... // if we wrap http client, then we can swap out the event switch to filter @@ -34,13 +34,15 @@ func SecureClient(c rpcclient.Client, cert *lite.Inquiring) Wrapper { } // ABCIQueryWithOptions exposes all options for the ABCI query and verifies the returned proof -func (w Wrapper) ABCIQueryWithOptions(path string, data data.Bytes, opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (w Wrapper) ABCIQueryWithOptions(path string, data cmn.HexBytes, + opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { + res, _, err := GetWithProofOptions(path, data, opts, w.Client, w.cert) return res, err } // ABCIQuery uses default options for the ABCI query and verifies the returned proof -func (w Wrapper) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (w Wrapper) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return w.ABCIQueryWithOptions(path, data, rpcclient.DefaultABCIQueryOptions) } diff --git a/lite/static.go b/lite/static_certifier.go similarity index 54% rename from lite/static.go rename to lite/static_certifier.go index abbef5785..1ec3b809a 100644 --- a/lite/static.go +++ b/lite/static_certifier.go @@ -10,62 +10,64 @@ import ( liteErr "github.com/tendermint/tendermint/lite/errors" ) -var _ Certifier = &Static{} +var _ Certifier = (*StaticCertifier)(nil) -// Static assumes a static set of validators, set on +// StaticCertifier assumes a static set of validators, set on // initilization and checks against them. // The signatures on every header is checked for > 2/3 votes // against the known validator set upon Certify // // Good for testing or really simple chains. Building block // to support real-world functionality. -type Static struct { +type StaticCertifier struct { chainID string vSet *types.ValidatorSet vhash []byte } -// NewStatic returns a new certifier with a static validator set. -func NewStatic(chainID string, vals *types.ValidatorSet) *Static { - return &Static{ +// NewStaticCertifier returns a new certifier with a static validator set. +func NewStaticCertifier(chainID string, vals *types.ValidatorSet) *StaticCertifier { + return &StaticCertifier{ chainID: chainID, vSet: vals, } } // ChainID returns the chain id. -func (c *Static) ChainID() string { - return c.chainID +// Implements Certifier. +func (sc *StaticCertifier) ChainID() string { + return sc.chainID } // Validators returns the validator set. -func (c *Static) Validators() *types.ValidatorSet { - return c.vSet +func (sc *StaticCertifier) Validators() *types.ValidatorSet { + return sc.vSet } // Hash returns the hash of the validator set. -func (c *Static) Hash() []byte { - if len(c.vhash) == 0 { - c.vhash = c.vSet.Hash() +func (sc *StaticCertifier) Hash() []byte { + if len(sc.vhash) == 0 { + sc.vhash = sc.vSet.Hash() } - return c.vhash + return sc.vhash } // Certify makes sure that the commit is valid. -func (c *Static) Certify(commit Commit) error { +// Implements Certifier. +func (sc *StaticCertifier) Certify(commit Commit) error { // do basic sanity checks - err := commit.ValidateBasic(c.chainID) + err := commit.ValidateBasic(sc.chainID) if err != nil { return err } // make sure it has the same validator set we have (static means static) - if !bytes.Equal(c.Hash(), commit.Header.ValidatorsHash) { + if !bytes.Equal(sc.Hash(), commit.Header.ValidatorsHash) { return liteErr.ErrValidatorsChanged() } // then make sure we have the proper signatures for this - err = c.vSet.VerifyCommit(c.chainID, commit.Commit.BlockID, + err = sc.vSet.VerifyCommit(sc.chainID, commit.Commit.BlockID, commit.Header.Height, commit.Commit) return errors.WithStack(err) } diff --git a/lite/static_test.go b/lite/static_certifier_test.go similarity index 97% rename from lite/static_test.go rename to lite/static_certifier_test.go index 3e4d59271..03567daa6 100644 --- a/lite/static_test.go +++ b/lite/static_certifier_test.go @@ -21,7 +21,7 @@ func TestStaticCert(t *testing.T) { vals := keys.ToValidators(20, 10) // and a certifier based on our known set chainID := "test-static" - cert := lite.NewStatic(chainID, vals) + cert := lite.NewStaticCertifier(chainID, vals) cases := []struct { keys lite.ValKeys diff --git a/mempool/mempool.go b/mempool/mempool.go index ccd615ac2..ec4f98478 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -3,7 +3,6 @@ package mempool import ( "bytes" "container/list" - "fmt" "sync" "sync/atomic" "time" @@ -49,7 +48,7 @@ TODO: Better handle abci client errors. (make it automatically handle connection */ -const cacheSize = 100000 +var ErrTxInCache = errors.New("Tx already exists in cache") // Mempool is an ordered in-memory pool for transactions before they are proposed in a consensus // round. Transaction validity is checked using the CheckTx abci message before the transaction is @@ -92,9 +91,8 @@ func NewMempool(config *cfg.MempoolConfig, proxyAppConn proxy.AppConnMempool, he recheckCursor: nil, recheckEnd: nil, logger: log.NewNopLogger(), - cache: newTxCache(cacheSize), + cache: newTxCache(config.CacheSize), } - mempool.initWAL() proxyAppConn.SetResponseCallback(mempool.resCb) return mempool } @@ -131,7 +129,7 @@ func (mem *Mempool) CloseWAL() bool { return true } -func (mem *Mempool) initWAL() { +func (mem *Mempool) InitWAL() { walDir := mem.config.WalDir() if walDir != "" { err := cmn.EnsureDir(walDir, 0700) @@ -161,6 +159,12 @@ func (mem *Mempool) Size() int { return mem.txs.Len() } +// Flushes the mempool connection to ensure async resCb calls are done e.g. +// from CheckTx. +func (mem *Mempool) FlushAppConn() error { + return mem.proxyAppConn.FlushSync() +} + // Flush removes all transactions from the mempool and cache func (mem *Mempool) Flush() { mem.proxyMtx.Lock() @@ -174,10 +178,17 @@ func (mem *Mempool) Flush() { } } -// TxsFrontWait returns the first transaction in the ordered list for peer goroutines to call .NextWait() on. -// It blocks until the mempool is not empty (ie. until the internal `mem.txs` has at least one element) -func (mem *Mempool) TxsFrontWait() *clist.CElement { - return mem.txs.FrontWait() +// TxsFront returns the first transaction in the ordered list for peer +// goroutines to call .NextWait() on. +func (mem *Mempool) TxsFront() *clist.CElement { + return mem.txs.Front() +} + +// TxsWaitChan returns a channel to wait on transactions. It will be closed +// once the mempool is not empty (ie. the internal `mem.txs` has at least one +// element) +func (mem *Mempool) TxsWaitChan() <-chan struct{} { + return mem.txs.WaitChan() } // CheckTx executes a new transaction against the application to determine its validity @@ -192,7 +203,7 @@ func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { // CACHE if mem.cache.Exists(tx) { - return fmt.Errorf("Tx already exists in cache") + return ErrTxInCache } mem.cache.Push(tx) // END CACHE @@ -349,9 +360,6 @@ func (mem *Mempool) collectTxs(maxTxs int) types.Txs { // NOTE: this should be called *after* block is committed by consensus. // NOTE: unsafe; Lock/Unlock must be managed by caller func (mem *Mempool) Update(height int64, txs types.Txs) error { - if err := mem.proxyAppConn.FlushSync(); err != nil { // To flush async resCb calls e.g. from CheckTx - return err - } // First, create a lookup map of txns in new txs. txsMap := make(map[string]struct{}) for _, tx := range txs { @@ -449,7 +457,7 @@ func newTxCache(cacheSize int) *txCache { // Reset resets the txCache to empty. func (cache *txCache) Reset() { cache.mtx.Lock() - cache.map_ = make(map[string]struct{}, cacheSize) + cache.map_ = make(map[string]struct{}, cache.size) cache.list.Init() cache.mtx.Unlock() } diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 4d75cc58d..5b04e9493 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -185,7 +185,7 @@ func TestSerialReap(t *testing.T) { t.Errorf("Client error committing: %v", err) } if len(res.Data) != 8 { - t.Errorf("Error committing. Hash:%X log:%v", res.Data, res.Log) + t.Errorf("Error committing. Hash:%X", res.Data) } } @@ -236,12 +236,13 @@ func TestMempoolCloseWAL(t *testing.T) { require.Equal(t, 0, len(m1), "no matches yet") // 3. Create the mempool - wcfg := *(cfg.DefaultMempoolConfig()) + wcfg := cfg.DefaultMempoolConfig() wcfg.RootDir = rootDir app := dummy.NewDummyApplication() cc := proxy.NewLocalClientCreator(app) appConnMem, _ := cc.NewABCIClient() - mempool := NewMempool(&wcfg, appConnMem, 10) + mempool := NewMempool(wcfg, appConnMem, 10) + mempool.InitWAL() // 4. Ensure that the directory contains the WAL file m2, err := filepath.Glob(filepath.Join(rootDir, "*")) diff --git a/mempool/reactor.go b/mempool/reactor.go index 4523f824a..58650a197 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -101,8 +101,6 @@ type PeerState interface { } // Send new mempool txs to peer. -// TODO: Handle mempool or reactor shutdown? -// As is this routine may block forever if no new txs come in. func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { if !memR.config.Broadcast { return @@ -110,15 +108,22 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { var next *clist.CElement for { - if !memR.IsRunning() || !peer.IsRunning() { - return // Quit! - } + // This happens because the CElement we were looking at got garbage + // collected (removed). That is, .NextWait() returned nil. Go ahead and + // start from the beginning. if next == nil { - // This happens because the CElement we were looking at got - // garbage collected (removed). That is, .NextWait() returned nil. - // Go ahead and start from the beginning. - next = memR.Mempool.TxsFrontWait() // Wait until a tx is available + select { + case <-memR.Mempool.TxsWaitChan(): // Wait until a tx is available + if next = memR.Mempool.TxsFront(); next == nil { + continue + } + case <-peer.Quit(): + return + case <-memR.Quit(): + return + } } + memTx := next.Value.(*mempoolTx) // make sure the peer is up to date height := memTx.Height() @@ -137,8 +142,15 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { continue } - next = next.NextWait() - continue + select { + case <-next.NextWaitChan(): + // see the start of the for loop for nil check + next = next.Next() + case <-peer.Quit(): + return + case <-memR.Quit(): + return + } } } diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index 45458a983..3cbc57481 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/fortytw2/leaktest" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/go-kit/kit/log/term" @@ -91,18 +93,65 @@ func _waitForTxs(t *testing.T, wg *sync.WaitGroup, txs types.Txs, reactorIdx int wg.Done() } -var ( +const ( NUM_TXS = 1000 TIMEOUT = 120 * time.Second // ridiculously high because CircleCI is slow ) func TestReactorBroadcastTxMessage(t *testing.T) { config := cfg.TestConfig() - N := 4 + const N = 4 reactors := makeAndConnectMempoolReactors(config, N) + defer func() { + for _, r := range reactors { + r.Stop() + } + }() // send a bunch of txs to the first reactor's mempool // and wait for them all to be received in the others txs := checkTxs(t, reactors[0].Mempool, NUM_TXS) waitForTxs(t, txs, reactors) } + +func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + config := cfg.TestConfig() + const N = 2 + reactors := makeAndConnectMempoolReactors(config, N) + defer func() { + for _, r := range reactors { + r.Stop() + } + }() + + // stop peer + sw := reactors[1].Switch + sw.StopPeerForError(sw.Peers().List()[0], errors.New("some reason")) + + // check that we are not leaking any go-routines + // i.e. broadcastTxRoutine finishes when peer is stopped + leaktest.CheckTimeout(t, 10*time.Second)() +} + +func TestBroadcastTxForPeerStopsWhenReactorStops(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + config := cfg.TestConfig() + const N = 2 + reactors := makeAndConnectMempoolReactors(config, N) + + // stop reactors + for _, r := range reactors { + r.Stop() + } + + // check that we are not leaking any go-routines + // i.e. broadcastTxRoutine finishes when reactor is stopped + leaktest.CheckTimeout(t, 10*time.Second)() +} diff --git a/node/node.go b/node/node.go index f922d8321..cd2b4bcb5 100644 --- a/node/node.go +++ b/node/node.go @@ -18,10 +18,11 @@ import ( bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/consensus" + cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/evidence" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/pex" "github.com/tendermint/tendermint/p2p/trust" "github.com/tendermint/tendermint/proxy" rpccore "github.com/tendermint/tendermint/rpc/core" @@ -52,7 +53,8 @@ type DBProvider func(*DBContext) (dbm.DB, error) // DefaultDBProvider returns a database using the DBBackend and DBDir // specified in the ctx.Config. func DefaultDBProvider(ctx *DBContext) (dbm.DB, error) { - return dbm.NewDB(ctx.ID, ctx.Config.DBBackend, ctx.Config.DBDir()), nil + dbType := dbm.DBBackendType(ctx.Config.DBBackend) + return dbm.NewDB(ctx.ID, dbType, ctx.Config.DBDir()), nil } // GenesisDocProvider returns a GenesisDoc. @@ -96,22 +98,21 @@ type Node struct { privValidator types.PrivValidator // local node's validator key // network - privKey crypto.PrivKeyEd25519 // local node's p2p key sw *p2p.Switch // p2p connections - addrBook *p2p.AddrBook // known peers + addrBook pex.AddrBook // known peers trustMetricStore *trust.TrustMetricStore // trust metrics for all peers // 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.MempoolReactor // for gossipping transactions - consensusState *consensus.ConsensusState // latest consensus state - consensusReactor *consensus.ConsensusReactor // for participating in the consensus - evidencePool *evidence.EvidencePool // tracking evidence - proxyApp proxy.AppConns // connection to the application - rpcListeners []net.Listener // rpc servers + blockStore *bc.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing + mempoolReactor *mempl.MempoolReactor // for gossipping transactions + consensusState *cs.ConsensusState // latest consensus state + consensusReactor *cs.ConsensusReactor // for participating in the consensus + evidencePool *evidence.EvidencePool // tracking evidence + proxyApp proxy.AppConns // connection to the application + rpcListeners []net.Listener // rpc servers txIndexer txindex.TxIndexer indexerService *txindex.IndexerService } @@ -159,7 +160,7 @@ func NewNode(config *cfg.Config, // and sync tendermint and the app by performing a handshake // and replaying any necessary blocks consensusLogger := logger.With("module", "consensus") - handshaker := consensus.NewHandshaker(stateDB, state, blockStore) + handshaker := cs.NewHandshaker(stateDB, state, blockStore) handshaker.SetLogger(consensusLogger) proxyApp := proxy.NewAppConns(clientCreator, handshaker) proxyApp.SetLogger(logger.With("module", "proxy")) @@ -170,9 +171,6 @@ func NewNode(config *cfg.Config, // reload the state (it may have been updated by the handshake) state = sm.LoadState(stateDB) - // Generate node PrivKey - privKey := crypto.GenPrivKeyEd25519() - // Decide whether to fast-sync or not // We don't fast-sync when the only validator is us. fastSync := config.FastSync @@ -185,14 +183,15 @@ func NewNode(config *cfg.Config, // Log whether this node is a validator or an observer if state.Validators.HasAddress(privValidator.GetAddress()) { - consensusLogger.Info("This node is a validator") + consensusLogger.Info("This node is a validator", "addr", privValidator.GetAddress(), "pubKey", privValidator.GetPubKey()) } else { - consensusLogger.Info("This node is not a validator") + consensusLogger.Info("This node is not a validator", "addr", privValidator.GetAddress(), "pubKey", privValidator.GetPubKey()) } // Make MempoolReactor mempoolLogger := logger.With("module", "mempool") mempool := mempl.NewMempool(config.Mempool, proxyApp.Mempool(), state.LastBlockHeight) + mempool.InitWAL() // no need to have the mempool wal during tests mempool.SetLogger(mempoolLogger) mempoolReactor := mempl.NewMempoolReactor(config.Mempool, mempool) mempoolReactor.SetLogger(mempoolLogger) @@ -222,13 +221,13 @@ func NewNode(config *cfg.Config, bcReactor.SetLogger(logger.With("module", "blockchain")) // Make ConsensusReactor - consensusState := consensus.NewConsensusState(config.Consensus, state.Copy(), + consensusState := cs.NewConsensusState(config.Consensus, state.Copy(), blockExec, blockStore, mempool, evidencePool) consensusState.SetLogger(consensusLogger) if privValidator != nil { consensusState.SetPrivValidator(privValidator) } - consensusReactor := consensus.NewConsensusReactor(consensusState, fastSync) + consensusReactor := cs.NewConsensusReactor(consensusState, fastSync) consensusReactor.SetLogger(consensusLogger) p2pLogger := logger.With("module", "p2p") @@ -241,10 +240,10 @@ func NewNode(config *cfg.Config, sw.AddReactor("EVIDENCE", evidenceReactor) // Optionally, start the pex reactor - var addrBook *p2p.AddrBook + var addrBook pex.AddrBook var trustMetricStore *trust.TrustMetricStore if config.P2P.PexReactor { - addrBook = p2p.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) + addrBook = pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) // Get the trust metric history data @@ -255,7 +254,12 @@ func NewNode(config *cfg.Config, trustMetricStore = trust.NewTrustMetricStore(trustHistoryDB, trust.DefaultConfig()) trustMetricStore.SetLogger(p2pLogger) - pexReactor := p2p.NewPEXReactor(addrBook) + var seeds []string + if config.P2P.Seeds != "" { + seeds = strings.Split(config.P2P.Seeds, ",") + } + pexReactor := pex.NewPEXReactor(addrBook, + &pex.PEXReactorConfig{Seeds: seeds, SeedMode: config.P2P.SeedMode}) pexReactor.SetLogger(p2pLogger) sw.AddReactor("PEX", pexReactor) } @@ -271,17 +275,17 @@ func NewNode(config *cfg.Config, return err } if resQuery.IsErr() { - return resQuery + return fmt.Errorf("Error querying abci app: %v", resQuery) } return nil }) - sw.SetPubKeyFilter(func(pubkey crypto.PubKeyEd25519) error { + sw.SetPubKeyFilter(func(pubkey crypto.PubKey) error { resQuery, err := proxyApp.Query().QuerySync(abci.RequestQuery{Path: cmn.Fmt("/p2p/filter/pubkey/%X", pubkey.Bytes())}) if err != nil { return err } if resQuery.IsErr() { - return resQuery + return fmt.Errorf("Error querying abci app: %v", resQuery) } return nil }) @@ -328,7 +332,6 @@ func NewNode(config *cfg.Config, genesisDoc: genDoc, privValidator: privValidator, - privKey: privKey, sw: sw, addrBook: addrBook, trustMetricStore: trustMetricStore, @@ -371,19 +374,26 @@ func (n *Node) OnStart() error { l := p2p.NewDefaultListener(protocol, address, n.config.P2P.SkipUPNP, n.Logger.With("module", "p2p")) n.sw.AddListener(l) + // Generate node PrivKey + // TODO: pass in like priv_val + nodeKey, err := p2p.LoadOrGenNodeKey(n.config.NodeKeyFile()) + if err != nil { + return err + } + n.Logger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", n.config.NodeKeyFile()) + // Start the switch - n.sw.SetNodeInfo(n.makeNodeInfo()) - n.sw.SetNodePrivKey(n.privKey) + n.sw.SetNodeInfo(n.makeNodeInfo(nodeKey.PubKey())) + n.sw.SetNodeKey(nodeKey) err = n.sw.Start() if err != nil { return err } - // If seeds exist, add them to the address book and dial out - if n.config.P2P.Seeds != "" { - // dial out - seeds := strings.Split(n.config.P2P.Seeds, ",") - if err := n.DialSeeds(seeds); err != nil { + // Always connect to persistent peers + if n.config.P2P.PersistentPeers != "" { + err = n.sw.DialPeersAsync(n.addrBook, strings.Split(n.config.P2P.PersistentPeers, ","), true) + if err != nil { return err } } @@ -494,12 +504,12 @@ func (n *Node) BlockStore() *bc.BlockStore { } // ConsensusState returns the Node's ConsensusState. -func (n *Node) ConsensusState() *consensus.ConsensusState { +func (n *Node) ConsensusState() *cs.ConsensusState { return n.consensusState } // ConsensusReactor returns the Node's ConsensusReactor. -func (n *Node) ConsensusReactor() *consensus.ConsensusReactor { +func (n *Node) ConsensusReactor() *cs.ConsensusReactor { return n.consensusReactor } @@ -534,25 +544,35 @@ func (n *Node) ProxyApp() proxy.AppConns { return n.proxyApp } -func (n *Node) makeNodeInfo() *p2p.NodeInfo { +func (n *Node) makeNodeInfo(pubKey crypto.PubKey) p2p.NodeInfo { txIndexerStatus := "on" if _, ok := n.txIndexer.(*null.TxIndex); ok { txIndexerStatus = "off" } - nodeInfo := &p2p.NodeInfo{ - PubKey: n.privKey.PubKey().Unwrap().(crypto.PubKeyEd25519), - Moniker: n.config.Moniker, + nodeInfo := p2p.NodeInfo{ + PubKey: pubKey, Network: n.genesisDoc.ChainID, Version: version.Version, + Channels: []byte{ + bc.BlockchainChannel, + cs.StateChannel, cs.DataChannel, cs.VoteChannel, cs.VoteSetBitsChannel, + mempl.MempoolChannel, + evidence.EvidenceChannel, + }, + Moniker: n.config.Moniker, Other: []string{ cmn.Fmt("wire_version=%v", wire.Version), cmn.Fmt("p2p_version=%v", p2p.Version), - cmn.Fmt("consensus_version=%v", consensus.Version), + cmn.Fmt("consensus_version=%v", cs.Version), cmn.Fmt("rpc_version=%v/%v", rpc.Version, rpccore.Version), cmn.Fmt("tx_index=%v", txIndexerStatus), }, } + if n.config.P2P.PexReactor { + nodeInfo.Channels = append(nodeInfo.Channels, pex.PexChannel) + } + rpcListenAddr := n.config.RPC.ListenAddress nodeInfo.Other = append(nodeInfo.Other, cmn.Fmt("rpc_addr=%v", rpcListenAddr)) @@ -571,15 +591,10 @@ func (n *Node) makeNodeInfo() *p2p.NodeInfo { //------------------------------------------------------------------------------ // NodeInfo returns the Node's Info from the Switch. -func (n *Node) NodeInfo() *p2p.NodeInfo { +func (n *Node) NodeInfo() p2p.NodeInfo { return n.sw.NodeInfo() } -// DialSeeds dials the given seeds on the Switch. -func (n *Node) DialSeeds(seeds []string) error { - return n.sw.DialSeeds(n.addrBook, seeds) -} - //------------------------------------------------------------------------------ var ( diff --git a/node/node_test.go b/node/node_test.go index eb8d109f3..ca5393823 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -41,7 +41,7 @@ func TestNodeStartStop(t *testing.T) { }() select { - case <-n.Quit: + case <-n.Quit(): case <-time.After(5 * time.Second): t.Fatal("timed out waiting for shutdown") } diff --git a/p2p/Dockerfile b/p2p/Dockerfile deleted file mode 100644 index 6c71b2f81..000000000 --- a/p2p/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM golang:latest - -RUN curl https://glide.sh/get | sh - -RUN mkdir -p /go/src/github.com/tendermint/tendermint/p2p -WORKDIR /go/src/github.com/tendermint/tendermint/p2p - -COPY glide.yaml /go/src/github.com/tendermint/tendermint/p2p/ -COPY glide.lock /go/src/github.com/tendermint/tendermint/p2p/ - -RUN glide install - -COPY . /go/src/github.com/tendermint/tendermint/p2p diff --git a/p2p/README.md b/p2p/README.md index d653b2caf..9a8ddc6c3 100644 --- a/p2p/README.md +++ b/p2p/README.md @@ -1,122 +1,11 @@ -# `tendermint/tendermint/p2p` +# p2p -[![CircleCI](https://circleci.com/gh/tendermint/tendermint/p2p.svg?style=svg)](https://circleci.com/gh/tendermint/tendermint/p2p) +The p2p package provides an abstraction around peer-to-peer communication. -`tendermint/tendermint/p2p` provides an abstraction around peer-to-peer communication.
+Docs: -## MConnection - -`MConnection` is a multiplex connection: - -__multiplex__ *noun* a system or signal involving simultaneous transmission of -several messages along a single channel of communication. - -Each `MConnection` handles message transmission on multiple abstract communication -`Channel`s. Each channel has a globally unique byte id. -The byte id and the relative priorities of each `Channel` are configured upon -initialization of the connection. - -The `MConnection` supports three packet types: Ping, Pong, and Msg. - -### Ping and Pong - -The ping and pong messages consist of writing a single byte to the connection; 0x1 and 0x2, respectively - -When we haven't received any messages on an `MConnection` in a time `pingTimeout`, we send a ping message. -When a ping is received on the `MConnection`, a pong is sent in response. - -If a pong is not received in sufficient time, the peer's score should be decremented (TODO). - -### Msg - -Messages in channels are chopped into smaller msgPackets for multiplexing. - -``` -type msgPacket struct { - ChannelID byte - EOF byte // 1 means message ends here. - Bytes []byte -} -``` - -The msgPacket is serialized using go-wire, and prefixed with a 0x3. -The received `Bytes` of a sequential set of packets are appended together -until a packet with `EOF=1` is received, at which point the complete serialized message -is returned for processing by the corresponding channels `onReceive` function. - -### Multiplexing - -Messages are sent from a single `sendRoutine`, which loops over a select statement that results in the sending -of a ping, a pong, or a batch of data messages. The batch of data messages may include messages from multiple channels. -Message bytes are queued for sending in their respective channel, with each channel holding one unsent message at a time. -Messages are chosen for a batch one a time from the channel with the lowest ratio of recently sent bytes to channel priority. - -## Sending Messages - -There are two methods for sending messages: -```go -func (m MConnection) Send(chID byte, msg interface{}) bool {} -func (m MConnection) TrySend(chID byte, msg interface{}) bool {} -``` - -`Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued -for the channel with the given id byte `chID`. The message `msg` is serialized -using the `tendermint/wire` submodule's `WriteBinary()` reflection routine. - -`TrySend(chID, msg)` is a nonblocking call that returns false if the channel's -queue is full. - -`Send()` and `TrySend()` are also exposed for each `Peer`. - -## Peer - -Each peer has one `MConnection` instance, and includes other information such as whether the connection -was outbound, whether the connection should be recreated if it closes, various identity information about the node, -and other higher level thread-safe data used by the reactors. - -## Switch/Reactor - -The `Switch` handles peer connections and exposes an API to receive incoming messages -on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one -or more `Channels`. So while sending outgoing messages is typically performed on the peer, -incoming messages are received on the reactor. - -```go -// Declare a MyReactor reactor that handles messages on MyChannelID. -type MyReactor struct{} - -func (reactor MyReactor) GetChannels() []*ChannelDescriptor { - return []*ChannelDescriptor{ChannelDescriptor{ID:MyChannelID, Priority: 1}} -} - -func (reactor MyReactor) Receive(chID byte, peer *Peer, msgBytes []byte) { - r, n, err := bytes.NewBuffer(msgBytes), new(int64), new(error) - msgString := ReadString(r, n, err) - fmt.Println(msgString) -} - -// Other Reactor methods omitted for brevity -... - -switch := NewSwitch([]Reactor{MyReactor{}}) - -... - -// Send a random message to all outbound connections -for _, peer := range switch.Peers().List() { - if peer.IsOutbound() { - peer.Send(MyChannelID, "Here's a random message") - } -} -``` - -### PexReactor/AddrBook - -A `PEXReactor` reactor implementation is provided to automate peer discovery. - -```go -book := p2p.NewAddrBook(addrBookFilePath) -pexReactor := p2p.NewPEXReactor(book) -... -switch := NewSwitch([]Reactor{pexReactor, myReactor, ...}) -``` +- [Connection](../docs/specification/new-spec/p2p/connection.md) for details on how connections and multiplexing work +- [Peer](../docs/specification/new-spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange +- [Node](../docs/specification/new-spec/p2p/node.md) for details about different types of nodes and how they should work +- [Pex](../docs/specification/new-spec/p2p/pex.md) for details on peer discovery and exchange +- [Config](../docs/specification/new-spec/p2p/config.md) for details on some config option \ No newline at end of file diff --git a/p2p/base_reactor.go b/p2p/base_reactor.go new file mode 100644 index 000000000..f0dd14147 --- /dev/null +++ b/p2p/base_reactor.go @@ -0,0 +1,53 @@ +package p2p + +import ( + "github.com/tendermint/tendermint/p2p/conn" + cmn "github.com/tendermint/tmlibs/common" +) + +type Reactor interface { + cmn.Service // Start, Stop + + // SetSwitch allows setting a switch. + SetSwitch(*Switch) + + // GetChannels returns the list of channel descriptors. + GetChannels() []*conn.ChannelDescriptor + + // AddPeer is called by the switch when a new peer is added. + AddPeer(peer Peer) + + // RemovePeer is called by the switch when the peer is stopped (due to error + // or other reason). + RemovePeer(peer Peer, reason interface{}) + + // Receive is called when msgBytes is received from peer. + // + // NOTE reactor can not keep msgBytes around after Receive completes without + // copying. + // + // CONTRACT: msgBytes are not nil. + Receive(chID byte, peer Peer, msgBytes []byte) +} + +//-------------------------------------- + +type BaseReactor struct { + cmn.BaseService // Provides Start, Stop, .Quit + Switch *Switch +} + +func NewBaseReactor(name string, impl Reactor) *BaseReactor { + return &BaseReactor{ + BaseService: *cmn.NewBaseService(nil, name, impl), + Switch: nil, + } +} + +func (br *BaseReactor) SetSwitch(sw *Switch) { + br.Switch = sw +} +func (_ *BaseReactor) GetChannels() []*conn.ChannelDescriptor { return nil } +func (_ *BaseReactor) AddPeer(peer Peer) {} +func (_ *BaseReactor) RemovePeer(peer Peer, reason interface{}) {} +func (_ *BaseReactor) Receive(chID byte, peer Peer, msgBytes []byte) {} diff --git a/p2p/conn_go110.go b/p2p/conn/conn_go110.go similarity index 86% rename from p2p/conn_go110.go rename to p2p/conn/conn_go110.go index 2fca7c3df..682188101 100644 --- a/p2p/conn_go110.go +++ b/p2p/conn/conn_go110.go @@ -1,6 +1,6 @@ // +build go1.10 -package p2p +package conn // Go1.10 has a proper net.Conn implementation that // has the SetDeadline method implemented as per @@ -10,6 +10,6 @@ package p2p import "net" -func netPipe() (net.Conn, net.Conn) { +func NetPipe() (net.Conn, net.Conn) { return net.Pipe() } diff --git a/p2p/conn_notgo110.go b/p2p/conn/conn_notgo110.go similarity index 94% rename from p2p/conn_notgo110.go rename to p2p/conn/conn_notgo110.go index a5c2f7410..ed642eb54 100644 --- a/p2p/conn_notgo110.go +++ b/p2p/conn/conn_notgo110.go @@ -1,6 +1,6 @@ // +build !go1.10 -package p2p +package conn import ( "net" @@ -24,7 +24,7 @@ func (p *pipe) SetDeadline(t time.Time) error { return nil } -func netPipe() (net.Conn, net.Conn) { +func NetPipe() (net.Conn, net.Conn) { p1, p2 := net.Pipe() return &pipe{p1}, &pipe{p2} } diff --git a/p2p/connection.go b/p2p/conn/connection.go similarity index 86% rename from p2p/connection.go rename to p2p/conn/connection.go index 626aeb10f..9acaf6175 100644 --- a/p2p/connection.go +++ b/p2p/conn/connection.go @@ -1,7 +1,8 @@ -package p2p +package conn import ( "bufio" + "errors" "fmt" "io" "math" @@ -11,20 +12,16 @@ import ( "time" wire "github.com/tendermint/go-wire" - tmlegacy "github.com/tendermint/go-wire/nowriter/tmlegacy" cmn "github.com/tendermint/tmlibs/common" flow "github.com/tendermint/tmlibs/flowrate" "github.com/tendermint/tmlibs/log" ) -var legacy = tmlegacy.TMEncoderLegacy{} - const ( numBatchMsgPackets = 10 minReadBufferSize = 1024 minWriteBufferSize = 65536 updateStats = 2 * time.Second - pingTimeout = 40 * time.Second // some of these defaults are written in the user config // flushThrottle, sendRate, recvRate @@ -37,6 +34,8 @@ const ( defaultSendRate = int64(512000) // 500KB/s defaultRecvRate = int64(512000) // 500KB/s defaultSendTimeout = 10 * time.Second + defaultPingInterval = 60 * time.Second + defaultPongTimeout = 45 * time.Second ) type receiveCbFunc func(chID byte, msgBytes []byte) @@ -84,13 +83,17 @@ type MConnection struct { errored uint32 config *MConnConfig - quit chan struct{} - flushTimer *cmn.ThrottleTimer // flush writes as necessary but throttled. - pingTimer *cmn.RepeatTimer // send pings periodically - chStatsTimer *cmn.RepeatTimer // update channel stats periodically + quit chan struct{} + flushTimer *cmn.ThrottleTimer // flush writes as necessary but throttled. + pingTimer *cmn.RepeatTimer // send pings periodically + + // close conn if pong is not received in pongTimeout + pongTimer *time.Timer + pongTimeoutCh chan bool // true - timeout, false - peer sent pong + + chStatsTimer *cmn.RepeatTimer // update channel stats periodically - LocalAddress *NetAddress - RemoteAddress *NetAddress + created time.Time // time of creation } // MConnConfig is a MConnection configuration. @@ -98,13 +101,21 @@ type MConnConfig struct { SendRate int64 `mapstructure:"send_rate"` RecvRate int64 `mapstructure:"recv_rate"` - maxMsgPacketPayloadSize int + // Maximum payload size + MaxMsgPacketPayloadSize int `mapstructure:"max_msg_packet_payload_size"` + + // Interval to flush writes (throttled) + FlushThrottle time.Duration `mapstructure:"flush_throttle"` + + // Interval to send pings + PingInterval time.Duration `mapstructure:"ping_interval"` - flushThrottle time.Duration + // Maximum wait time for pongs + PongTimeout time.Duration `mapstructure:"pong_timeout"` } func (cfg *MConnConfig) maxMsgPacketTotalSize() int { - return cfg.maxMsgPacketPayloadSize + maxMsgPacketOverheadSize + return cfg.MaxMsgPacketPayloadSize + maxMsgPacketOverheadSize } // DefaultMConnConfig returns the default config. @@ -112,8 +123,10 @@ func DefaultMConnConfig() *MConnConfig { return &MConnConfig{ SendRate: defaultSendRate, RecvRate: defaultRecvRate, - maxMsgPacketPayloadSize: defaultMaxMsgPacketPayloadSize, - flushThrottle: defaultFlushThrottle, + MaxMsgPacketPayloadSize: defaultMaxMsgPacketPayloadSize, + FlushThrottle: defaultFlushThrottle, + PingInterval: defaultPingInterval, + PongTimeout: defaultPongTimeout, } } @@ -129,6 +142,10 @@ func NewMConnection(conn net.Conn, chDescs []*ChannelDescriptor, onReceive recei // NewMConnectionWithConfig wraps net.Conn and creates multiplex connection with a config func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc, config *MConnConfig) *MConnection { + if config.PongTimeout >= config.PingInterval { + panic("pongTimeout must be less than pingInterval (otherwise, next ping will reset pong timer)") + } + mconn := &MConnection{ conn: conn, bufReader: bufio.NewReaderSize(conn, minReadBufferSize), @@ -136,13 +153,10 @@ func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onRec sendMonitor: flow.New(0, 0), recvMonitor: flow.New(0, 0), send: make(chan struct{}, 1), - pong: make(chan struct{}), + pong: make(chan struct{}, 1), onReceive: onReceive, onError: onError, config: config, - - LocalAddress: NewNetAddress(conn.LocalAddr()), - RemoteAddress: NewNetAddress(conn.RemoteAddr()), } // Create channels @@ -175,8 +189,9 @@ func (c *MConnection) OnStart() error { return err } c.quit = make(chan struct{}) - c.flushTimer = cmn.NewThrottleTimer("flush", c.config.flushThrottle) - c.pingTimer = cmn.NewRepeatTimer("ping", pingTimeout) + c.flushTimer = cmn.NewThrottleTimer("flush", c.config.FlushThrottle) + c.pingTimer = cmn.NewRepeatTimer("ping", c.config.PingInterval) + c.pongTimeoutCh = make(chan bool, 1) c.chStatsTimer = cmn.NewRepeatTimer("chStats", updateStats) go c.sendRoutine() go c.recvRoutine() @@ -193,11 +208,11 @@ func (c *MConnection) OnStop() { close(c.quit) } c.conn.Close() // nolint: errcheck + // We can't close pong safely here because // recvRoutine may write to it after we've stopped. // Though it doesn't need to get closed at all, // we close it @ recvRoutine. - // close(c.pong) } func (c *MConnection) String() string { @@ -320,12 +335,26 @@ FOR_LOOP: } case <-c.pingTimer.Chan(): c.Logger.Debug("Send Ping") - legacy.WriteOctet(packetTypePing, c.bufWriter, &n, &err) + wire.WriteByte(packetTypePing, c.bufWriter, &n, &err) c.sendMonitor.Update(int(n)) + c.Logger.Debug("Starting pong timer", "dur", c.config.PongTimeout) + c.pongTimer = time.AfterFunc(c.config.PongTimeout, func() { + select { + case c.pongTimeoutCh <- true: + default: + } + }) c.flush() + case timeout := <-c.pongTimeoutCh: + if timeout { + c.Logger.Debug("Pong timeout") + err = errors.New("pong timeout") + } else { + c.stopPongTimer() + } case <-c.pong: c.Logger.Debug("Send Pong") - legacy.WriteOctet(packetTypePong, c.bufWriter, &n, &err) + wire.WriteByte(packetTypePong, c.bufWriter, &n, &err) c.sendMonitor.Update(int(n)) c.flush() case <-c.quit: @@ -353,6 +382,7 @@ FOR_LOOP: } // Cleanup + c.stopPongTimer() } // Returns true if messages from channels were exhausted. @@ -413,6 +443,7 @@ func (c *MConnection) sendMsgPacket() bool { // recvRoutine reads msgPackets and reconstructs the message using the channels' "recving" buffer. // After a whole message has been assembled, it's pushed to onReceive(). // Blocks depending on how the connection is throttled. +// Otherwise, it never blocks. func (c *MConnection) recvRoutine() { defer c._recover() @@ -453,11 +484,20 @@ FOR_LOOP: switch pktType { case packetTypePing: // TODO: prevent abuse, as they cause flush()'s. + // https://github.com/tendermint/tendermint/issues/1190 c.Logger.Debug("Receive Ping") - c.pong <- struct{}{} + select { + case c.pong <- struct{}{}: + default: + // never block + } case packetTypePong: - // do nothing c.Logger.Debug("Receive Pong") + select { + case c.pongTimeoutCh <- false: + default: + // never block + } case packetTypeMsg: pkt, n, err := msgPacket{}, int(0), error(nil) wire.ReadBinaryPtr(&pkt, c.bufReader, c.config.maxMsgPacketTotalSize(), &n, &err) @@ -474,6 +514,7 @@ FOR_LOOP: err := fmt.Errorf("Unknown channel %X", pkt.ChannelID) c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err) c.stopForError(err) + break FOR_LOOP } msgBytes, err := channel.recvMsgPacket(pkt) @@ -493,11 +534,8 @@ FOR_LOOP: err := fmt.Errorf("Unknown message type %X", pktType) c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err) c.stopForError(err) + break FOR_LOOP } - - // TODO: shouldn't this go in the sendRoutine? - // Better to send a ping packet when *we* haven't sent anything for a while. - c.pingTimer.Reset() } // Cleanup @@ -507,7 +545,18 @@ FOR_LOOP: } } +// not goroutine-safe +func (c *MConnection) stopPongTimer() { + if c.pongTimer != nil { + if !c.pongTimer.Stop() { + <-c.pongTimer.C + } + c.pongTimer = nil + } +} + type ConnectionStatus struct { + Duration time.Duration SendMonitor flow.Status RecvMonitor flow.Status Channels []ChannelStatus @@ -523,6 +572,7 @@ type ChannelStatus struct { func (c *MConnection) Status() ConnectionStatus { var status ConnectionStatus + status.Duration = time.Since(c.created) status.SendMonitor = c.sendMonitor.Status() status.RecvMonitor = c.recvMonitor.Status() status.Channels = make([]ChannelStatus, len(c.channels)) @@ -588,7 +638,7 @@ func newChannel(conn *MConnection, desc ChannelDescriptor) *Channel { desc: desc, sendQueue: make(chan []byte, desc.SendQueueCapacity), recving: make([]byte, 0, desc.RecvBufferCapacity), - maxMsgPacketPayloadSize: conn.config.maxMsgPacketPayloadSize, + maxMsgPacketPayloadSize: conn.config.MaxMsgPacketPayloadSize, } } @@ -677,11 +727,12 @@ func (ch *Channel) writeMsgPacketTo(w io.Writer) (n int, err error) { } func writeMsgPacketTo(packet msgPacket, w io.Writer, n *int, err *error) { - legacy.WriteOctet(packetTypeMsg, w, n, err) + wire.WriteByte(packetTypeMsg, w, n, err) wire.WriteBinary(packet, w, n, err) } -// Handles incoming msgPackets. Returns a msg bytes if msg is complete. +// Handles incoming msgPackets. It returns a message bytes if message is +// complete. NOTE message bytes may change on next call to recvMsgPacket. // Not goroutine-safe func (ch *Channel) recvMsgPacket(packet msgPacket) ([]byte, error) { ch.Logger.Debug("Read Msg Packet", "conn", ch.conn, "packet", packet) @@ -691,6 +742,7 @@ func (ch *Channel) recvMsgPacket(packet msgPacket) ([]byte, error) { ch.recving = append(ch.recving, packet.Bytes...) if packet.EOF == byte(0x01) { msgBytes := ch.recving + // clear the slice without re-allocating. // http://stackoverflow.com/questions/16971741/how-do-you-clear-a-slice-in-go // suggests this could be a memory leak, but we might as well keep the memory for the channel until it closes, diff --git a/p2p/connection_test.go b/p2p/conn/connection_test.go similarity index 63% rename from p2p/connection_test.go rename to p2p/conn/connection_test.go index 2a64764ea..d308ea61a 100644 --- a/p2p/connection_test.go +++ b/p2p/conn/connection_test.go @@ -1,4 +1,4 @@ -package p2p +package conn import ( "net" @@ -22,8 +22,11 @@ func createTestMConnection(conn net.Conn) *MConnection { } func createMConnectionWithCallbacks(conn net.Conn, onReceive func(chID byte, msgBytes []byte), onError func(r interface{})) *MConnection { + cfg := DefaultMConnConfig() + cfg.PingInterval = 90 * time.Millisecond + cfg.PongTimeout = 45 * time.Millisecond chDescs := []*ChannelDescriptor{&ChannelDescriptor{ID: 0x01, Priority: 1, SendQueueCapacity: 1}} - c := NewMConnection(conn, chDescs, onReceive, onError) + c := NewMConnectionWithConfig(conn, chDescs, onReceive, onError, cfg) c.SetLogger(log.TestingLogger()) return c } @@ -31,7 +34,7 @@ func createMConnectionWithCallbacks(conn net.Conn, onReceive func(chID byte, msg func TestMConnectionSend(t *testing.T) { assert, require := assert.New(t), require.New(t) - server, client := netPipe() + server, client := NetPipe() defer server.Close() // nolint: errcheck defer client.Close() // nolint: errcheck @@ -64,7 +67,7 @@ func TestMConnectionSend(t *testing.T) { func TestMConnectionReceive(t *testing.T) { assert, require := assert.New(t), require.New(t) - server, client := netPipe() + server, client := NetPipe() defer server.Close() // nolint: errcheck defer client.Close() // nolint: errcheck @@ -102,7 +105,7 @@ func TestMConnectionReceive(t *testing.T) { func TestMConnectionStatus(t *testing.T) { assert, require := assert.New(t), require.New(t) - server, client := netPipe() + server, client := NetPipe() defer server.Close() // nolint: errcheck defer client.Close() // nolint: errcheck @@ -116,10 +119,180 @@ func TestMConnectionStatus(t *testing.T) { assert.Zero(status.Channels[0].SendQueueSize) } +func TestMConnectionPongTimeoutResultsInError(t *testing.T) { + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn := createMConnectionWithCallbacks(client, onReceive, onError) + err := mconn.Start() + require.Nil(t, err) + defer mconn.Stop() + + serverGotPing := make(chan struct{}) + go func() { + // read ping + server.Read(make([]byte, 1)) + serverGotPing <- struct{}{} + }() + <-serverGotPing + + pongTimerExpired := mconn.config.PongTimeout + 20*time.Millisecond + select { + case msgBytes := <-receivedCh: + t.Fatalf("Expected error, but got %v", msgBytes) + case err := <-errorsCh: + assert.NotNil(t, err) + case <-time.After(pongTimerExpired): + t.Fatalf("Expected to receive error after %v", pongTimerExpired) + } +} + +func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) { + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn := createMConnectionWithCallbacks(client, onReceive, onError) + err := mconn.Start() + require.Nil(t, err) + defer mconn.Stop() + + // sending 3 pongs in a row (abuse) + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + + serverGotPing := make(chan struct{}) + go func() { + // read ping (one byte) + _, err = server.Read(make([]byte, 1)) + require.Nil(t, err) + serverGotPing <- struct{}{} + // respond with pong + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + }() + <-serverGotPing + + pongTimerExpired := mconn.config.PongTimeout + 20*time.Millisecond + select { + case msgBytes := <-receivedCh: + t.Fatalf("Expected no data, but got %v", msgBytes) + case err := <-errorsCh: + t.Fatalf("Expected no error, but got %v", err) + case <-time.After(pongTimerExpired): + assert.True(t, mconn.IsRunning()) + } +} + +func TestMConnectionMultiplePings(t *testing.T) { + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn := createMConnectionWithCallbacks(client, onReceive, onError) + err := mconn.Start() + require.Nil(t, err) + defer mconn.Stop() + + // sending 3 pings in a row (abuse) + // see https://github.com/tendermint/tendermint/issues/1190 + _, err = server.Write([]byte{packetTypePing}) + require.Nil(t, err) + _, err = server.Read(make([]byte, 1)) + require.Nil(t, err) + _, err = server.Write([]byte{packetTypePing}) + require.Nil(t, err) + _, err = server.Read(make([]byte, 1)) + require.Nil(t, err) + _, err = server.Write([]byte{packetTypePing}) + require.Nil(t, err) + _, err = server.Read(make([]byte, 1)) + require.Nil(t, err) + + assert.True(t, mconn.IsRunning()) +} + +func TestMConnectionPingPongs(t *testing.T) { + server, client := net.Pipe() + defer server.Close() + defer client.Close() + + receivedCh := make(chan []byte) + errorsCh := make(chan interface{}) + onReceive := func(chID byte, msgBytes []byte) { + receivedCh <- msgBytes + } + onError := func(r interface{}) { + errorsCh <- r + } + mconn := createMConnectionWithCallbacks(client, onReceive, onError) + err := mconn.Start() + require.Nil(t, err) + defer mconn.Stop() + + serverGotPing := make(chan struct{}) + go func() { + // read ping + server.Read(make([]byte, 1)) + serverGotPing <- struct{}{} + // respond with pong + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + + time.Sleep(mconn.config.PingInterval) + + // read ping + server.Read(make([]byte, 1)) + // respond with pong + _, err = server.Write([]byte{packetTypePong}) + require.Nil(t, err) + }() + <-serverGotPing + + pongTimerExpired := (mconn.config.PongTimeout + 20*time.Millisecond) * 2 + select { + case msgBytes := <-receivedCh: + t.Fatalf("Expected no data, but got %v", msgBytes) + case err := <-errorsCh: + t.Fatalf("Expected no error, but got %v", err) + case <-time.After(2 * pongTimerExpired): + assert.True(t, mconn.IsRunning()) + } +} + func TestMConnectionStopsAndReturnsError(t *testing.T) { assert, require := assert.New(t), require.New(t) - server, client := netPipe() + server, client := NetPipe() defer server.Close() // nolint: errcheck defer client.Close() // nolint: errcheck @@ -152,7 +325,7 @@ func TestMConnectionStopsAndReturnsError(t *testing.T) { } func newClientAndServerConnsForReadErrors(require *require.Assertions, chOnErr chan struct{}) (*MConnection, *MConnection) { - server, client := netPipe() + server, client := NetPipe() onReceive := func(chID byte, msgBytes []byte) {} onError := func(r interface{}) {} @@ -283,7 +456,7 @@ func TestMConnectionReadErrorUnknownMsgType(t *testing.T) { func TestMConnectionTrySend(t *testing.T) { assert, require := assert.New(t), require.New(t) - server, client := netPipe() + server, client := NetPipe() defer server.Close() defer client.Close() @@ -303,13 +476,7 @@ func TestMConnectionTrySend(t *testing.T) { mconn.TrySend(0x01, msg) resultCh <- "TrySend" }() - go func() { - mconn.Send(0x01, msg) - resultCh <- "Send" - }() assert.False(mconn.CanSend(0x01)) assert.False(mconn.TrySend(0x01, msg)) assert.Equal("TrySend", <-resultCh) - server.Read(make([]byte, len(msg))) - assert.Equal("Send", <-resultCh) // Order constrained by parallel blocking above } diff --git a/p2p/secret_connection.go b/p2p/conn/secret_connection.go similarity index 94% rename from p2p/secret_connection.go rename to p2p/conn/secret_connection.go index aec0a7519..aa6db05bb 100644 --- a/p2p/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -4,7 +4,7 @@ // is known ahead of time, and thus we are technically // still vulnerable to MITM. (TODO!) // See docs/sts-final.pdf for more info -package p2p +package conn import ( "bytes" @@ -38,7 +38,7 @@ type SecretConnection struct { recvBuffer []byte recvNonce *[24]byte sendNonce *[24]byte - remPubKey crypto.PubKeyEd25519 + remPubKey crypto.PubKey shrSecret *[32]byte // shared secret } @@ -46,9 +46,9 @@ type SecretConnection struct { // Returns nil if error in handshake. // Caller should call conn.Close() // See docs/sts-final.pdf for more information. -func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25519) (*SecretConnection, error) { +func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) { - locPubKey := locPrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + locPubKey := locPrivKey.PubKey() // Generate ephemeral keys for perfect forward secrecy. locEphPub, locEphPriv := genEphKeys() @@ -100,12 +100,12 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKeyEd25 } // We've authorized. - sc.remPubKey = remPubKey.Unwrap().(crypto.PubKeyEd25519) + sc.remPubKey = remPubKey return sc, nil } // Returns authenticated remote pubkey -func (sc *SecretConnection) RemotePubKey() crypto.PubKeyEd25519 { +func (sc *SecretConnection) RemotePubKey() crypto.PubKey { return sc.remPubKey } @@ -258,8 +258,8 @@ func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) { return hash32(append(loPubKey[:], hiPubKey[:]...)) } -func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKeyEd25519) (signature crypto.SignatureEd25519) { - signature = locPrivKey.Sign(challenge[:]).Unwrap().(crypto.SignatureEd25519) +func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature crypto.Signature) { + signature = locPrivKey.Sign(challenge[:]) return } @@ -268,7 +268,7 @@ type authSigMessage struct { Sig crypto.Signature } -func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKeyEd25519, signature crypto.SignatureEd25519) (*authSigMessage, error) { +func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature crypto.Signature) (*authSigMessage, error) { var recvMsg authSigMessage var err1, err2 error diff --git a/p2p/secret_connection_test.go b/p2p/conn/secret_connection_test.go similarity index 92% rename from p2p/secret_connection_test.go rename to p2p/conn/secret_connection_test.go index 8b58fb417..4cf715dd2 100644 --- a/p2p/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -1,11 +1,10 @@ -package p2p +package conn import ( - "bytes" "io" "testing" - "github.com/tendermint/go-crypto" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" ) @@ -32,10 +31,10 @@ func makeDummyConnPair() (fooConn, barConn dummyConn) { func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) { fooConn, barConn := makeDummyConnPair() - fooPrvKey := crypto.GenPrivKeyEd25519() - fooPubKey := fooPrvKey.PubKey().Unwrap().(crypto.PubKeyEd25519) - barPrvKey := crypto.GenPrivKeyEd25519() - barPubKey := barPrvKey.PubKey().Unwrap().(crypto.PubKeyEd25519) + fooPrvKey := crypto.GenPrivKeyEd25519().Wrap() + fooPubKey := fooPrvKey.PubKey() + barPrvKey := crypto.GenPrivKeyEd25519().Wrap() + barPubKey := barPrvKey.PubKey() cmn.Parallel( func() { @@ -46,7 +45,7 @@ func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection return } remotePubBytes := fooSecConn.RemotePubKey() - if !bytes.Equal(remotePubBytes[:], barPubKey[:]) { + if !remotePubBytes.Equals(barPubKey) { tb.Errorf("Unexpected fooSecConn.RemotePubKey. Expected %v, got %v", barPubKey, fooSecConn.RemotePubKey()) } @@ -59,7 +58,7 @@ func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection return } remotePubBytes := barSecConn.RemotePubKey() - if !bytes.Equal(remotePubBytes[:], fooPubKey[:]) { + if !remotePubBytes.Equals(fooPubKey) { tb.Errorf("Unexpected barSecConn.RemotePubKey. Expected %v, got %v", fooPubKey, barSecConn.RemotePubKey()) } @@ -93,7 +92,7 @@ func TestSecretConnectionReadWrite(t *testing.T) { genNodeRunner := func(nodeConn dummyConn, nodeWrites []string, nodeReads *[]string) func() { return func() { // Node handskae - nodePrvKey := crypto.GenPrivKeyEd25519() + nodePrvKey := crypto.GenPrivKeyEd25519().Wrap() nodeSecretConn, err := MakeSecretConnection(nodeConn, nodePrvKey) if err != nil { t.Errorf("Failed to establish SecretConnection for node: %v", err) diff --git a/p2p/errors.go b/p2p/errors.go new file mode 100644 index 000000000..cb6a7051a --- /dev/null +++ b/p2p/errors.go @@ -0,0 +1,20 @@ +package p2p + +import ( + "errors" + "fmt" +) + +var ( + ErrSwitchDuplicatePeer = errors.New("Duplicate peer") + ErrSwitchConnectToSelf = errors.New("Connect to self") +) + +type ErrSwitchAuthenticationFailure struct { + Dialed *NetAddress + Got ID +} + +func (e ErrSwitchAuthenticationFailure) Error() string { + return fmt.Sprintf("Failed to authenticate peer. Dialed %v, but got peer with ID %s", e.Dialed, e.Got) +} diff --git a/p2p/key.go b/p2p/key.go new file mode 100644 index 000000000..ea0f0b071 --- /dev/null +++ b/p2p/key.go @@ -0,0 +1,113 @@ +package p2p + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" +) + +// ID is a hex-encoded crypto.Address +type ID string + +// IDByteLength is the length of a crypto.Address. Currently only 20. +// TODO: support other length addresses ? +const IDByteLength = 20 + +//------------------------------------------------------------------------------ +// Persistent peer ID +// TODO: encrypt on disk + +// NodeKey is the persistent peer key. +// It contains the nodes private key for authentication. +type NodeKey struct { + PrivKey crypto.PrivKey `json:"priv_key"` // our priv key +} + +// ID returns the peer's canonical ID - the hash of its public key. +func (nodeKey *NodeKey) ID() ID { + return PubKeyToID(nodeKey.PubKey()) +} + +// PubKey returns the peer's PubKey +func (nodeKey *NodeKey) PubKey() crypto.PubKey { + return nodeKey.PrivKey.PubKey() +} + +// PubKeyToID returns the ID corresponding to the given PubKey. +// It's the hex-encoding of the pubKey.Address(). +func PubKeyToID(pubKey crypto.PubKey) ID { + return ID(hex.EncodeToString(pubKey.Address())) +} + +// LoadOrGenNodeKey attempts to load the NodeKey from the given filePath. +// If the file does not exist, it generates and saves a new NodeKey. +func LoadOrGenNodeKey(filePath string) (*NodeKey, error) { + if cmn.FileExists(filePath) { + nodeKey, err := loadNodeKey(filePath) + if err != nil { + return nil, err + } + return nodeKey, nil + } else { + return genNodeKey(filePath) + } +} + +func loadNodeKey(filePath string) (*NodeKey, error) { + jsonBytes, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + nodeKey := new(NodeKey) + err = json.Unmarshal(jsonBytes, nodeKey) + if err != nil { + return nil, fmt.Errorf("Error reading NodeKey from %v: %v\n", filePath, err) + } + return nodeKey, nil +} + +func genNodeKey(filePath string) (*NodeKey, error) { + privKey := crypto.GenPrivKeyEd25519().Wrap() + nodeKey := &NodeKey{ + PrivKey: privKey, + } + + jsonBytes, err := json.Marshal(nodeKey) + if err != nil { + return nil, err + } + err = ioutil.WriteFile(filePath, jsonBytes, 0600) + if err != nil { + return nil, err + } + return nodeKey, nil +} + +//------------------------------------------------------------------------------ + +// MakePoWTarget returns the big-endian encoding of 2^(targetBits - difficulty) - 1. +// It can be used as a Proof of Work target. +// NOTE: targetBits must be a multiple of 8 and difficulty must be less than targetBits. +func MakePoWTarget(difficulty, targetBits uint) []byte { + if targetBits%8 != 0 { + panic(fmt.Sprintf("targetBits (%d) not a multiple of 8", targetBits)) + } + if difficulty >= targetBits { + panic(fmt.Sprintf("difficulty (%d) >= targetBits (%d)", difficulty, targetBits)) + } + targetBytes := targetBits / 8 + zeroPrefixLen := (int(difficulty) / 8) + prefix := bytes.Repeat([]byte{0}, zeroPrefixLen) + mod := (difficulty % 8) + if mod > 0 { + nonZeroPrefix := byte(1<<(8-mod) - 1) + prefix = append(prefix, nonZeroPrefix) + } + tailLen := int(targetBytes) - len(prefix) + return append(prefix, bytes.Repeat([]byte{0xFF}, tailLen)...) +} diff --git a/p2p/key_test.go b/p2p/key_test.go new file mode 100644 index 000000000..c2e1f3e0e --- /dev/null +++ b/p2p/key_test.go @@ -0,0 +1,50 @@ +package p2p + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tmlibs/common" +) + +func TestLoadOrGenNodeKey(t *testing.T) { + filePath := filepath.Join(os.TempDir(), cmn.RandStr(12)+"_peer_id.json") + + nodeKey, err := LoadOrGenNodeKey(filePath) + assert.Nil(t, err) + + nodeKey2, err := LoadOrGenNodeKey(filePath) + assert.Nil(t, err) + + assert.Equal(t, nodeKey, nodeKey2) +} + +//---------------------------------------------------------- + +func padBytes(bz []byte, targetBytes int) []byte { + return append(bz, bytes.Repeat([]byte{0xFF}, targetBytes-len(bz))...) +} + +func TestPoWTarget(t *testing.T) { + + targetBytes := 20 + cases := []struct { + difficulty uint + target []byte + }{ + {0, padBytes([]byte{}, targetBytes)}, + {1, padBytes([]byte{127}, targetBytes)}, + {8, padBytes([]byte{0}, targetBytes)}, + {9, padBytes([]byte{0, 127}, targetBytes)}, + {10, padBytes([]byte{0, 63}, targetBytes)}, + {16, padBytes([]byte{0, 0}, targetBytes)}, + {17, padBytes([]byte{0, 0, 127}, targetBytes)}, + } + + for _, c := range cases { + assert.Equal(t, MakePoWTarget(c.difficulty, 20*8), c.target) + } +} diff --git a/p2p/netaddress.go b/p2p/netaddress.go index 41c2cc976..333d16e5d 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -5,6 +5,7 @@ package p2p import ( + "encoding/hex" "flag" "fmt" "net" @@ -12,41 +13,69 @@ import ( "strings" "time" + "github.com/pkg/errors" cmn "github.com/tendermint/tmlibs/common" ) // NetAddress defines information about a peer on the network -// including its IP address, and port. +// including its ID, IP address, and port. type NetAddress struct { + ID ID IP net.IP Port uint16 str string } +// IDAddressString returns id@hostPort. +func IDAddressString(id ID, hostPort string) string { + return fmt.Sprintf("%s@%s", id, hostPort) +} + // 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. // TODO: socks proxies? -func NewNetAddress(addr net.Addr) *NetAddress { +func NewNetAddress(id ID, addr net.Addr) *NetAddress { tcpAddr, ok := addr.(*net.TCPAddr) if !ok { if flag.Lookup("test.v") == nil { // normal run cmn.PanicSanity(cmn.Fmt("Only TCPAddrs are supported. Got: %v", addr)) } else { // in testing - return NewNetAddressIPPort(net.IP("0.0.0.0"), 0) + netAddr := NewNetAddressIPPort(net.IP("0.0.0.0"), 0) + netAddr.ID = id + return netAddr } } ip := tcpAddr.IP port := uint16(tcpAddr.Port) - return NewNetAddressIPPort(ip, port) + netAddr := NewNetAddressIPPort(ip, port) + netAddr.ID = id + return netAddr } // NewNetAddressString returns a new NetAddress using the provided -// address in the form of "IP:Port". Also resolves the host if host -// is not an IP. +// address in the form of "ID@IP:Port", where the ID is optional. +// Also resolves the host if host is not an IP. func NewNetAddressString(addr string) (*NetAddress, error) { - host, portStr, err := net.SplitHostPort(removeProtocolIfDefined(addr)) + addr = removeProtocolIfDefined(addr) + + var id ID + spl := strings.Split(addr, "@") + if len(spl) == 2 { + idStr := spl[0] + idBytes, err := hex.DecodeString(idStr) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("Address (%s) contains invalid ID", addr)) + } + if len(idBytes) != IDByteLength { + return nil, fmt.Errorf("Address (%s) contains ID of invalid length (%d). Should be %d hex-encoded bytes", + addr, len(idBytes), IDByteLength) + } + id, addr = ID(idStr), spl[1] + } + + host, portStr, err := net.SplitHostPort(addr) if err != nil { return nil, err } @@ -68,6 +97,7 @@ func NewNetAddressString(addr string) (*NetAddress, error) { } na := NewNetAddressIPPort(ip, uint16(port)) + na.ID = id return na, nil } @@ -93,46 +123,54 @@ func NewNetAddressIPPort(ip net.IP, port uint16) *NetAddress { na := &NetAddress{ IP: ip, Port: port, - str: net.JoinHostPort( - ip.String(), - strconv.FormatUint(uint64(port), 10), - ), } return na } -// Equals reports whether na and other are the same addresses. +// Equals reports whether na and other are the same addresses, +// including their ID, IP, and Port. func (na *NetAddress) Equals(other interface{}) bool { if o, ok := other.(*NetAddress); ok { return na.String() == o.String() } - return false } -func (na *NetAddress) Less(other interface{}) bool { +// Same returns true is na has the same non-empty ID or DialString as other. +func (na *NetAddress) Same(other interface{}) bool { if o, ok := other.(*NetAddress); ok { - return na.String() < o.String() + if na.DialString() == o.DialString() { + return true + } + if na.ID != "" && na.ID == o.ID { + return true + } } - - cmn.PanicSanity("Cannot compare unequal types") return false } -// String representation. +// String representation: @: func (na *NetAddress) String() string { if na.str == "" { - na.str = net.JoinHostPort( - na.IP.String(), - strconv.FormatUint(uint64(na.Port), 10), - ) + addrStr := na.DialString() + if na.ID != "" { + addrStr = IDAddressString(na.ID, addrStr) + } + na.str = addrStr } return na.str } +func (na *NetAddress) DialString() string { + return net.JoinHostPort( + na.IP.String(), + strconv.FormatUint(uint64(na.Port), 10), + ) +} + // Dial calls net.Dial on the address. func (na *NetAddress) Dial() (net.Conn, error) { - conn, err := net.Dial("tcp", na.String()) + conn, err := net.Dial("tcp", na.DialString()) if err != nil { return nil, err } @@ -141,7 +179,7 @@ func (na *NetAddress) Dial() (net.Conn, error) { // DialTimeout calls net.DialTimeout on the address. func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) { - conn, err := net.DialTimeout("tcp", na.String(), timeout) + conn, err := net.DialTimeout("tcp", na.DialString(), timeout) if err != nil { return nil, err } diff --git a/p2p/netaddress_test.go b/p2p/netaddress_test.go index 137be090c..6c1930a2f 100644 --- a/p2p/netaddress_test.go +++ b/p2p/netaddress_test.go @@ -13,12 +13,12 @@ func TestNewNetAddress(t *testing.T) { tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") require.Nil(err) - addr := NewNetAddress(tcpAddr) + addr := NewNetAddress("", tcpAddr) assert.Equal("127.0.0.1:8080", addr.String()) assert.NotPanics(func() { - NewNetAddress(&net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) + NewNetAddress("", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) }, "Calling NewNetAddress with UDPAddr should not panic in testing") } @@ -38,6 +38,23 @@ func TestNewNetAddressString(t *testing.T) { {"notahost:8080", "", false}, {"8082", "", false}, {"127.0.0:8080000", "", false}, + + {"deadbeef@127.0.0.1:8080", "", false}, + {"this-isnot-hex@127.0.0.1:8080", "", false}, + {"xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, + {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, + + {"tcp://deadbeef@127.0.0.1:8080", "", false}, + {"tcp://this-isnot-hex@127.0.0.1:8080", "", false}, + {"tcp://xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, + {"tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, + + {"tcp://@127.0.0.1:8080", "", false}, + {"tcp://@", "", false}, + {"", "", false}, + {"@", "", false}, + {" @", "", false}, + {" @ ", "", false}, } for _, tc := range testCases { diff --git a/p2p/node_info.go b/p2p/node_info.go new file mode 100644 index 000000000..a5bb9da53 --- /dev/null +++ b/p2p/node_info.go @@ -0,0 +1,143 @@ +package p2p + +import ( + "fmt" + "strings" + + crypto "github.com/tendermint/go-crypto" +) + +const ( + maxNodeInfoSize = 10240 // 10Kb + maxNumChannels = 16 // plenty of room for upgrades, for now +) + +func MaxNodeInfoSize() int { + return maxNodeInfoSize +} + +// NodeInfo is the basic node information exchanged +// between two peers during the Tendermint P2P handshake. +type NodeInfo struct { + // Authenticate + PubKey crypto.PubKey `json:"pub_key"` // authenticated pubkey + ListenAddr string `json:"listen_addr"` // accepting incoming + + // Check compatibility + Network string `json:"network"` // network/chain ID + Version string `json:"version"` // major.minor.revision + Channels []byte `json:"channels"` // channels this node knows about + + // Sanitize + Moniker string `json:"moniker"` // arbitrary moniker + Other []string `json:"other"` // other application specific data +} + +// Validate checks the self-reported NodeInfo is safe. +// It returns an error if the info.PubKey doesn't match the given pubKey, +// or if there are too many Channels or any duplicate Channels. +// TODO: constraints for Moniker/Other? Or is that for the UI ? +func (info NodeInfo) Validate(pubKey crypto.PubKey) error { + if !info.PubKey.Equals(pubKey) { + return fmt.Errorf("info.PubKey (%v) doesn't match peer.PubKey (%v)", + info.PubKey, pubKey) + } + + if len(info.Channels) > maxNumChannels { + return fmt.Errorf("info.Channels is too long (%v). Max is %v", len(info.Channels), maxNumChannels) + } + + channels := make(map[byte]struct{}) + for _, ch := range info.Channels { + _, ok := channels[ch] + if ok { + return fmt.Errorf("info.Channels contains duplicate channel id %v", ch) + } + channels[ch] = struct{}{} + } + return nil +} + +// CompatibleWith checks if two NodeInfo are compatible with eachother. +// CONTRACT: two nodes are compatible if the major/minor versions match and network match +// and they have at least one channel in common. +func (info NodeInfo) CompatibleWith(other NodeInfo) error { + iMajor, iMinor, _, iErr := splitVersion(info.Version) + oMajor, oMinor, _, oErr := splitVersion(other.Version) + + // if our own version number is not formatted right, we messed up + if iErr != nil { + return iErr + } + + // version number must be formatted correctly ("x.x.x") + if oErr != nil { + return oErr + } + + // major version must match + if iMajor != oMajor { + return fmt.Errorf("Peer is on a different major version. Got %v, expected %v", oMajor, iMajor) + } + + // minor version must match + if iMinor != oMinor { + return fmt.Errorf("Peer is on a different minor version. Got %v, expected %v", oMinor, iMinor) + } + + // nodes must be on the same network + if info.Network != other.Network { + return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network, info.Network) + } + + // if we have no channels, we're just testing + if len(info.Channels) == 0 { + return nil + } + + // for each of our channels, check if they have it + found := false +OUTER_LOOP: + for _, ch1 := range info.Channels { + for _, ch2 := range other.Channels { + if ch1 == ch2 { + found = true + break OUTER_LOOP // only need one + } + } + } + if !found { + return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", info.Channels, other.Channels) + } + return nil +} + +func (info NodeInfo) ID() ID { + return PubKeyToID(info.PubKey) +} + +// NetAddress returns a NetAddress derived from the NodeInfo - +// it includes the authenticated peer ID and the self-reported +// ListenAddr. Note that the ListenAddr is not authenticated and +// may not match that address actually dialed if its an outbound peer. +func (info NodeInfo) NetAddress() *NetAddress { + id := PubKeyToID(info.PubKey) + addr := info.ListenAddr + netAddr, err := NewNetAddressString(IDAddressString(id, addr)) + if err != nil { + panic(err) // everything should be well formed by now + } + return netAddr +} + +func (info NodeInfo) String() string { + return fmt.Sprintf("NodeInfo{pk: %v, moniker: %v, network: %v [listen %v], version: %v (%v)}", info.PubKey, info.Moniker, info.Network, info.ListenAddr, info.Version, info.Other) +} + +func splitVersion(version string) (string, string, string, error) { + spl := strings.Split(version, ".") + if len(spl) != 3 { + return "", "", "", fmt.Errorf("Invalid version format %v", version) + } + return spl[0], spl[1], spl[2], nil +} diff --git a/p2p/peer.go b/p2p/peer.go index cc7f4927a..67ce411cd 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -11,17 +11,19 @@ import ( wire "github.com/tendermint/go-wire" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" + + tmconn "github.com/tendermint/tendermint/p2p/conn" ) // Peer is an interface representing a peer connected on a reactor. type Peer interface { cmn.Service - Key() string - IsOutbound() bool - IsPersistent() bool - NodeInfo() *NodeInfo - Status() ConnectionStatus + ID() ID // peer's cryptographic ID + IsOutbound() bool // did we dial the peer + IsPersistent() bool // do we redial this peer when we disconnect + NodeInfo() NodeInfo // peer's info + Status() tmconn.ConnectionStatus Send(byte, interface{}) bool TrySend(byte, interface{}) bool @@ -30,9 +32,9 @@ type Peer interface { Get(string) interface{} } -// Peer could be marked as persistent, in which case you can use -// Redial function to reconnect. Note that inbound peers can't be -// made persistent. They should be made persistent on the other end. +//---------------------------------------------------------- + +// peer implements Peer. // // Before using a peer, you will need to perform a handshake on connection. type peer struct { @@ -40,13 +42,14 @@ type peer struct { outbound bool - conn net.Conn // source connection - mconn *MConnection // multiplex connection + conn net.Conn // source connection + mconn *tmconn.MConnection // multiplex connection persistent bool config *PeerConfig - nodeInfo *NodeInfo + nodeInfo NodeInfo // peer's node info + channels []byte // channels the peer knows about Data *cmn.CMap // User data. } @@ -58,7 +61,7 @@ type PeerConfig struct { HandshakeTimeout time.Duration `mapstructure:"handshake_timeout"` DialTimeout time.Duration `mapstructure:"dial_timeout"` - MConfig *MConnConfig `mapstructure:"connection"` + MConfig *tmconn.MConnConfig `mapstructure:"connection"` Fuzz bool `mapstructure:"fuzz"` // fuzz connection (for testing) FuzzConfig *FuzzConnConfig `mapstructure:"fuzz_config"` @@ -70,14 +73,14 @@ func DefaultPeerConfig() *PeerConfig { AuthEnc: true, HandshakeTimeout: 20, // * time.Second, DialTimeout: 3, // * time.Second, - MConfig: DefaultMConnConfig(), + MConfig: tmconn.DefaultMConnConfig(), Fuzz: false, FuzzConfig: DefaultFuzzConnConfig(), } } -func newOutboundPeer(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*peer, error) { +func newOutboundPeer(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, + onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig, persistent bool) (*peer, error) { conn, err := dial(addr, config) if err != nil { @@ -91,17 +94,21 @@ func newOutboundPeer(addr *NetAddress, reactorsByCh map[byte]Reactor, chDescs [] } return nil, err } + peer.persistent = persistent + return peer, nil } -func newInboundPeer(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*peer, error) { +func newInboundPeer(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, + onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig) (*peer, error) { + + // TODO: issue PoW challenge return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config) } -func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, - onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*peer, error) { +func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, + onPeerError func(Peer, interface{}), ourNodePrivKey crypto.PrivKey, config *PeerConfig) (*peer, error) { conn := rawConn @@ -118,13 +125,13 @@ func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[ } var err error - conn, err = MakeSecretConnection(conn, ourNodePrivKey) + conn, err = tmconn.MakeSecretConnection(conn, ourNodePrivKey) if err != nil { return nil, errors.Wrap(err, "Error creating peer") } } - // Key and NodeInfo are set after Handshake + // NodeInfo is set after Handshake p := &peer{ outbound: outbound, conn: conn, @@ -139,23 +146,41 @@ func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[ return p, nil } +//--------------------------------------------------- +// Implements cmn.Service + +// SetLogger implements BaseService. func (p *peer) SetLogger(l log.Logger) { p.Logger = l p.mconn.SetLogger(l) } -// CloseConn should be used when the peer was created, but never started. -func (p *peer) CloseConn() { - p.conn.Close() // nolint: errcheck +// OnStart implements BaseService. +func (p *peer) OnStart() error { + if err := p.BaseService.OnStart(); err != nil { + return err + } + err := p.mconn.Start() + return err } -// makePersistent marks the peer as persistent. -func (p *peer) makePersistent() { - if !p.outbound { - panic("inbound peers can't be made persistent") - } +// OnStop implements BaseService. +func (p *peer) OnStop() { + p.BaseService.OnStop() + p.mconn.Stop() // stop everything and close the conn +} + +//--------------------------------------------------- +// Implements Peer - p.persistent = true +// ID returns the peer's ID - the hex encoded hash of its pubkey. +func (p *peer) ID() ID { + return PubKeyToID(p.PubKey()) +} + +// IsOutbound returns true if the connection is outbound, false otherwise. +func (p *peer) IsOutbound() bool { + return p.outbound } // IsPersistent returns true if the peer is persitent, false otherwise. @@ -163,25 +188,92 @@ func (p *peer) IsPersistent() bool { return p.persistent } -// HandshakeTimeout performs a handshake between a given node and the peer. +// NodeInfo returns a copy of the peer's NodeInfo. +func (p *peer) NodeInfo() NodeInfo { + return p.nodeInfo +} + +// Status returns the peer's ConnectionStatus. +func (p *peer) Status() tmconn.ConnectionStatus { + return p.mconn.Status() +} + +// Send msg to the channel identified by chID byte. Returns false if the send +// queue is full after timeout, specified by MConnection. +func (p *peer) Send(chID byte, msg interface{}) bool { + if !p.IsRunning() { + // see Switch#Broadcast, where we fetch the list of peers and loop over + // them - while we're looping, one peer may be removed and stopped. + return false + } else if !p.hasChannel(chID) { + return false + } + return p.mconn.Send(chID, msg) +} + +// TrySend msg to the channel identified by chID byte. Immediately returns +// false if the send queue is full. +func (p *peer) TrySend(chID byte, msg interface{}) bool { + if !p.IsRunning() { + return false + } else if !p.hasChannel(chID) { + return false + } + return p.mconn.TrySend(chID, msg) +} + +// Get the data for a given key. +func (p *peer) Get(key string) interface{} { + return p.Data.Get(key) +} + +// Set sets the data for the given key. +func (p *peer) Set(key string, data interface{}) { + p.Data.Set(key, data) +} + +// hasChannel returns true if the peer reported +// knowing about the given chID. +func (p *peer) hasChannel(chID byte) bool { + for _, ch := range p.channels { + if ch == chID { + return true + } + } + // NOTE: probably will want to remove this + // but could be helpful while the feature is new + p.Logger.Debug("Unknown channel for peer", "channel", chID, "channels", p.channels) + return false +} + +//--------------------------------------------------- +// methods used by the Switch + +// CloseConn should be called by the Switch if the peer was created but never started. +func (p *peer) CloseConn() { + p.conn.Close() // nolint: errcheck +} + +// HandshakeTimeout performs the Tendermint P2P handshake between a given node and the peer +// by exchanging their NodeInfo. It sets the received nodeInfo on the peer. // NOTE: blocking -func (p *peer) HandshakeTimeout(ourNodeInfo *NodeInfo, timeout time.Duration) error { +func (p *peer) HandshakeTimeout(ourNodeInfo NodeInfo, timeout time.Duration) error { // Set deadline for handshake so we don't block forever on conn.ReadFull if err := p.conn.SetDeadline(time.Now().Add(timeout)); err != nil { return errors.Wrap(err, "Error setting deadline") } - var peerNodeInfo = new(NodeInfo) + var peerNodeInfo NodeInfo var err1 error var err2 error cmn.Parallel( func() { var n int - wire.WriteBinary(ourNodeInfo, p.conn, &n, &err1) + wire.WriteBinary(&ourNodeInfo, p.conn, &n, &err1) }, func() { var n int - wire.ReadBinary(peerNodeInfo, p.conn, maxNodeInfoSize, &n, &err2) + wire.ReadBinary(&peerNodeInfo, p.conn, MaxNodeInfoSize(), &n, &err2) p.Logger.Info("Peer handshake", "peerNodeInfo", peerNodeInfo) }) if err1 != nil { @@ -191,84 +283,35 @@ func (p *peer) HandshakeTimeout(ourNodeInfo *NodeInfo, timeout time.Duration) er return errors.Wrap(err2, "Error during handshake/read") } - if p.config.AuthEnc { - // Check that the professed PubKey matches the sconn's. - if !peerNodeInfo.PubKey.Equals(p.PubKey().Wrap()) { - return fmt.Errorf("Ignoring connection with unmatching pubkey: %v vs %v", - peerNodeInfo.PubKey, p.PubKey()) - } - } - // Remove deadline if err := p.conn.SetDeadline(time.Time{}); err != nil { return errors.Wrap(err, "Error removing deadline") } - peerNodeInfo.RemoteAddr = p.Addr().String() - - p.nodeInfo = peerNodeInfo + p.setNodeInfo(peerNodeInfo) return nil } +func (p *peer) setNodeInfo(nodeInfo NodeInfo) { + p.nodeInfo = nodeInfo + // cache the channels so we dont copy nodeInfo + // every time we check hasChannel + p.channels = nodeInfo.Channels +} + // Addr returns peer's remote network address. func (p *peer) Addr() net.Addr { return p.conn.RemoteAddr() } // PubKey returns peer's public key. -func (p *peer) PubKey() crypto.PubKeyEd25519 { - if p.config.AuthEnc { - return p.conn.(*SecretConnection).RemotePubKey() - } - if p.NodeInfo() == nil { - panic("Attempt to get peer's PubKey before calling Handshake") +func (p *peer) PubKey() crypto.PubKey { + if !p.nodeInfo.PubKey.Empty() { + return p.nodeInfo.PubKey + } else if p.config.AuthEnc { + return p.conn.(*tmconn.SecretConnection).RemotePubKey() } - return p.PubKey() -} - -// OnStart implements BaseService. -func (p *peer) OnStart() error { - if err := p.BaseService.OnStart(); err != nil { - return err - } - err := p.mconn.Start() - return err -} - -// OnStop implements BaseService. -func (p *peer) OnStop() { - p.BaseService.OnStop() - p.mconn.Stop() -} - -// Connection returns underlying MConnection. -func (p *peer) Connection() *MConnection { - return p.mconn -} - -// IsOutbound returns true if the connection is outbound, false otherwise. -func (p *peer) IsOutbound() bool { - return p.outbound -} - -// Send msg to the channel identified by chID byte. Returns false if the send -// queue is full after timeout, specified by MConnection. -func (p *peer) Send(chID byte, msg interface{}) bool { - if !p.IsRunning() { - // see Switch#Broadcast, where we fetch the list of peers and loop over - // them - while we're looping, one peer may be removed and stopped. - return false - } - return p.mconn.Send(chID, msg) -} - -// TrySend msg to the channel identified by chID byte. Immediately returns -// false if the send queue is full. -func (p *peer) TrySend(chID byte, msg interface{}) bool { - if !p.IsRunning() { - return false - } - return p.mconn.TrySend(chID, msg) + panic("Attempt to get peer's PubKey before calling Handshake") } // CanSend returns true if the send queue is not full, false otherwise. @@ -282,45 +325,14 @@ func (p *peer) CanSend(chID byte) bool { // String representation. func (p *peer) String() string { if p.outbound { - return fmt.Sprintf("Peer{%v %v out}", p.mconn, p.Key()) + return fmt.Sprintf("Peer{%v %v out}", p.mconn, p.ID()) } - return fmt.Sprintf("Peer{%v %v in}", p.mconn, p.Key()) + return fmt.Sprintf("Peer{%v %v in}", p.mconn, p.ID()) } -// Equals reports whenever 2 peers are actually represent the same node. -func (p *peer) Equals(other Peer) bool { - return p.Key() == other.Key() -} - -// Get the data for a given key. -func (p *peer) Get(key string) interface{} { - return p.Data.Get(key) -} - -// Set sets the data for the given key. -func (p *peer) Set(key string, data interface{}) { - p.Data.Set(key, data) -} - -// Key returns the peer's id key. -func (p *peer) Key() string { - return p.nodeInfo.ListenAddr // XXX: should probably be PubKey.KeyString() -} - -// NodeInfo returns a copy of the peer's NodeInfo. -func (p *peer) NodeInfo() *NodeInfo { - if p.nodeInfo == nil { - return nil - } - n := *p.nodeInfo // copy - return &n -} - -// Status returns the peer's ConnectionStatus. -func (p *peer) Status() ConnectionStatus { - return p.mconn.Status() -} +//------------------------------------------------------------------ +// helper funcs func dial(addr *NetAddress, config *PeerConfig) (net.Conn, error) { conn, err := addr.DialTimeout(config.DialTimeout * time.Second) @@ -330,8 +342,8 @@ func dial(addr *NetAddress, config *PeerConfig) (net.Conn, error) { return conn, nil } -func createMConnection(conn net.Conn, p *peer, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, - onPeerError func(Peer, interface{}), config *MConnConfig) *MConnection { +func createMConnection(conn net.Conn, p *peer, reactorsByCh map[byte]Reactor, chDescs []*tmconn.ChannelDescriptor, + onPeerError func(Peer, interface{}), config *tmconn.MConnConfig) *tmconn.MConnection { onReceive := func(chID byte, msgBytes []byte) { reactor := reactorsByCh[chID] @@ -345,5 +357,5 @@ func createMConnection(conn net.Conn, p *peer, reactorsByCh map[byte]Reactor, ch onPeerError(p, r) } - return NewMConnectionWithConfig(conn, chDescs, onReceive, onError, config) + return tmconn.NewMConnectionWithConfig(conn, chDescs, onReceive, onError, config) } diff --git a/p2p/peer_set.go b/p2p/peer_set.go index c21748cf9..dc53174a1 100644 --- a/p2p/peer_set.go +++ b/p2p/peer_set.go @@ -6,8 +6,8 @@ import ( // IPeerSet has a (immutable) subset of the methods of PeerSet. type IPeerSet interface { - Has(key string) bool - Get(key string) Peer + Has(key ID) bool + Get(key ID) Peer List() []Peer Size() int } @@ -18,7 +18,7 @@ type IPeerSet interface { // Iteration over the peers is super fast and thread-safe. type PeerSet struct { mtx sync.Mutex - lookup map[string]*peerSetItem + lookup map[ID]*peerSetItem list []Peer } @@ -30,7 +30,7 @@ type peerSetItem struct { // NewPeerSet creates a new peerSet with a list of initial capacity of 256 items. func NewPeerSet() *PeerSet { return &PeerSet{ - lookup: make(map[string]*peerSetItem), + lookup: make(map[ID]*peerSetItem), list: make([]Peer, 0, 256), } } @@ -40,7 +40,7 @@ func NewPeerSet() *PeerSet { func (ps *PeerSet) Add(peer Peer) error { ps.mtx.Lock() defer ps.mtx.Unlock() - if ps.lookup[peer.Key()] != nil { + if ps.lookup[peer.ID()] != nil { return ErrSwitchDuplicatePeer } @@ -48,13 +48,13 @@ func (ps *PeerSet) Add(peer Peer) error { // Appending is safe even with other goroutines // iterating over the ps.list slice. ps.list = append(ps.list, peer) - ps.lookup[peer.Key()] = &peerSetItem{peer, index} + ps.lookup[peer.ID()] = &peerSetItem{peer, index} return nil } // Has returns true iff the PeerSet contains // the peer referred to by this peerKey. -func (ps *PeerSet) Has(peerKey string) bool { +func (ps *PeerSet) Has(peerKey ID) bool { ps.mtx.Lock() _, ok := ps.lookup[peerKey] ps.mtx.Unlock() @@ -62,7 +62,7 @@ func (ps *PeerSet) Has(peerKey string) bool { } // Get looks up a peer by the provided peerKey. -func (ps *PeerSet) Get(peerKey string) Peer { +func (ps *PeerSet) Get(peerKey ID) Peer { ps.mtx.Lock() defer ps.mtx.Unlock() item, ok := ps.lookup[peerKey] @@ -77,7 +77,7 @@ func (ps *PeerSet) Get(peerKey string) Peer { func (ps *PeerSet) Remove(peer Peer) { ps.mtx.Lock() defer ps.mtx.Unlock() - item := ps.lookup[peer.Key()] + item := ps.lookup[peer.ID()] if item == nil { return } @@ -90,18 +90,18 @@ func (ps *PeerSet) Remove(peer Peer) { // If it's the last peer, that's an easy special case. if index == len(ps.list)-1 { ps.list = newList - delete(ps.lookup, peer.Key()) + delete(ps.lookup, peer.ID()) return } // Replace the popped item with the last item in the old list. lastPeer := ps.list[len(ps.list)-1] - lastPeerKey := lastPeer.Key() + lastPeerKey := lastPeer.ID() lastPeerItem := ps.lookup[lastPeerKey] newList[index] = lastPeer lastPeerItem.index = index ps.list = newList - delete(ps.lookup, peer.Key()) + delete(ps.lookup, peer.ID()) } // Size returns the number of unique items in the peerSet. diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index a7f29315a..e906eb8e7 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -7,15 +7,16 @@ import ( "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" ) // Returns an empty dummy peer func randPeer() *peer { return &peer{ - nodeInfo: &NodeInfo{ - RemoteAddr: cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), + nodeInfo: NodeInfo{ ListenAddr: cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256), + PubKey: crypto.GenPrivKeyEd25519().Wrap().PubKey(), }, } } @@ -39,7 +40,7 @@ func TestPeerSetAddRemoveOne(t *testing.T) { peerSet.Remove(peerAtFront) wantSize := n - i - 1 for j := 0; j < 2; j++ { - assert.Equal(t, false, peerSet.Has(peerAtFront.Key()), "#%d Run #%d: failed to remove peer", i, j) + assert.Equal(t, false, peerSet.Has(peerAtFront.ID()), "#%d Run #%d: failed to remove peer", i, j) assert.Equal(t, wantSize, peerSet.Size(), "#%d Run #%d: failed to remove peer and decrement size", i, j) // Test the route of removing the now non-existent element peerSet.Remove(peerAtFront) @@ -58,7 +59,7 @@ func TestPeerSetAddRemoveOne(t *testing.T) { for i := n - 1; i >= 0; i-- { peerAtEnd := peerList[i] peerSet.Remove(peerAtEnd) - assert.Equal(t, false, peerSet.Has(peerAtEnd.Key()), "#%d: failed to remove item at end", i) + assert.Equal(t, false, peerSet.Has(peerAtEnd.ID()), "#%d: failed to remove item at end", i) assert.Equal(t, i, peerSet.Size(), "#%d: differing sizes after peerSet.Remove(atEndPeer)", i) } } @@ -82,7 +83,7 @@ func TestPeerSetAddRemoveMany(t *testing.T) { for i, peer := range peers { peerSet.Remove(peer) - if peerSet.Has(peer.Key()) { + if peerSet.Has(peer.ID()) { t.Errorf("Failed to remove peer") } if peerSet.Size() != len(peers)-i-1 { @@ -129,7 +130,7 @@ func TestPeerSetGet(t *testing.T) { t.Parallel() peerSet := NewPeerSet() peer := randPeer() - assert.Nil(t, peerSet.Get(peer.Key()), "expecting a nil lookup, before .Add") + assert.Nil(t, peerSet.Get(peer.ID()), "expecting a nil lookup, before .Add") if err := peerSet.Add(peer); err != nil { t.Fatalf("Failed to add new peer: %v", err) @@ -142,7 +143,7 @@ func TestPeerSetGet(t *testing.T) { wg.Add(1) go func(i int) { defer wg.Done() - got, want := peerSet.Get(peer.Key()), peer + got, want := peerSet.Get(peer.ID()), peer assert.Equal(t, got, want, "#%d: got=%v want=%v", i, got, want) }(i) } diff --git a/p2p/peer_test.go b/p2p/peer_test.go index b53b0bb12..a2f5ed05f 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -10,13 +10,16 @@ import ( "github.com/stretchr/testify/require" crypto "github.com/tendermint/go-crypto" + tmconn "github.com/tendermint/tendermint/p2p/conn" ) +const testCh = 0x01 + func TestPeerBasic(t *testing.T) { assert, require := assert.New(t), require.New(t) // simulate remote peer - rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519().Wrap(), Config: DefaultPeerConfig()} rp.Start() defer rp.Stop() @@ -30,7 +33,7 @@ func TestPeerBasic(t *testing.T) { assert.True(p.IsRunning()) assert.True(p.IsOutbound()) assert.False(p.IsPersistent()) - p.makePersistent() + p.persistent = true assert.True(p.IsPersistent()) assert.Equal(rp.Addr().String(), p.Addr().String()) assert.Equal(rp.PubKey(), p.PubKey()) @@ -43,7 +46,7 @@ func TestPeerWithoutAuthEnc(t *testing.T) { config.AuthEnc = false // simulate remote peer - rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: config} + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519().Wrap(), Config: config} rp.Start() defer rp.Stop() @@ -64,7 +67,7 @@ func TestPeerSend(t *testing.T) { config.AuthEnc = false // simulate remote peer - rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: config} + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519().Wrap(), Config: config} rp.Start() defer rp.Stop() @@ -76,25 +79,26 @@ func TestPeerSend(t *testing.T) { defer p.Stop() - assert.True(p.CanSend(0x01)) - assert.True(p.Send(0x01, "Asylum")) + assert.True(p.CanSend(testCh)) + assert.True(p.Send(testCh, "Asylum")) } func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) (*peer, error) { - chDescs := []*ChannelDescriptor{ - {ID: 0x01, Priority: 1}, + chDescs := []*tmconn.ChannelDescriptor{ + {ID: testCh, Priority: 1}, } - reactorsByCh := map[byte]Reactor{0x01: NewTestReactor(chDescs, true)} - pk := crypto.GenPrivKeyEd25519() - p, err := newOutboundPeer(addr, reactorsByCh, chDescs, func(p Peer, r interface{}) {}, pk, config) + reactorsByCh := map[byte]Reactor{testCh: NewTestReactor(chDescs, true)} + pk := crypto.GenPrivKeyEd25519().Wrap() + p, err := newOutboundPeer(addr, reactorsByCh, chDescs, func(p Peer, r interface{}) {}, pk, config, false) if err != nil { return nil, err } - err = p.HandshakeTimeout(&NodeInfo{ - PubKey: pk.PubKey().Unwrap().(crypto.PubKeyEd25519), - Moniker: "host_peer", - Network: "testing", - Version: "123.123.123", + err = p.HandshakeTimeout(NodeInfo{ + PubKey: pk.PubKey(), + Moniker: "host_peer", + Network: "testing", + Version: "123.123.123", + Channels: []byte{testCh}, }, 1*time.Second) if err != nil { return nil, err @@ -103,7 +107,7 @@ func createOutboundPeerAndPerformHandshake(addr *NetAddress, config *PeerConfig) } type remotePeer struct { - PrivKey crypto.PrivKeyEd25519 + PrivKey crypto.PrivKey Config *PeerConfig addr *NetAddress quit chan struct{} @@ -113,8 +117,8 @@ func (p *remotePeer) Addr() *NetAddress { return p.addr } -func (p *remotePeer) PubKey() crypto.PubKeyEd25519 { - return p.PrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) +func (p *remotePeer) PubKey() crypto.PubKey { + return p.PrivKey.PubKey() } func (p *remotePeer) Start() { @@ -122,7 +126,7 @@ func (p *remotePeer) Start() { if e != nil { golog.Fatalf("net.Listen tcp :0: %+v", e) } - p.addr = NewNetAddress(l.Addr()) + p.addr = NewNetAddress("", l.Addr()) p.quit = make(chan struct{}) go p.accept(l) } @@ -137,15 +141,17 @@ func (p *remotePeer) accept(l net.Listener) { if err != nil { golog.Fatalf("Failed to accept conn: %+v", err) } - peer, err := newInboundPeer(conn, make(map[byte]Reactor), make([]*ChannelDescriptor, 0), func(p Peer, r interface{}) {}, p.PrivKey, p.Config) + peer, err := newInboundPeer(conn, make(map[byte]Reactor), make([]*tmconn.ChannelDescriptor, 0), func(p Peer, r interface{}) {}, p.PrivKey, p.Config) if err != nil { golog.Fatalf("Failed to create a peer: %+v", err) } - err = peer.HandshakeTimeout(&NodeInfo{ - PubKey: p.PrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519), - Moniker: "remote_peer", - Network: "testing", - Version: "123.123.123", + err = peer.HandshakeTimeout(NodeInfo{ + PubKey: p.PrivKey.PubKey(), + Moniker: "remote_peer", + Network: "testing", + Version: "123.123.123", + ListenAddr: l.Addr().String(), + Channels: []byte{testCh}, }, 1*time.Second) if err != nil { golog.Fatalf("Failed to perform handshake: %+v", err) diff --git a/p2p/addrbook.go b/p2p/pex/addrbook.go similarity index 57% rename from p2p/addrbook.go rename to p2p/pex/addrbook.go index 8f924d129..95ad70fe7 100644 --- a/p2p/addrbook.go +++ b/p2p/pex/addrbook.go @@ -2,94 +2,80 @@ // Originally Copyright (c) 2013-2014 Conformal Systems LLC. // https://github.com/conformal/btcd/blob/master/LICENSE -package p2p +package pex import ( + "crypto/sha256" "encoding/binary" - "encoding/json" "fmt" "math" "math/rand" "net" - "os" "sync" "time" crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/p2p" cmn "github.com/tendermint/tmlibs/common" ) const ( - // addresses under which the address manager will claim to need more addresses. - needAddressThreshold = 1000 - - // interval used to dump the address cache to disk for future use. - dumpAddressInterval = time.Minute * 2 - - // max addresses in each old address bucket. - oldBucketSize = 64 - - // buckets we split old addresses over. - oldBucketCount = 64 - - // max addresses in each new address bucket. - newBucketSize = 64 - - // buckets that we spread new addresses over. - newBucketCount = 256 - - // old buckets over which an address group will be spread. - oldBucketsPerGroup = 4 + bucketTypeNew = 0x01 + bucketTypeOld = 0x02 +) - // new buckets over which a source address group will be spread. - newBucketsPerGroup = 32 +// AddrBook is an address book used for tracking peers +// so we can gossip about them to others and select +// peers to dial. +// TODO: break this up? +type AddrBook interface { + cmn.Service - // buckets a frequently seen new address may end up in. - maxNewBucketsPerAddress = 4 + // Add our own addresses so we don't later add ourselves + AddOurAddress(*p2p.NetAddress) - // days before which we assume an address has vanished - // if we have not seen it announced in that long. - numMissingDays = 30 + // Add and remove an address + AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error + RemoveAddress(addr *p2p.NetAddress) - // tries without a single success before we assume an address is bad. - numRetries = 3 + // Do we need more peers? + NeedMoreAddrs() bool - // max failures we will accept without a success before considering an address bad. - maxFailures = 10 + // Pick an address to dial + PickAddress(newBias int) *p2p.NetAddress - // days since the last success before we will consider evicting an address. - minBadDays = 7 + // Mark address + MarkGood(*p2p.NetAddress) + MarkAttempt(*p2p.NetAddress) + MarkBad(*p2p.NetAddress) - // % of total addresses known returned by GetSelection. - getSelectionPercent = 23 + // Send a selection of addresses to peers + GetSelection() []*p2p.NetAddress - // min addresses that must be returned by GetSelection. Useful for bootstrapping. - minGetSelection = 32 + // TODO: remove + ListOfKnownAddresses() []*knownAddress - // max addresses returned by GetSelection - // NOTE: this must match "maxPexMessageSize" - maxGetSelection = 250 -) + // Persist to disk + Save() +} -const ( - bucketTypeNew = 0x01 - bucketTypeOld = 0x02 -) +var _ AddrBook = (*addrBook)(nil) -// AddrBook - concurrency safe peer address manager. -type AddrBook struct { +// addrBook - concurrency safe peer address manager. +// Implements AddrBook. +type addrBook struct { cmn.BaseService // immutable after creation filePath string routabilityStrict bool - key string + key string // random prefix for bucket placement // accessed concurrently mtx sync.Mutex rand *rand.Rand - ourAddrs map[string]*NetAddress - addrLookup map[string]*knownAddress // new & old + ourAddrs map[string]*p2p.NetAddress + addrLookup map[p2p.ID]*knownAddress // new & old bucketsOld []map[string]*knownAddress bucketsNew []map[string]*knownAddress nOld int @@ -100,11 +86,11 @@ type AddrBook struct { // NewAddrBook creates a new address book. // Use Start to begin processing asynchronous address updates. -func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook { - am := &AddrBook{ - rand: rand.New(rand.NewSource(time.Now().UnixNano())), - ourAddrs: make(map[string]*NetAddress), - addrLookup: make(map[string]*knownAddress), +func NewAddrBook(filePath string, routabilityStrict bool) *addrBook { + am := &addrBook{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), // TODO: seed from outside + ourAddrs: make(map[string]*p2p.NetAddress), + addrLookup: make(map[p2p.ID]*knownAddress), filePath: filePath, routabilityStrict: routabilityStrict, } @@ -113,8 +99,9 @@ func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook { return am } +// Initialize the buckets. // When modifying this, don't forget to update loadFromFile() -func (a *AddrBook) init() { +func (a *addrBook) init() { a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits // New addr buckets a.bucketsNew = make([]map[string]*knownAddress, newBucketCount) @@ -129,7 +116,7 @@ func (a *AddrBook) init() { } // OnStart implements Service. -func (a *AddrBook) OnStart() error { +func (a *addrBook) OnStart() error { if err := a.BaseService.OnStart(); err != nil { return err } @@ -144,62 +131,56 @@ func (a *AddrBook) OnStart() error { } // OnStop implements Service. -func (a *AddrBook) OnStop() { +func (a *addrBook) OnStop() { a.BaseService.OnStop() } -func (a *AddrBook) Wait() { +func (a *addrBook) Wait() { a.wg.Wait() } -// AddOurAddress adds another one of our addresses. -func (a *AddrBook) AddOurAddress(addr *NetAddress) { +//------------------------------------------------------- + +// AddOurAddress one of our addresses. +func (a *addrBook) AddOurAddress(addr *p2p.NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() a.Logger.Info("Add our address to book", "addr", addr) a.ourAddrs[addr.String()] = addr } -// OurAddresses returns a list of our addresses. -func (a *AddrBook) OurAddresses() []*NetAddress { - addrs := []*NetAddress{} - for _, addr := range a.ourAddrs { - addrs = append(addrs, addr) - } - return addrs -} - -// AddAddress adds the given address as received from the given source. +// AddAddress implements AddrBook - adds the given address as received from the given source. // NOTE: addr must not be nil -func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) error { +func (a *addrBook) AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error { a.mtx.Lock() defer a.mtx.Unlock() return a.addAddress(addr, src) } -// NeedMoreAddrs returns true if there are not have enough addresses in the book. -func (a *AddrBook) NeedMoreAddrs() bool { - return a.Size() < needAddressThreshold -} - -// Size returns the number of addresses in the book. -func (a *AddrBook) Size() int { +// RemoveAddress implements AddrBook - removes the address from the book. +func (a *addrBook) RemoveAddress(addr *p2p.NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() - return a.size() + ka := a.addrLookup[addr.ID] + if ka == nil { + return + } + a.Logger.Info("Remove address from book", "addr", ka.Addr, "ID", ka.ID) + a.removeFromAllBuckets(ka) } -func (a *AddrBook) size() int { - return a.nNew + a.nOld +// NeedMoreAddrs implements AddrBook - returns true if there are not have enough addresses in the book. +func (a *addrBook) NeedMoreAddrs() bool { + return a.Size() < needAddressThreshold } -// PickAddress picks an address to connect to. +// PickAddress implements AddrBook. It picks an address to connect to. // The address is picked randomly from an old or new bucket according // to the newBias argument, which must be between [0, 100] (or else is truncated to that range) // and determines how biased we are to pick an address from a new bucket. // PickAddress returns nil if the AddrBook is empty or if we try to pick // from an empty bucket. -func (a *AddrBook) PickAddress(newBias int) *NetAddress { +func (a *addrBook) PickAddress(newBias int) *p2p.NetAddress { a.mtx.Lock() defer a.mtx.Unlock() @@ -243,12 +224,12 @@ func (a *AddrBook) PickAddress(newBias int) *NetAddress { return nil } -// MarkGood marks the peer as good and moves it into an "old" bucket. -// XXX: we never call this! -func (a *AddrBook) MarkGood(addr *NetAddress) { +// MarkGood implements AddrBook - it marks the peer as good and +// moves it into an "old" bucket. +func (a *addrBook) MarkGood(addr *p2p.NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() - ka := a.addrLookup[addr.String()] + ka := a.addrLookup[addr.ID] if ka == nil { return } @@ -258,39 +239,26 @@ func (a *AddrBook) MarkGood(addr *NetAddress) { } } -// MarkAttempt marks that an attempt was made to connect to the address. -func (a *AddrBook) MarkAttempt(addr *NetAddress) { +// MarkAttempt implements AddrBook - it marks that an attempt was made to connect to the address. +func (a *addrBook) MarkAttempt(addr *p2p.NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() - ka := a.addrLookup[addr.String()] + ka := a.addrLookup[addr.ID] if ka == nil { return } ka.markAttempt() } -// MarkBad currently just ejects the address. In the future, consider -// blacklisting. -func (a *AddrBook) MarkBad(addr *NetAddress) { +// MarkBad implements AddrBook. Currently it just ejects the address. +// TODO: black list for some amount of time +func (a *addrBook) MarkBad(addr *p2p.NetAddress) { a.RemoveAddress(addr) } -// RemoveAddress removes the address from the book. -func (a *AddrBook) RemoveAddress(addr *NetAddress) { - a.mtx.Lock() - defer a.mtx.Unlock() - ka := a.addrLookup[addr.String()] - if ka == nil { - return - } - a.Logger.Info("Remove address from book", "addr", addr) - a.removeFromAllBuckets(ka) -} - -/* Peer exchange */ - -// GetSelection randomly selects some addresses (old & new). Suitable for peer-exchange protocols. -func (a *AddrBook) GetSelection() []*NetAddress { +// GetSelection implements AddrBook. +// It randomly selects some addresses (old & new). Suitable for peer-exchange protocols. +func (a *addrBook) GetSelection() []*p2p.NetAddress { a.mtx.Lock() defer a.mtx.Unlock() @@ -298,10 +266,10 @@ func (a *AddrBook) GetSelection() []*NetAddress { return nil } - allAddr := make([]*NetAddress, a.size()) + allAddr := make([]*p2p.NetAddress, a.size()) i := 0 - for _, v := range a.addrLookup { - allAddr[i] = v.Addr + for _, ka := range a.addrLookup { + allAddr[i] = ka.Addr i++ } @@ -323,90 +291,39 @@ func (a *AddrBook) GetSelection() []*NetAddress { return allAddr[:numAddresses] } -/* Loading & Saving */ - -type addrBookJSON struct { - Key string - Addrs []*knownAddress -} - -func (a *AddrBook) saveToFile(filePath string) { - a.Logger.Info("Saving AddrBook to file", "size", a.Size()) - +// ListOfKnownAddresses returns the new and old addresses. +func (a *addrBook) ListOfKnownAddresses() []*knownAddress { a.mtx.Lock() defer a.mtx.Unlock() - // Compile Addrs - addrs := []*knownAddress{} - for _, ka := range a.addrLookup { - addrs = append(addrs, ka) - } - - aJSON := &addrBookJSON{ - Key: a.key, - Addrs: addrs, - } - jsonBytes, err := json.MarshalIndent(aJSON, "", "\t") - if err != nil { - a.Logger.Error("Failed to save AddrBook to file", "err", err) - return - } - err = cmn.WriteFileAtomic(filePath, jsonBytes, 0644) - if err != nil { - a.Logger.Error("Failed to save AddrBook to file", "file", filePath, "err", err) + addrs := []*knownAddress{} + for _, addr := range a.addrLookup { + addrs = append(addrs, addr.copy()) } + return addrs } -// Returns false if file does not exist. -// cmn.Panics if file is corrupt. -func (a *AddrBook) loadFromFile(filePath string) bool { - // If doesn't exist, do nothing. - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - return false - } +//------------------------------------------------ - // Load addrBookJSON{} - r, err := os.Open(filePath) - if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err)) - } - defer r.Close() // nolint: errcheck - aJSON := &addrBookJSON{} - dec := json.NewDecoder(r) - err = dec.Decode(aJSON) - if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err)) - } - - // Restore all the fields... - // Restore the key - a.key = aJSON.Key - // Restore .bucketsNew & .bucketsOld - for _, ka := range aJSON.Addrs { - for _, bucketIndex := range ka.Buckets { - bucket := a.getBucket(ka.BucketType, bucketIndex) - bucket[ka.Addr.String()] = ka - } - a.addrLookup[ka.Addr.String()] = ka - if ka.BucketType == bucketTypeNew { - a.nNew++ - } else { - a.nOld++ - } - } - return true +// Size returns the number of addresses in the book. +func (a *addrBook) Size() int { + a.mtx.Lock() + defer a.mtx.Unlock() + return a.size() } -// Save saves the book. -func (a *AddrBook) Save() { - a.Logger.Info("Saving AddrBook to file", "size", a.Size()) - a.saveToFile(a.filePath) +func (a *addrBook) size() int { + return a.nNew + a.nOld } -/* Private methods */ +//---------------------------------------------------------- + +// Save persists the address book to disk. +func (a *addrBook) Save() { + a.saveToFile(a.filePath) // thread safe +} -func (a *AddrBook) saveRoutine() { +func (a *addrBook) saveRoutine() { defer a.wg.Done() saveFileTicker := time.NewTicker(dumpAddressInterval) @@ -415,7 +332,7 @@ out: select { case <-saveFileTicker.C: a.saveToFile(a.filePath) - case <-a.Quit: + case <-a.Quit(): break out } } @@ -424,7 +341,9 @@ out: a.Logger.Info("Address handler done") } -func (a *AddrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAddress { +//---------------------------------------------------------- + +func (a *addrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAddress { switch bucketType { case bucketTypeNew: return a.bucketsNew[bucketIdx] @@ -438,7 +357,7 @@ func (a *AddrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAd // Adds ka to new bucket. Returns false if it couldn't do it cuz buckets full. // NOTE: currently it always returns true. -func (a *AddrBook) addToNewBucket(ka *knownAddress, bucketIdx int) bool { +func (a *addrBook) addToNewBucket(ka *knownAddress, bucketIdx int) bool { // Sanity check if ka.isOld() { a.Logger.Error(cmn.Fmt("Cannot add address already in old bucket to a new bucket: %v", ka)) @@ -455,24 +374,25 @@ func (a *AddrBook) addToNewBucket(ka *knownAddress, bucketIdx int) bool { // Enforce max addresses. if len(bucket) > newBucketSize { - a.Logger.Info("new bucket is full, expiring old ") + a.Logger.Info("new bucket is full, expiring new") a.expireNew(bucketIdx) } // Add to bucket. bucket[addrStr] = ka + // increment nNew if the peer doesnt already exist in a bucket if ka.addBucketRef(bucketIdx) == 1 { a.nNew++ } - // Ensure in addrLookup - a.addrLookup[addrStr] = ka + // Add it to addrLookup + a.addrLookup[ka.ID()] = ka return true } // Adds ka to old bucket. Returns false if it couldn't do it cuz buckets full. -func (a *AddrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool { +func (a *addrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool { // Sanity check if ka.isNew() { a.Logger.Error(cmn.Fmt("Cannot add new address to old bucket: %v", ka)) @@ -503,12 +423,12 @@ func (a *AddrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool { } // Ensure in addrLookup - a.addrLookup[addrStr] = ka + a.addrLookup[ka.ID()] = ka return true } -func (a *AddrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx int) { +func (a *addrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx int) { if ka.BucketType != bucketType { a.Logger.Error(cmn.Fmt("Bucket type mismatch: %v", ka)) return @@ -521,11 +441,11 @@ func (a *AddrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx } else { a.nOld-- } - delete(a.addrLookup, ka.Addr.String()) + delete(a.addrLookup, ka.ID()) } } -func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) { +func (a *addrBook) removeFromAllBuckets(ka *knownAddress) { for _, bucketIdx := range ka.Buckets { bucket := a.getBucket(ka.BucketType, bucketIdx) delete(bucket, ka.Addr.String()) @@ -536,10 +456,12 @@ func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) { } else { a.nOld-- } - delete(a.addrLookup, ka.Addr.String()) + delete(a.addrLookup, ka.ID()) } -func (a *AddrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress { +//---------------------------------------------------------- + +func (a *addrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress { bucket := a.getBucket(bucketType, bucketIdx) var oldest *knownAddress for _, ka := range bucket { @@ -550,7 +472,9 @@ func (a *AddrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress { return oldest } -func (a *AddrBook) addAddress(addr, src *NetAddress) error { +// adds the address to a "new" bucket. if its already in one, +// it only adds it probabilistically +func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { if a.routabilityStrict && !addr.Routable() { return fmt.Errorf("Cannot add non-routable address %v", addr) } @@ -559,7 +483,7 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) error { return fmt.Errorf("Cannot add ourselves with address %v", addr) } - ka := a.addrLookup[addr.String()] + ka := a.addrLookup[addr.ID] if ka != nil { // Already old. @@ -580,7 +504,10 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) error { } bucket := a.calcNewBucket(addr, src) - a.addToNewBucket(ka, bucket) + added := a.addToNewBucket(ka, bucket) + if !added { + a.Logger.Info("Can't add new address, addr book is full", "address", addr, "total", a.size()) + } a.Logger.Info("Added new address", "address", addr, "total", a.size()) return nil @@ -588,7 +515,7 @@ func (a *AddrBook) addAddress(addr, src *NetAddress) error { // Make space in the new buckets by expiring the really bad entries. // If no bad entries are available we remove the oldest. -func (a *AddrBook) expireNew(bucketIdx int) { +func (a *addrBook) expireNew(bucketIdx int) { for addrStr, ka := range a.bucketsNew[bucketIdx] { // If an entry is bad, throw it away if ka.isBad() { @@ -603,10 +530,10 @@ func (a *AddrBook) expireNew(bucketIdx int) { a.removeFromBucket(oldest, bucketTypeNew, bucketIdx) } -// Promotes an address from new to old. -// TODO: Move to old probabilistically. -// The better a node is, the less likely it should be evicted from an old bucket. -func (a *AddrBook) moveToOld(ka *knownAddress) { +// Promotes an address from new to old. If the destination bucket is full, +// demote the oldest one to a "new" bucket. +// TODO: Demote more probabilistically? +func (a *addrBook) moveToOld(ka *knownAddress) { // Sanity check if ka.isOld() { a.Logger.Error(cmn.Fmt("Cannot promote address that is already old %v", ka)) @@ -649,9 +576,12 @@ func (a *AddrBook) moveToOld(ka *knownAddress) { } } +//--------------------------------------------------------------------- +// calculate bucket placements + // doublesha256( key + sourcegroup + // int64(doublesha256(key + group + sourcegroup))%bucket_per_group ) % num_new_buckets -func (a *AddrBook) calcNewBucket(addr, src *NetAddress) int { +func (a *addrBook) calcNewBucket(addr, src *p2p.NetAddress) int { data1 := []byte{} data1 = append(data1, []byte(a.key)...) data1 = append(data1, []byte(a.groupKey(addr))...) @@ -672,7 +602,7 @@ func (a *AddrBook) calcNewBucket(addr, src *NetAddress) int { // doublesha256( key + group + // int64(doublesha256(key + addr))%buckets_per_group ) % num_old_buckets -func (a *AddrBook) calcOldBucket(addr *NetAddress) int { +func (a *addrBook) calcOldBucket(addr *p2p.NetAddress) int { data1 := []byte{} data1 = append(data1, []byte(a.key)...) data1 = append(data1, []byte(addr.String())...) @@ -694,7 +624,7 @@ func (a *AddrBook) calcOldBucket(addr *NetAddress) int { // This is the /16 for IPv4, the /32 (/36 for he.net) for IPv6, the string // "local" for a local address and the string "unroutable" for an unroutable // address. -func (a *AddrBook) groupKey(na *NetAddress) string { +func (a *addrBook) groupKey(na *p2p.NetAddress) string { if a.routabilityStrict && na.Local() { return "local" } @@ -739,127 +669,12 @@ func (a *AddrBook) groupKey(na *NetAddress) string { return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(bits, 128)}).String() } -//----------------------------------------------------------------------------- - -/* - knownAddress - - tracks information about a known network address that is used - to determine how viable an address is. -*/ -type knownAddress struct { - Addr *NetAddress - Src *NetAddress - Attempts int32 - LastAttempt time.Time - LastSuccess time.Time - BucketType byte - Buckets []int -} - -func newKnownAddress(addr *NetAddress, src *NetAddress) *knownAddress { - return &knownAddress{ - Addr: addr, - Src: src, - Attempts: 0, - LastAttempt: time.Now(), - BucketType: bucketTypeNew, - Buckets: nil, - } -} - -func (ka *knownAddress) isOld() bool { - return ka.BucketType == bucketTypeOld -} - -func (ka *knownAddress) isNew() bool { - return ka.BucketType == bucketTypeNew -} - -func (ka *knownAddress) markAttempt() { - now := time.Now() - ka.LastAttempt = now - ka.Attempts += 1 -} - -func (ka *knownAddress) markGood() { - now := time.Now() - ka.LastAttempt = now - ka.Attempts = 0 - ka.LastSuccess = now -} - -func (ka *knownAddress) addBucketRef(bucketIdx int) int { - for _, bucket := range ka.Buckets { - if bucket == bucketIdx { - // TODO refactor to return error? - // log.Warn(Fmt("Bucket already exists in ka.Buckets: %v", ka)) - return -1 - } - } - ka.Buckets = append(ka.Buckets, bucketIdx) - return len(ka.Buckets) -} - -func (ka *knownAddress) removeBucketRef(bucketIdx int) int { - buckets := []int{} - for _, bucket := range ka.Buckets { - if bucket != bucketIdx { - buckets = append(buckets, bucket) - } - } - if len(buckets) != len(ka.Buckets)-1 { - // TODO refactor to return error? - // log.Warn(Fmt("bucketIdx not found in ka.Buckets: %v", ka)) - return -1 - } - ka.Buckets = buckets - return len(ka.Buckets) -} - -/* - An address is bad if the address in question is a New address, has not been tried in the last - minute, and meets one of the following criteria: - - 1) It claims to be from the future - 2) It hasn't been seen in over a month - 3) It has failed at least three times and never succeeded - 4) It has failed ten times in the last week - - All addresses that meet these criteria are assumed to be worthless and not - worth keeping hold of. - - XXX: so a good peer needs us to call MarkGood before the conditions above are reached! -*/ -func (ka *knownAddress) isBad() bool { - // Is Old --> good - if ka.BucketType == bucketTypeOld { - return false - } - - // Has been attempted in the last minute --> good - if ka.LastAttempt.Before(time.Now().Add(-1 * time.Minute)) { - return false - } - - // Too old? - // XXX: does this mean if we've kept a connection up for this long we'll disconnect?! - // and shouldn't it be .Before ? - if ka.LastAttempt.After(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { - return true - } - - // Never succeeded? - if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { - return true - } - - // Hasn't succeeded in too long? - // XXX: does this mean if we've kept a connection up for this long we'll disconnect?! - if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && - ka.Attempts >= maxFailures { - return true - } - - return false +// doubleSha256 calculates sha256(sha256(b)) and returns the resulting bytes. +func doubleSha256(b []byte) []byte { + hasher := sha256.New() + hasher.Write(b) // nolint: errcheck, gas + sum := hasher.Sum(nil) + hasher.Reset() + hasher.Write(sum) // nolint: errcheck, gas + return hasher.Sum(nil) } diff --git a/p2p/addrbook_test.go b/p2p/pex/addrbook_test.go similarity index 91% rename from p2p/addrbook_test.go rename to p2p/pex/addrbook_test.go index d84c008ed..166d31847 100644 --- a/p2p/addrbook_test.go +++ b/p2p/pex/addrbook_test.go @@ -1,12 +1,15 @@ -package p2p +package pex import ( + "encoding/hex" "fmt" "io/ioutil" "math/rand" "testing" "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/p2p" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" ) @@ -102,7 +105,7 @@ func TestAddrBookLookup(t *testing.T) { src := addrSrc.src book.AddAddress(addr, src) - ka := book.addrLookup[addr.String()] + ka := book.addrLookup[addr.ID] assert.NotNil(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) if !(ka.Addr.Equals(addr) && ka.Src.Equals(src)) { @@ -166,8 +169,8 @@ func TestAddrBookHandlesDuplicates(t *testing.T) { } type netAddressPair struct { - addr *NetAddress - src *NetAddress + addr *p2p.NetAddress + src *p2p.NetAddress } func randNetAddressPairs(t *testing.T, n int) []netAddressPair { @@ -178,7 +181,7 @@ func randNetAddressPairs(t *testing.T, n int) []netAddressPair { return randAddrs } -func randIPv4Address(t *testing.T) *NetAddress { +func randIPv4Address(t *testing.T) *p2p.NetAddress { for { ip := fmt.Sprintf("%v.%v.%v.%v", rand.Intn(254)+1, @@ -187,7 +190,9 @@ func randIPv4Address(t *testing.T) *NetAddress { rand.Intn(255), ) port := rand.Intn(65535-1) + 1 - addr, err := NewNetAddressString(fmt.Sprintf("%v:%v", ip, port)) + id := p2p.ID(hex.EncodeToString(cmn.RandBytes(p2p.IDByteLength))) + idAddr := p2p.IDAddressString(id, fmt.Sprintf("%v:%v", ip, port)) + addr, err := p2p.NewNetAddressString(idAddr) assert.Nil(t, err, "error generating rand network address") if addr.Routable() { return addr diff --git a/p2p/pex/file.go b/p2p/pex/file.go new file mode 100644 index 000000000..38142dd9d --- /dev/null +++ b/p2p/pex/file.go @@ -0,0 +1,83 @@ +package pex + +import ( + "encoding/json" + "os" + + cmn "github.com/tendermint/tmlibs/common" +) + +/* Loading & Saving */ + +type addrBookJSON struct { + Key string `json:"key"` + Addrs []*knownAddress `json:"addrs"` +} + +func (a *addrBook) saveToFile(filePath string) { + a.Logger.Info("Saving AddrBook to file", "size", a.Size()) + + a.mtx.Lock() + defer a.mtx.Unlock() + // Compile Addrs + addrs := []*knownAddress{} + for _, ka := range a.addrLookup { + addrs = append(addrs, ka) + } + + aJSON := &addrBookJSON{ + Key: a.key, + Addrs: addrs, + } + + jsonBytes, err := json.MarshalIndent(aJSON, "", "\t") + if err != nil { + a.Logger.Error("Failed to save AddrBook to file", "err", err) + return + } + err = cmn.WriteFileAtomic(filePath, jsonBytes, 0644) + if err != nil { + a.Logger.Error("Failed to save AddrBook to file", "file", filePath, "err", err) + } +} + +// Returns false if file does not exist. +// cmn.Panics if file is corrupt. +func (a *addrBook) loadFromFile(filePath string) bool { + // If doesn't exist, do nothing. + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + + // Load addrBookJSON{} + r, err := os.Open(filePath) + if err != nil { + cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err)) + } + defer r.Close() // nolint: errcheck + aJSON := &addrBookJSON{} + dec := json.NewDecoder(r) + err = dec.Decode(aJSON) + if err != nil { + cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err)) + } + + // Restore all the fields... + // Restore the key + a.key = aJSON.Key + // Restore .bucketsNew & .bucketsOld + for _, ka := range aJSON.Addrs { + for _, bucketIndex := range ka.Buckets { + bucket := a.getBucket(ka.BucketType, bucketIndex) + bucket[ka.Addr.String()] = ka + } + a.addrLookup[ka.ID()] = ka + if ka.BucketType == bucketTypeNew { + a.nNew++ + } else { + a.nOld++ + } + } + return true +} diff --git a/p2p/pex/known_address.go b/p2p/pex/known_address.go new file mode 100644 index 000000000..085eb10fa --- /dev/null +++ b/p2p/pex/known_address.go @@ -0,0 +1,142 @@ +package pex + +import ( + "time" + + "github.com/tendermint/tendermint/p2p" +) + +// knownAddress tracks information about a known network address +// that is used to determine how viable an address is. +type knownAddress struct { + Addr *p2p.NetAddress `json:"addr"` + Src *p2p.NetAddress `json:"src"` + Attempts int32 `json:"attempts"` + LastAttempt time.Time `json:"last_attempt"` + LastSuccess time.Time `json:"last_success"` + BucketType byte `json:"bucket_type"` + Buckets []int `json:"buckets"` +} + +func newKnownAddress(addr *p2p.NetAddress, src *p2p.NetAddress) *knownAddress { + return &knownAddress{ + Addr: addr, + Src: src, + Attempts: 0, + LastAttempt: time.Now(), + BucketType: bucketTypeNew, + Buckets: nil, + } +} + +func (ka *knownAddress) ID() p2p.ID { + return ka.Addr.ID +} + +func (ka *knownAddress) copy() *knownAddress { + return &knownAddress{ + Addr: ka.Addr, + Src: ka.Src, + Attempts: ka.Attempts, + LastAttempt: ka.LastAttempt, + LastSuccess: ka.LastSuccess, + BucketType: ka.BucketType, + Buckets: ka.Buckets, + } +} + +func (ka *knownAddress) isOld() bool { + return ka.BucketType == bucketTypeOld +} + +func (ka *knownAddress) isNew() bool { + return ka.BucketType == bucketTypeNew +} + +func (ka *knownAddress) markAttempt() { + now := time.Now() + ka.LastAttempt = now + ka.Attempts += 1 +} + +func (ka *knownAddress) markGood() { + now := time.Now() + ka.LastAttempt = now + ka.Attempts = 0 + ka.LastSuccess = now +} + +func (ka *knownAddress) addBucketRef(bucketIdx int) int { + for _, bucket := range ka.Buckets { + if bucket == bucketIdx { + // TODO refactor to return error? + // log.Warn(Fmt("Bucket already exists in ka.Buckets: %v", ka)) + return -1 + } + } + ka.Buckets = append(ka.Buckets, bucketIdx) + return len(ka.Buckets) +} + +func (ka *knownAddress) removeBucketRef(bucketIdx int) int { + buckets := []int{} + for _, bucket := range ka.Buckets { + if bucket != bucketIdx { + buckets = append(buckets, bucket) + } + } + if len(buckets) != len(ka.Buckets)-1 { + // TODO refactor to return error? + // log.Warn(Fmt("bucketIdx not found in ka.Buckets: %v", ka)) + return -1 + } + ka.Buckets = buckets + return len(ka.Buckets) +} + +/* + An address is bad if the address in question is a New address, has not been tried in the last + minute, and meets one of the following criteria: + + 1) It claims to be from the future + 2) It hasn't been seen in over a week + 3) It has failed at least three times and never succeeded + 4) It has failed ten times in the last week + + All addresses that meet these criteria are assumed to be worthless and not + worth keeping hold of. + + XXX: so a good peer needs us to call MarkGood before the conditions above are reached! +*/ +func (ka *knownAddress) isBad() bool { + // Is Old --> good + if ka.BucketType == bucketTypeOld { + return false + } + + // Has been attempted in the last minute --> good + if ka.LastAttempt.Before(time.Now().Add(-1 * time.Minute)) { + return false + } + + // Too old? + // XXX: does this mean if we've kept a connection up for this long we'll disconnect?! + // and shouldn't it be .Before ? + if ka.LastAttempt.After(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { + return true + } + + // Never succeeded? + if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { + return true + } + + // Hasn't succeeded in too long? + // XXX: does this mean if we've kept a connection up for this long we'll disconnect?! + if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && + ka.Attempts >= maxFailures { + return true + } + + return false +} diff --git a/p2p/pex/params.go b/p2p/pex/params.go new file mode 100644 index 000000000..f94e1021c --- /dev/null +++ b/p2p/pex/params.go @@ -0,0 +1,55 @@ +package pex + +import "time" + +const ( + // addresses under which the address manager will claim to need more addresses. + needAddressThreshold = 1000 + + // interval used to dump the address cache to disk for future use. + dumpAddressInterval = time.Minute * 2 + + // max addresses in each old address bucket. + oldBucketSize = 64 + + // buckets we split old addresses over. + oldBucketCount = 64 + + // max addresses in each new address bucket. + newBucketSize = 64 + + // buckets that we spread new addresses over. + newBucketCount = 256 + + // old buckets over which an address group will be spread. + oldBucketsPerGroup = 4 + + // new buckets over which a source address group will be spread. + newBucketsPerGroup = 32 + + // buckets a frequently seen new address may end up in. + maxNewBucketsPerAddress = 4 + + // days before which we assume an address has vanished + // if we have not seen it announced in that long. + numMissingDays = 7 + + // tries without a single success before we assume an address is bad. + numRetries = 3 + + // max failures we will accept without a success before considering an address bad. + maxFailures = 10 // ? + + // days since the last success before we will consider evicting an address. + minBadDays = 7 + + // % of total addresses known returned by GetSelection. + getSelectionPercent = 23 + + // min addresses that must be returned by GetSelection. Useful for bootstrapping. + minGetSelection = 32 + + // max addresses returned by GetSelection + // NOTE: this must match "maxPexMessageSize" + maxGetSelection = 250 +) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go new file mode 100644 index 000000000..5aeca8f76 --- /dev/null +++ b/p2p/pex/pex_reactor.go @@ -0,0 +1,551 @@ +package pex + +import ( + "bytes" + "fmt" + "math/rand" + "reflect" + "sort" + "time" + + "github.com/pkg/errors" + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/conn" +) + +type Peer = p2p.Peer + +const ( + // PexChannel is a channel for PEX messages + PexChannel = byte(0x00) + + maxPexMessageSize = 1048576 // 1MB + + // ensure we have enough peers + defaultEnsurePeersPeriod = 30 * time.Second + defaultMinNumOutboundPeers = 10 + + // Seed/Crawler constants + // TODO: + // We want seeds to only advertise good peers. + // Peers are marked by external mechanisms. + // We need a config value that can be set to be + // on the order of how long it would take before a good + // peer is marked good. + defaultSeedDisconnectWaitPeriod = 2 * time.Minute // disconnect after this + defaultCrawlPeerInterval = 2 * time.Minute // dont redial for this. TODO: back-off + defaultCrawlPeersPeriod = 30 * time.Second // check some peers every this +) + +// PEXReactor handles PEX (peer exchange) and ensures that an +// adequate number of peers are connected to the switch. +// +// It uses `AddrBook` (address book) to store `NetAddress`es of the peers. +// +// ## Preventing abuse +// +// Only accept pexAddrsMsg from peers we sent a corresponding pexRequestMsg too. +// Only accept one pexRequestMsg every ~defaultEnsurePeersPeriod. +type PEXReactor struct { + p2p.BaseReactor + + book AddrBook + config *PEXReactorConfig + ensurePeersPeriod time.Duration + + // maps to prevent abuse + requestsSent *cmn.CMap // ID->struct{}: unanswered send requests + lastReceivedRequests *cmn.CMap // ID->time.Time: last time peer requested from us +} + +// PEXReactorConfig holds reactor specific configuration data. +type PEXReactorConfig struct { + // Seed/Crawler mode + SeedMode bool + + // Seeds is a list of addresses reactor may use + // if it can't connect to peers in the addrbook. + Seeds []string +} + +// NewPEXReactor creates new PEX reactor. +func NewPEXReactor(b AddrBook, config *PEXReactorConfig) *PEXReactor { + r := &PEXReactor{ + book: b, + config: config, + ensurePeersPeriod: defaultEnsurePeersPeriod, + requestsSent: cmn.NewCMap(), + lastReceivedRequests: cmn.NewCMap(), + } + r.BaseReactor = *p2p.NewBaseReactor("PEXReactor", r) + return r +} + +// OnStart implements BaseService +func (r *PEXReactor) OnStart() error { + if err := r.BaseReactor.OnStart(); err != nil { + return err + } + err := r.book.Start() + if err != nil && err != cmn.ErrAlreadyStarted { + return err + } + + // return err if user provided a bad seed address + if err := r.checkSeeds(); err != nil { + return err + } + + // Check if this node should run + // in seed/crawler mode + if r.config.SeedMode { + go r.crawlPeersRoutine() + } else { + go r.ensurePeersRoutine() + } + return nil +} + +// OnStop implements BaseService +func (r *PEXReactor) OnStop() { + r.BaseReactor.OnStop() + r.book.Stop() +} + +// GetChannels implements Reactor +func (r *PEXReactor) GetChannels() []*conn.ChannelDescriptor { + return []*conn.ChannelDescriptor{ + { + ID: PexChannel, + Priority: 1, + SendQueueCapacity: 10, + }, + } +} + +// AddPeer implements Reactor by adding peer to the address book (if inbound) +// or by requesting more addresses (if outbound). +func (r *PEXReactor) AddPeer(p Peer) { + if p.IsOutbound() { + // For outbound peers, the address is already in the books - + // either via DialPeersAsync or r.Receive. + // Ask it for more peers if we need. + if r.book.NeedMoreAddrs() { + r.RequestAddrs(p) + } + } else { + // For inbound peers, the peer is its own source, + // and its NodeInfo has already been validated. + // Let the ensurePeersRoutine handle asking for more + // peers when we need - we don't trust inbound peers as much. + addr := p.NodeInfo().NetAddress() + r.book.AddAddress(addr, addr) + } +} + +// RemovePeer implements Reactor. +func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) { + id := string(p.ID()) + r.requestsSent.Delete(id) + r.lastReceivedRequests.Delete(id) +} + +// Receive implements Reactor by handling incoming PEX messages. +func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) { + _, msg, err := DecodeMessage(msgBytes) + if err != nil { + r.Logger.Error("Error decoding message", "err", err) + return + } + r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg) + + switch msg := msg.(type) { + case *pexRequestMessage: + // Check we're not receiving too many requests + if err := r.receiveRequest(src); err != nil { + r.Switch.StopPeerForError(src, err) + return + } + + // Seeds disconnect after sending a batch of addrs + if r.config.SeedMode { + // TODO: should we be more selective ? + r.SendAddrs(src, r.book.GetSelection()) + r.Switch.StopPeerGracefully(src) + } else { + r.SendAddrs(src, r.book.GetSelection()) + } + + case *pexAddrsMessage: + // If we asked for addresses, add them to the book + if err := r.ReceiveAddrs(msg.Addrs, src); err != nil { + r.Switch.StopPeerForError(src, err) + return + } + default: + r.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) + } +} + +func (r *PEXReactor) receiveRequest(src Peer) error { + id := string(src.ID()) + v := r.lastReceivedRequests.Get(id) + if v == nil { + // initialize with empty time + lastReceived := time.Time{} + r.lastReceivedRequests.Set(id, lastReceived) + return nil + } + + lastReceived := v.(time.Time) + if lastReceived.Equal(time.Time{}) { + // first time gets a free pass. then we start tracking the time + lastReceived = time.Now() + r.lastReceivedRequests.Set(id, lastReceived) + return nil + } + + now := time.Now() + if now.Sub(lastReceived) < r.ensurePeersPeriod/3 { + return fmt.Errorf("Peer (%v) is sending too many PEX requests. Disconnecting", src.ID()) + } + r.lastReceivedRequests.Set(id, now) + return nil +} + +// RequestAddrs asks peer for more addresses if we do not already +// have a request out for this peer. +func (r *PEXReactor) RequestAddrs(p Peer) { + id := string(p.ID()) + if r.requestsSent.Has(id) { + return + } + r.requestsSent.Set(id, struct{}{}) + p.Send(PexChannel, struct{ PexMessage }{&pexRequestMessage{}}) +} + +// ReceiveAddrs adds the given addrs to the addrbook if theres an open +// request for this peer and deletes the open request. +// If there's no open request for the src peer, it returns an error. +func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { + id := string(src.ID()) + + if !r.requestsSent.Has(id) { + return errors.New("Received unsolicited pexAddrsMessage") + } + + r.requestsSent.Delete(id) + + srcAddr := src.NodeInfo().NetAddress() + for _, netAddr := range addrs { + if netAddr != nil { + r.book.AddAddress(netAddr, srcAddr) + } + } + return nil +} + +// SendAddrs sends addrs to the peer. +func (r *PEXReactor) SendAddrs(p Peer, netAddrs []*p2p.NetAddress) { + p.Send(PexChannel, struct{ PexMessage }{&pexAddrsMessage{Addrs: netAddrs}}) +} + +// SetEnsurePeersPeriod sets period to ensure peers connected. +func (r *PEXReactor) SetEnsurePeersPeriod(d time.Duration) { + r.ensurePeersPeriod = d +} + +// Ensures that sufficient peers are connected. (continuous) +func (r *PEXReactor) ensurePeersRoutine() { + // Randomize when routine starts + ensurePeersPeriodMs := r.ensurePeersPeriod.Nanoseconds() / 1e6 + time.Sleep(time.Duration(rand.Int63n(ensurePeersPeriodMs)) * time.Millisecond) + + // fire once immediately. + // ensures we dial the seeds right away if the book is empty + r.ensurePeers() + + // fire periodically + ticker := time.NewTicker(r.ensurePeersPeriod) + for { + select { + case <-ticker.C: + r.ensurePeers() + case <-r.Quit(): + ticker.Stop() + return + } + } +} + +// ensurePeers ensures that sufficient peers are connected. (once) +// +// heuristic that we haven't perfected yet, or, perhaps is manually edited by +// the node operator. It should not be used to compute what addresses are +// already connected or not. +func (r *PEXReactor) ensurePeers() { + numOutPeers, numInPeers, numDialing := r.Switch.NumPeers() + numToDial := defaultMinNumOutboundPeers - (numOutPeers + numDialing) + r.Logger.Info("Ensure peers", "numOutPeers", numOutPeers, "numDialing", numDialing, "numToDial", numToDial) + if numToDial <= 0 { + return + } + + // bias to prefer more vetted peers when we have fewer connections. + // not perfect, but somewhate ensures that we prioritize connecting to more-vetted + // NOTE: range here is [10, 90]. Too high ? + newBias := cmn.MinInt(numOutPeers, 8)*10 + 10 + + toDial := make(map[p2p.ID]*p2p.NetAddress) + // Try maxAttempts times to pick numToDial addresses to dial + maxAttempts := numToDial * 3 + for i := 0; i < maxAttempts && len(toDial) < numToDial; i++ { + try := r.book.PickAddress(newBias) + if try == nil { + continue + } + if _, selected := toDial[try.ID]; selected { + continue + } + if dialling := r.Switch.IsDialing(try.ID); dialling { + continue + } + if connected := r.Switch.Peers().Has(try.ID); connected { + continue + } + r.Logger.Info("Will dial address", "addr", try) + toDial[try.ID] = try + } + + // Dial picked addresses + for _, item := range toDial { + go func(picked *p2p.NetAddress) { + _, err := r.Switch.DialPeerWithAddress(picked, false) + if err != nil { + // TODO: detect more "bad peer" scenarios + if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok { + r.book.MarkBad(picked) + } else { + r.book.MarkAttempt(picked) + } + } + }(item) + } + + // If we need more addresses, pick a random peer and ask for more. + if r.book.NeedMoreAddrs() { + peers := r.Switch.Peers().List() + peersCount := len(peers) + if peersCount > 0 { + peer := peers[rand.Int()%peersCount] // nolint: gas + r.Logger.Info("We need more addresses. Sending pexRequest to random peer", "peer", peer) + r.RequestAddrs(peer) + } + } + + // If we are not connected to nor dialing anybody, fallback to dialing a seed. + if numOutPeers+numInPeers+numDialing+len(toDial) == 0 { + r.Logger.Info("No addresses to dial nor connected peers. Falling back to seeds") + r.dialSeeds() + } +} + +// check seed addresses are well formed +func (r *PEXReactor) checkSeeds() error { + lSeeds := len(r.config.Seeds) + if lSeeds == 0 { + return nil + } + _, errs := p2p.NewNetAddressStrings(r.config.Seeds) + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} + +// randomly dial seeds until we connect to one or exhaust them +func (r *PEXReactor) dialSeeds() { + lSeeds := len(r.config.Seeds) + if lSeeds == 0 { + return + } + seedAddrs, _ := p2p.NewNetAddressStrings(r.config.Seeds) + + perm := rand.Perm(lSeeds) + // perm := r.Switch.rng.Perm(lSeeds) + for _, i := range perm { + // dial a random seed + seedAddr := seedAddrs[i] + peer, err := r.Switch.DialPeerWithAddress(seedAddr, false) + if err != nil { + r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) + } else { + r.Switch.Logger.Info("Connected to seed", "peer", peer) + return + } + } + r.Switch.Logger.Error("Couldn't connect to any seeds") +} + +//---------------------------------------------------------- + +// Explores the network searching for more peers. (continuous) +// Seed/Crawler Mode causes this node to quickly disconnect +// from peers, except other seed nodes. +func (r *PEXReactor) crawlPeersRoutine() { + // Do an initial crawl + r.crawlPeers() + + // Fire periodically + ticker := time.NewTicker(defaultCrawlPeersPeriod) + + for { + select { + case <-ticker.C: + r.attemptDisconnects() + r.crawlPeers() + case <-r.Quit(): + return + } + } +} + +// crawlPeerInfo handles temporary data needed for the +// network crawling performed during seed/crawler mode. +type crawlPeerInfo struct { + // The listening address of a potential peer we learned about + Addr *p2p.NetAddress + + // The last time we attempt to reach this address + LastAttempt time.Time + + // The last time we successfully reached this address + LastSuccess time.Time +} + +// oldestFirst implements sort.Interface for []crawlPeerInfo +// based on the LastAttempt field. +type oldestFirst []crawlPeerInfo + +func (of oldestFirst) Len() int { return len(of) } +func (of oldestFirst) Swap(i, j int) { of[i], of[j] = of[j], of[i] } +func (of oldestFirst) Less(i, j int) bool { return of[i].LastAttempt.Before(of[j].LastAttempt) } + +// getPeersToCrawl returns addresses of potential peers that we wish to validate. +// NOTE: The status information is ordered as described above. +func (r *PEXReactor) getPeersToCrawl() []crawlPeerInfo { + var of oldestFirst + + // TODO: be more selective + addrs := r.book.ListOfKnownAddresses() + for _, addr := range addrs { + if len(addr.ID()) == 0 { + continue // dont use peers without id + } + + of = append(of, crawlPeerInfo{ + Addr: addr.Addr, + LastAttempt: addr.LastAttempt, + LastSuccess: addr.LastSuccess, + }) + } + sort.Sort(of) + return of +} + +// crawlPeers will crawl the network looking for new peer addresses. (once) +func (r *PEXReactor) crawlPeers() { + peerInfos := r.getPeersToCrawl() + + now := time.Now() + // Use addresses we know of to reach additional peers + for _, pi := range peerInfos { + // Do not attempt to connect with peers we recently dialed + if now.Sub(pi.LastAttempt) < defaultCrawlPeerInterval { + continue + } + // Otherwise, attempt to connect with the known address + _, err := r.Switch.DialPeerWithAddress(pi.Addr, false) + if err != nil { + r.book.MarkAttempt(pi.Addr) + continue + } + } + // Crawl the connected peers asking for more addresses + for _, pi := range peerInfos { + // We will wait a minimum period of time before crawling peers again + if now.Sub(pi.LastAttempt) >= defaultCrawlPeerInterval { + peer := r.Switch.Peers().Get(pi.Addr.ID) + if peer != nil { + r.RequestAddrs(peer) + } + } + } +} + +// attemptDisconnects checks if we've been with each peer long enough to disconnect +func (r *PEXReactor) attemptDisconnects() { + for _, peer := range r.Switch.Peers().List() { + status := peer.Status() + if status.Duration < defaultSeedDisconnectWaitPeriod { + continue + } + if peer.IsPersistent() { + continue + } + r.Switch.StopPeerGracefully(peer) + } +} + +//----------------------------------------------------------------------------- +// Messages + +const ( + msgTypeRequest = byte(0x01) + msgTypeAddrs = byte(0x02) +) + +// PexMessage is a primary type for PEX messages. Underneath, it could contain +// either pexRequestMessage, or pexAddrsMessage messages. +type PexMessage interface{} + +var _ = wire.RegisterInterface( + struct{ PexMessage }{}, + wire.ConcreteType{&pexRequestMessage{}, msgTypeRequest}, + wire.ConcreteType{&pexAddrsMessage{}, msgTypeAddrs}, +) + +// DecodeMessage implements interface registered above. +func DecodeMessage(bz []byte) (msgType byte, msg PexMessage, err error) { + msgType = bz[0] + n := new(int) + r := bytes.NewReader(bz) + msg = wire.ReadBinary(struct{ PexMessage }{}, r, maxPexMessageSize, n, &err).(struct{ PexMessage }).PexMessage + return +} + +/* +A pexRequestMessage requests additional peer addresses. +*/ +type pexRequestMessage struct { +} + +func (m *pexRequestMessage) String() string { + return "[pexRequest]" +} + +/* +A message with announced peer addresses. +*/ +type pexAddrsMessage struct { + Addrs []*p2p.NetAddress +} + +func (m *pexAddrsMessage) String() string { + return fmt.Sprintf("[pexAddrs %v]", m.Addrs) +} diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go new file mode 100644 index 000000000..82dafecd4 --- /dev/null +++ b/p2p/pex/pex_reactor_test.go @@ -0,0 +1,370 @@ +package pex + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/conn" +) + +var ( + config *cfg.P2PConfig +) + +func init() { + config = cfg.DefaultP2PConfig() + config.PexReactor = true +} + +func TestPEXReactorBasic(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", true) + book.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + + assert.NotNil(r) + assert.NotEmpty(r.GetChannels()) +} + +func TestPEXReactorAddRemovePeer(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", true) + book.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + + size := book.Size() + peer := p2p.CreateRandomPeer(false) + + r.AddPeer(peer) + assert.Equal(size+1, book.Size()) + + r.RemovePeer(peer, "peer not available") + assert.Equal(size+1, book.Size()) + + outboundPeer := p2p.CreateRandomPeer(true) + + r.AddPeer(outboundPeer) + assert.Equal(size+1, book.Size(), "outbound peers should not be added to the address book") + + r.RemovePeer(outboundPeer, "peer not available") + assert.Equal(size+1, book.Size()) +} + +func TestPEXReactorRunning(t *testing.T) { + N := 3 + switches := make([]*p2p.Switch, N) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(t, err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", false) + book.SetLogger(log.TestingLogger()) + + // create switches + for i := 0; i < N; i++ { + switches[i] = p2p.MakeSwitch(config, i, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + sw.SetLogger(log.TestingLogger().With("switch", i)) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + r.SetEnsurePeersPeriod(250 * time.Millisecond) + sw.AddReactor("pex", r) + return sw + }) + } + + // fill the address book and add listeners + for _, s := range switches { + addr, _ := p2p.NewNetAddressString(s.NodeInfo().ListenAddr) + book.AddAddress(addr, addr) + s.AddListener(p2p.NewDefaultListener("tcp", s.NodeInfo().ListenAddr, true, log.TestingLogger())) + } + + // start switches + for _, s := range switches { + err := s.Start() // start switch and reactors + require.Nil(t, err) + } + + assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) + + // stop them + for _, s := range switches { + s.Stop() + } +} + +func assertPeersWithTimeout(t *testing.T, switches []*p2p.Switch, checkPeriod, timeout time.Duration, nPeers int) { + ticker := time.NewTicker(checkPeriod) + remaining := timeout + for { + select { + case <-ticker.C: + // check peers are connected + allGood := true + for _, s := range switches { + outbound, inbound, _ := s.NumPeers() + if outbound+inbound < nPeers { + allGood = false + } + } + remaining -= checkPeriod + if remaining < 0 { + remaining = 0 + } + if allGood { + return + } + case <-time.After(remaining): + numPeersStr := "" + for i, s := range switches { + outbound, inbound, _ := s.NumPeers() + numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound) + } + t.Errorf("expected all switches to be connected to at least one peer (switches: %s)", numPeersStr) + return + } + } +} + +func TestPEXReactorReceive(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", false) + book.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + + peer := p2p.CreateRandomPeer(false) + + // we have to send a request to receive responses + r.RequestAddrs(peer) + + size := book.Size() + addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()} + msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) + r.Receive(PexChannel, peer, msg) + assert.Equal(size+1, book.Size()) + + msg = wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) + r.Receive(PexChannel, peer, msg) +} + +func TestPEXReactorRequestMessageAbuse(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", true) + book.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + sw := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) + sw.SetLogger(log.TestingLogger()) + sw.AddReactor("PEX", r) + r.SetSwitch(sw) + r.SetLogger(log.TestingLogger()) + + peer := newMockPeer() + p2p.AddPeerToSwitch(sw, peer) + assert.True(sw.Peers().Has(peer.ID())) + + id := string(peer.ID()) + msg := wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) + + // first time creates the entry + r.Receive(PexChannel, peer, msg) + assert.True(r.lastReceivedRequests.Has(id)) + assert.True(sw.Peers().Has(peer.ID())) + + // next time sets the last time value + r.Receive(PexChannel, peer, msg) + assert.True(r.lastReceivedRequests.Has(id)) + assert.True(sw.Peers().Has(peer.ID())) + + // third time is too many too soon - peer is removed + r.Receive(PexChannel, peer, msg) + assert.False(r.lastReceivedRequests.Has(id)) + assert.False(sw.Peers().Has(peer.ID())) +} + +func TestPEXReactorAddrsMessageAbuse(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", true) + book.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + sw := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { return sw }) + sw.SetLogger(log.TestingLogger()) + sw.AddReactor("PEX", r) + r.SetSwitch(sw) + r.SetLogger(log.TestingLogger()) + + peer := newMockPeer() + p2p.AddPeerToSwitch(sw, peer) + assert.True(sw.Peers().Has(peer.ID())) + + id := string(peer.ID()) + + // request addrs from the peer + r.RequestAddrs(peer) + assert.True(r.requestsSent.Has(id)) + assert.True(sw.Peers().Has(peer.ID())) + + addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()} + msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) + + // receive some addrs. should clear the request + r.Receive(PexChannel, peer, msg) + assert.False(r.requestsSent.Has(id)) + assert.True(sw.Peers().Has(peer.ID())) + + // receiving more addrs causes a disconnect + r.Receive(PexChannel, peer, msg) + assert.False(sw.Peers().Has(peer.ID())) +} + +func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) { + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(t, err) + defer os.RemoveAll(dir) // nolint: errcheck + + book := NewAddrBook(dir+"addrbook.json", false) + book.SetLogger(log.TestingLogger()) + + // 1. create seed + seed := p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + sw.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{}) + r.SetLogger(log.TestingLogger()) + r.SetEnsurePeersPeriod(250 * time.Millisecond) + sw.AddReactor("pex", r) + return sw + }) + seed.AddListener(p2p.NewDefaultListener("tcp", seed.NodeInfo().ListenAddr, true, log.TestingLogger())) + err = seed.Start() + require.Nil(t, err) + defer seed.Stop() + + // 2. create usual peer + sw := p2p.MakeSwitch(config, 1, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + sw.SetLogger(log.TestingLogger()) + + r := NewPEXReactor(book, &PEXReactorConfig{Seeds: []string{seed.NodeInfo().ListenAddr}}) + r.SetLogger(log.TestingLogger()) + r.SetEnsurePeersPeriod(250 * time.Millisecond) + sw.AddReactor("pex", r) + return sw + }) + err = sw.Start() + require.Nil(t, err) + defer sw.Stop() + + // 3. check that peer at least connects to seed + assertPeersWithTimeout(t, []*p2p.Switch{sw}, 10*time.Millisecond, 10*time.Second, 1) +} + +func TestPEXReactorCrawlStatus(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(err) + defer os.RemoveAll(dir) // nolint: errcheck + book := NewAddrBook(dir+"addrbook.json", false) + book.SetLogger(log.TestingLogger()) + + pexR := NewPEXReactor(book, &PEXReactorConfig{SeedMode: true}) + // Seed/Crawler mode uses data from the Switch + p2p.MakeSwitch(config, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { + pexR.SetLogger(log.TestingLogger()) + sw.SetLogger(log.TestingLogger().With("switch", i)) + sw.AddReactor("pex", pexR) + return sw + }) + + // Create a peer, add it to the peer set and the addrbook. + peer := p2p.CreateRandomPeer(false) + p2p.AddPeerToSwitch(pexR.Switch, peer) + addr1 := peer.NodeInfo().NetAddress() + pexR.book.AddAddress(addr1, addr1) + + // Add a non-connected address to the book. + _, addr2 := p2p.CreateRoutableAddr() + pexR.book.AddAddress(addr2, addr1) + + // Get some peerInfos to crawl + peerInfos := pexR.getPeersToCrawl() + + // Make sure it has the proper number of elements + assert.Equal(2, len(peerInfos)) + + // TODO: test +} + +type mockPeer struct { + *cmn.BaseService + pubKey crypto.PubKey + addr *p2p.NetAddress + outbound, persistent bool +} + +func newMockPeer() mockPeer { + _, netAddr := p2p.CreateRoutableAddr() + mp := mockPeer{ + addr: netAddr, + pubKey: crypto.GenPrivKeyEd25519().Wrap().PubKey(), + } + mp.BaseService = cmn.NewBaseService(nil, "MockPeer", mp) + mp.Start() + return mp +} + +func (mp mockPeer) ID() p2p.ID { return p2p.PubKeyToID(mp.pubKey) } +func (mp mockPeer) IsOutbound() bool { return mp.outbound } +func (mp mockPeer) IsPersistent() bool { return mp.persistent } +func (mp mockPeer) NodeInfo() p2p.NodeInfo { + return p2p.NodeInfo{ + PubKey: mp.pubKey, + ListenAddr: mp.addr.DialString(), + } +} +func (mp mockPeer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } +func (mp mockPeer) Send(byte, interface{}) bool { return false } +func (mp mockPeer) TrySend(byte, interface{}) bool { return false } +func (mp mockPeer) Set(string, interface{}) {} +func (mp mockPeer) Get(string) interface{} { return nil } diff --git a/p2p/pex_reactor.go b/p2p/pex_reactor.go deleted file mode 100644 index 2bfe7dcab..000000000 --- a/p2p/pex_reactor.go +++ /dev/null @@ -1,356 +0,0 @@ -package p2p - -import ( - "bytes" - "fmt" - "math/rand" - "reflect" - "time" - - wire "github.com/tendermint/go-wire" - cmn "github.com/tendermint/tmlibs/common" -) - -const ( - // PexChannel is a channel for PEX messages - PexChannel = byte(0x00) - - // period to ensure peers connected - defaultEnsurePeersPeriod = 30 * time.Second - minNumOutboundPeers = 10 - maxPexMessageSize = 1048576 // 1MB - - // maximum pex messages one peer can send to us during `msgCountByPeerFlushInterval` - defaultMaxMsgCountByPeer = 1000 - msgCountByPeerFlushInterval = 1 * time.Hour -) - -// PEXReactor handles PEX (peer exchange) and ensures that an -// adequate number of peers are connected to the switch. -// -// It uses `AddrBook` (address book) to store `NetAddress`es of the peers. -// -// ## Preventing abuse -// -// For now, it just limits the number of messages from one peer to -// `defaultMaxMsgCountByPeer` messages per `msgCountByPeerFlushInterval` (1000 -// msg/hour). -// -// NOTE [2017-01-17]: -// Limiting is fine for now. Maybe down the road we want to keep track of the -// quality of peer messages so if peerA keeps telling us about peers we can't -// connect to then maybe we should care less about peerA. But I don't think -// that kind of complexity is priority right now. -type PEXReactor struct { - BaseReactor - - book *AddrBook - ensurePeersPeriod time.Duration - - // tracks message count by peer, so we can prevent abuse - msgCountByPeer *cmn.CMap - maxMsgCountByPeer uint16 -} - -// NewPEXReactor creates new PEX reactor. -func NewPEXReactor(b *AddrBook) *PEXReactor { - r := &PEXReactor{ - book: b, - ensurePeersPeriod: defaultEnsurePeersPeriod, - msgCountByPeer: cmn.NewCMap(), - maxMsgCountByPeer: defaultMaxMsgCountByPeer, - } - r.BaseReactor = *NewBaseReactor("PEXReactor", r) - return r -} - -// OnStart implements BaseService -func (r *PEXReactor) OnStart() error { - if err := r.BaseReactor.OnStart(); err != nil { - return err - } - err := r.book.Start() - if err != nil && err != cmn.ErrAlreadyStarted { - return err - } - go r.ensurePeersRoutine() - go r.flushMsgCountByPeer() - return nil -} - -// OnStop implements BaseService -func (r *PEXReactor) OnStop() { - r.BaseReactor.OnStop() - r.book.Stop() -} - -// GetChannels implements Reactor -func (r *PEXReactor) GetChannels() []*ChannelDescriptor { - return []*ChannelDescriptor{ - { - ID: PexChannel, - Priority: 1, - SendQueueCapacity: 10, - }, - } -} - -// AddPeer implements Reactor by adding peer to the address book (if inbound) -// or by requesting more addresses (if outbound). -func (r *PEXReactor) AddPeer(p Peer) { - if p.IsOutbound() { - // For outbound peers, the address is already in the books. - // Either it was added in DialSeeds or when we - // received the peer's address in r.Receive - if r.book.NeedMoreAddrs() { - r.RequestPEX(p) - } - } else { // For inbound connections, the peer is its own source - addr, err := NewNetAddressString(p.NodeInfo().ListenAddr) - if err != nil { - // peer gave us a bad ListenAddr. TODO: punish - r.Logger.Error("Error in AddPeer: invalid peer address", "addr", p.NodeInfo().ListenAddr, "err", err) - return - } - r.book.AddAddress(addr, addr) - } -} - -// RemovePeer implements Reactor. -func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) { - // If we aren't keeping track of local temp data for each peer here, then we - // don't have to do anything. -} - -// Receive implements Reactor by handling incoming PEX messages. -func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) { - srcAddrStr := src.NodeInfo().RemoteAddr - srcAddr, err := NewNetAddressString(srcAddrStr) - if err != nil { - // this should never happen. TODO: cancel conn - r.Logger.Error("Error in Receive: invalid peer address", "addr", srcAddrStr, "err", err) - return - } - - r.IncrementMsgCountForPeer(srcAddrStr) - if r.ReachedMaxMsgCountForPeer(srcAddrStr) { - r.Logger.Error("Maximum number of messages reached for peer", "peer", srcAddrStr) - // TODO remove src from peers? - return - } - - _, msg, err := DecodeMessage(msgBytes) - if err != nil { - r.Logger.Error("Error decoding message", "err", err) - return - } - r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg) - - switch msg := msg.(type) { - case *pexRequestMessage: - // src requested some peers. - // NOTE: we might send an empty selection - r.SendAddrs(src, r.book.GetSelection()) - case *pexAddrsMessage: - // We received some peer addresses from src. - // TODO: (We don't want to get spammed with bad peers) - for _, addr := range msg.Addrs { - if addr != nil { - r.book.AddAddress(addr, srcAddr) - } - } - default: - r.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) - } -} - -// RequestPEX asks peer for more addresses. -func (r *PEXReactor) RequestPEX(p Peer) { - p.Send(PexChannel, struct{ PexMessage }{&pexRequestMessage{}}) -} - -// SendAddrs sends addrs to the peer. -func (r *PEXReactor) SendAddrs(p Peer, addrs []*NetAddress) { - p.Send(PexChannel, struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) -} - -// SetEnsurePeersPeriod sets period to ensure peers connected. -func (r *PEXReactor) SetEnsurePeersPeriod(d time.Duration) { - r.ensurePeersPeriod = d -} - -// SetMaxMsgCountByPeer sets maximum messages one peer can send to us during 'msgCountByPeerFlushInterval'. -func (r *PEXReactor) SetMaxMsgCountByPeer(v uint16) { - r.maxMsgCountByPeer = v -} - -// ReachedMaxMsgCountForPeer returns true if we received too many -// messages from peer with address `addr`. -// NOTE: assumes the value in the CMap is non-nil -func (r *PEXReactor) ReachedMaxMsgCountForPeer(addr string) bool { - return r.msgCountByPeer.Get(addr).(uint16) >= r.maxMsgCountByPeer -} - -// Increment or initialize the msg count for the peer in the CMap -func (r *PEXReactor) IncrementMsgCountForPeer(addr string) { - var count uint16 - countI := r.msgCountByPeer.Get(addr) - if countI != nil { - count = countI.(uint16) - } - count++ - r.msgCountByPeer.Set(addr, count) -} - -// Ensures that sufficient peers are connected. (continuous) -func (r *PEXReactor) ensurePeersRoutine() { - // Randomize when routine starts - ensurePeersPeriodMs := r.ensurePeersPeriod.Nanoseconds() / 1e6 - time.Sleep(time.Duration(rand.Int63n(ensurePeersPeriodMs)) * time.Millisecond) - - // fire once immediately. - r.ensurePeers() - - // fire periodically - ticker := time.NewTicker(r.ensurePeersPeriod) - - for { - select { - case <-ticker.C: - r.ensurePeers() - case <-r.Quit: - ticker.Stop() - return - } - } -} - -// ensurePeers ensures that sufficient peers are connected. (once) -// -// Old bucket / New bucket are arbitrary categories to denote whether an -// address is vetted or not, and this needs to be determined over time via a -// heuristic that we haven't perfected yet, or, perhaps is manually edited by -// the node operator. It should not be used to compute what addresses are -// already connected or not. -// -// TODO Basically, we need to work harder on our good-peer/bad-peer marking. -// What we're currently doing in terms of marking good/bad peers is just a -// placeholder. It should not be the case that an address becomes old/vetted -// upon a single successful connection. -func (r *PEXReactor) ensurePeers() { - numOutPeers, _, numDialing := r.Switch.NumPeers() - numToDial := minNumOutboundPeers - (numOutPeers + numDialing) - r.Logger.Info("Ensure peers", "numOutPeers", numOutPeers, "numDialing", numDialing, "numToDial", numToDial) - if numToDial <= 0 { - return - } - - // bias to prefer more vetted peers when we have fewer connections. - // not perfect, but somewhate ensures that we prioritize connecting to more-vetted - // NOTE: range here is [10, 90]. Too high ? - newBias := cmn.MinInt(numOutPeers, 8)*10 + 10 - - toDial := make(map[string]*NetAddress) - // Try maxAttempts times to pick numToDial addresses to dial - maxAttempts := numToDial * 3 - for i := 0; i < maxAttempts && len(toDial) < numToDial; i++ { - try := r.book.PickAddress(newBias) - if try == nil { - continue - } - if _, selected := toDial[try.IP.String()]; selected { - continue - } - if dialling := r.Switch.IsDialing(try); dialling { - continue - } - // XXX: Should probably use pubkey as peer key ... - if connected := r.Switch.Peers().Has(try.String()); connected { - continue - } - r.Logger.Info("Will dial address", "addr", try) - toDial[try.IP.String()] = try - } - - // Dial picked addresses - for _, item := range toDial { - go func(picked *NetAddress) { - _, err := r.Switch.DialPeerWithAddress(picked, false) - if err != nil { - r.book.MarkAttempt(picked) - } - }(item) - } - - // If we need more addresses, pick a random peer and ask for more. - if r.book.NeedMoreAddrs() { - if peers := r.Switch.Peers().List(); len(peers) > 0 { - i := rand.Int() % len(peers) // nolint: gas - peer := peers[i] - r.Logger.Info("No addresses to dial. Sending pexRequest to random peer", "peer", peer) - r.RequestPEX(peer) - } - } -} - -func (r *PEXReactor) flushMsgCountByPeer() { - ticker := time.NewTicker(msgCountByPeerFlushInterval) - - for { - select { - case <-ticker.C: - r.msgCountByPeer.Clear() - case <-r.Quit: - ticker.Stop() - return - } - } -} - -//----------------------------------------------------------------------------- -// Messages - -const ( - msgTypeRequest = byte(0x01) - msgTypeAddrs = byte(0x02) -) - -// PexMessage is a primary type for PEX messages. Underneath, it could contain -// either pexRequestMessage, or pexAddrsMessage messages. -type PexMessage interface{} - -var _ = wire.RegisterInterface( - struct{ PexMessage }{}, - wire.ConcreteType{&pexRequestMessage{}, msgTypeRequest}, - wire.ConcreteType{&pexAddrsMessage{}, msgTypeAddrs}, -) - -// DecodeMessage implements interface registered above. -func DecodeMessage(bz []byte) (msgType byte, msg PexMessage, err error) { - msgType = bz[0] - n := new(int) - r := bytes.NewReader(bz) - msg = wire.ReadBinary(struct{ PexMessage }{}, r, maxPexMessageSize, n, &err).(struct{ PexMessage }).PexMessage - return -} - -/* -A pexRequestMessage requests additional peer addresses. -*/ -type pexRequestMessage struct { -} - -func (m *pexRequestMessage) String() string { - return "[pexRequest]" -} - -/* -A message with announced peer addresses. -*/ -type pexAddrsMessage struct { - Addrs []*NetAddress -} - -func (m *pexAddrsMessage) String() string { - return fmt.Sprintf("[pexAddrs %v]", m.Addrs) -} diff --git a/p2p/pex_reactor_test.go b/p2p/pex_reactor_test.go deleted file mode 100644 index a14f0eb2a..000000000 --- a/p2p/pex_reactor_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package p2p - -import ( - "fmt" - "io/ioutil" - "math/rand" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - wire "github.com/tendermint/go-wire" - cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/log" -) - -func TestPEXReactorBasic(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book) - r.SetLogger(log.TestingLogger()) - - assert.NotNil(r) - assert.NotEmpty(r.GetChannels()) -} - -func TestPEXReactorAddRemovePeer(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book) - r.SetLogger(log.TestingLogger()) - - size := book.Size() - peer := createRandomPeer(false) - - r.AddPeer(peer) - assert.Equal(size+1, book.Size()) - - r.RemovePeer(peer, "peer not available") - assert.Equal(size+1, book.Size()) - - outboundPeer := createRandomPeer(true) - - r.AddPeer(outboundPeer) - assert.Equal(size+1, book.Size(), "outbound peers should not be added to the address book") - - r.RemovePeer(outboundPeer, "peer not available") - assert.Equal(size+1, book.Size()) -} - -func TestPEXReactorRunning(t *testing.T) { - N := 3 - switches := make([]*Switch, N) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(t, err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) - book.SetLogger(log.TestingLogger()) - - // create switches - for i := 0; i < N; i++ { - switches[i] = makeSwitch(config, i, "127.0.0.1", "123.123.123", func(i int, sw *Switch) *Switch { - sw.SetLogger(log.TestingLogger().With("switch", i)) - - r := NewPEXReactor(book) - r.SetLogger(log.TestingLogger()) - r.SetEnsurePeersPeriod(250 * time.Millisecond) - sw.AddReactor("pex", r) - return sw - }) - } - - // fill the address book and add listeners - for _, s := range switches { - addr, _ := NewNetAddressString(s.NodeInfo().ListenAddr) - book.AddAddress(addr, addr) - s.AddListener(NewDefaultListener("tcp", s.NodeInfo().ListenAddr, true, log.TestingLogger())) - } - - // start switches - for _, s := range switches { - err := s.Start() // start switch and reactors - require.Nil(t, err) - } - - assertSomePeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second) - - // stop them - for _, s := range switches { - s.Stop() - } -} - -func assertSomePeersWithTimeout(t *testing.T, switches []*Switch, checkPeriod, timeout time.Duration) { - ticker := time.NewTicker(checkPeriod) - for { - select { - case <-ticker.C: - // check peers are connected - allGood := true - for _, s := range switches { - outbound, inbound, _ := s.NumPeers() - if outbound+inbound == 0 { - allGood = false - } - } - if allGood { - return - } - case <-time.After(timeout): - numPeersStr := "" - for i, s := range switches { - outbound, inbound, _ := s.NumPeers() - numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound) - } - t.Errorf("expected all switches to be connected to at least one peer (switches: %s)", numPeersStr) - } - } -} - -func TestPEXReactorReceive(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", false) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book) - r.SetLogger(log.TestingLogger()) - - peer := createRandomPeer(false) - - size := book.Size() - netAddr, _ := NewNetAddressString(peer.NodeInfo().ListenAddr) - addrs := []*NetAddress{netAddr} - msg := wire.BinaryBytes(struct{ PexMessage }{&pexAddrsMessage{Addrs: addrs}}) - r.Receive(PexChannel, peer, msg) - assert.Equal(size+1, book.Size()) - - msg = wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) - r.Receive(PexChannel, peer, msg) -} - -func TestPEXReactorAbuseFromPeer(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - dir, err := ioutil.TempDir("", "pex_reactor") - require.Nil(err) - defer os.RemoveAll(dir) // nolint: errcheck - book := NewAddrBook(dir+"addrbook.json", true) - book.SetLogger(log.TestingLogger()) - - r := NewPEXReactor(book) - r.SetLogger(log.TestingLogger()) - r.SetMaxMsgCountByPeer(5) - - peer := createRandomPeer(false) - - msg := wire.BinaryBytes(struct{ PexMessage }{&pexRequestMessage{}}) - for i := 0; i < 10; i++ { - r.Receive(PexChannel, peer, msg) - } - - assert.True(r.ReachedMaxMsgCountForPeer(peer.NodeInfo().ListenAddr)) -} - -func createRoutableAddr() (addr string, netAddr *NetAddress) { - for { - addr = cmn.Fmt("%v.%v.%v.%v:46656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256) - netAddr, _ = NewNetAddressString(addr) - if netAddr.Routable() { - break - } - } - return -} - -func createRandomPeer(outbound bool) *peer { - addr, netAddr := createRoutableAddr() - p := &peer{ - nodeInfo: &NodeInfo{ - ListenAddr: addr, - RemoteAddr: netAddr.String(), - }, - outbound: outbound, - mconn: &MConnection{RemoteAddress: netAddr}, - } - p.SetLogger(log.TestingLogger().With("peer", addr)) - return p -} diff --git a/p2p/switch.go b/p2p/switch.go index 76b019806..f1d02dcfc 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -5,18 +5,20 @@ import ( "math" "math/rand" "net" + "sync" "time" "github.com/pkg/errors" crypto "github.com/tendermint/go-crypto" cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/p2p/conn" cmn "github.com/tendermint/tmlibs/common" ) const ( // wait a random amount of time from this interval - // before dialing seeds or reconnecting to help prevent DoS + // before dialing peers or reconnecting to help prevent DoS dialRandomizerIntervalMilliseconds = 3000 // repeatedly try to reconnect for a few minutes @@ -30,46 +32,19 @@ const ( reconnectBackOffBaseSeconds = 3 ) -type Reactor interface { - cmn.Service // Start, Stop - - SetSwitch(*Switch) - GetChannels() []*ChannelDescriptor - AddPeer(peer Peer) - RemovePeer(peer Peer, reason interface{}) - Receive(chID byte, peer Peer, msgBytes []byte) // CONTRACT: msgBytes are not nil -} - -//-------------------------------------- - -type BaseReactor struct { - cmn.BaseService // Provides Start, Stop, .Quit - Switch *Switch -} - -func NewBaseReactor(name string, impl Reactor) *BaseReactor { - return &BaseReactor{ - BaseService: *cmn.NewBaseService(nil, name, impl), - Switch: nil, - } -} +//----------------------------------------------------------------------------- -func (br *BaseReactor) SetSwitch(sw *Switch) { - br.Switch = sw +type AddrBook interface { + AddAddress(addr *NetAddress, src *NetAddress) error + Save() } -func (_ *BaseReactor) GetChannels() []*ChannelDescriptor { return nil } -func (_ *BaseReactor) AddPeer(peer Peer) {} -func (_ *BaseReactor) RemovePeer(peer Peer, reason interface{}) {} -func (_ *BaseReactor) Receive(chID byte, peer Peer, msgBytes []byte) {} //----------------------------------------------------------------------------- -/* -The `Switch` handles peer connections and exposes an API to receive incoming messages -on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one -or more `Channels`. So while sending outgoing messages is typically performed on the peer, -incoming messages are received on the reactor. -*/ +// `Switch` handles peer connections and exposes an API to receive incoming messages +// on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one +// or more `Channels`. So while sending outgoing messages is typically performed on the peer, +// incoming messages are received on the reactor. type Switch struct { cmn.BaseService @@ -77,33 +52,28 @@ type Switch struct { peerConfig *PeerConfig listeners []Listener reactors map[string]Reactor - chDescs []*ChannelDescriptor + chDescs []*conn.ChannelDescriptor reactorsByCh map[byte]Reactor peers *PeerSet dialing *cmn.CMap - nodeInfo *NodeInfo // our node info - nodePrivKey crypto.PrivKeyEd25519 // our node privkey + nodeInfo NodeInfo // our node info + nodeKey *NodeKey // our node privkey filterConnByAddr func(net.Addr) error - filterConnByPubKey func(crypto.PubKeyEd25519) error + filterConnByPubKey func(crypto.PubKey) error rng *rand.Rand // seed for randomizing dial times and orders } -var ( - ErrSwitchDuplicatePeer = errors.New("Duplicate peer") -) - func NewSwitch(config *cfg.P2PConfig) *Switch { sw := &Switch{ config: config, peerConfig: DefaultPeerConfig(), reactors: make(map[string]Reactor), - chDescs: make([]*ChannelDescriptor, 0), + chDescs: make([]*conn.ChannelDescriptor, 0), reactorsByCh: make(map[byte]Reactor), peers: NewPeerSet(), dialing: cmn.NewCMap(), - nodeInfo: nil, } // Ensure we have a completely undeterministic PRNG. cmd.RandInt64() draws @@ -111,15 +81,18 @@ func NewSwitch(config *cfg.P2PConfig) *Switch { sw.rng = rand.New(rand.NewSource(cmn.RandInt64())) // TODO: collapse the peerConfig into the config ? - sw.peerConfig.MConfig.flushThrottle = time.Duration(config.FlushThrottleTimeout) * time.Millisecond + sw.peerConfig.MConfig.FlushThrottle = time.Duration(config.FlushThrottleTimeout) * time.Millisecond sw.peerConfig.MConfig.SendRate = config.SendRate sw.peerConfig.MConfig.RecvRate = config.RecvRate - sw.peerConfig.MConfig.maxMsgPacketPayloadSize = config.MaxMsgPacketPayloadSize + sw.peerConfig.MConfig.MaxMsgPacketPayloadSize = config.MaxMsgPacketPayloadSize sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw) return sw } +//--------------------------------------------------------------------- +// Switch setup + // AddReactor adds the given reactor to the switch. // NOTE: Not goroutine safe. func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { @@ -171,26 +144,25 @@ func (sw *Switch) IsListening() bool { // SetNodeInfo sets the switch's NodeInfo for checking compatibility and handshaking with other nodes. // NOTE: Not goroutine safe. -func (sw *Switch) SetNodeInfo(nodeInfo *NodeInfo) { +func (sw *Switch) SetNodeInfo(nodeInfo NodeInfo) { sw.nodeInfo = nodeInfo } // NodeInfo returns the switch's NodeInfo. // NOTE: Not goroutine safe. -func (sw *Switch) NodeInfo() *NodeInfo { +func (sw *Switch) NodeInfo() NodeInfo { return sw.nodeInfo } -// SetNodePrivKey sets the switch's private key for authenticated encryption. -// NOTE: Overwrites sw.nodeInfo.PubKey. +// SetNodeKey sets the switch's private key for authenticated encryption. // NOTE: Not goroutine safe. -func (sw *Switch) SetNodePrivKey(nodePrivKey crypto.PrivKeyEd25519) { - sw.nodePrivKey = nodePrivKey - if sw.nodeInfo != nil { - sw.nodeInfo.PubKey = nodePrivKey.PubKey().Unwrap().(crypto.PubKeyEd25519) - } +func (sw *Switch) SetNodeKey(nodeKey *NodeKey) { + sw.nodeKey = nodeKey } +//--------------------------------------------------------------------- +// Service start/stop + // OnStart implements BaseService. It starts all the reactors, peers, and listeners. func (sw *Switch) OnStart() error { // Start reactors @@ -226,188 +198,31 @@ func (sw *Switch) OnStop() { } } -// addPeer checks the given peer's validity, performs a handshake, and adds the -// peer to the switch and to all registered reactors. -// NOTE: This performs a blocking handshake before the peer is added. -// NOTE: If error is returned, caller is responsible for calling peer.CloseConn() -func (sw *Switch) addPeer(peer *peer) error { - - if err := sw.FilterConnByAddr(peer.Addr()); err != nil { - return err - } - - if err := sw.FilterConnByPubKey(peer.PubKey()); err != nil { - return err - } - - if err := peer.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.peerConfig.HandshakeTimeout*time.Second)); err != nil { - return err - } - - // Avoid self - if sw.nodeInfo.PubKey.Equals(peer.PubKey().Wrap()) { - return errors.New("Ignoring connection from self") - } - - // Check version, chain id - if err := sw.nodeInfo.CompatibleWith(peer.NodeInfo()); err != nil { - return err - } - - // Check for duplicate peer - if sw.peers.Has(peer.Key()) { - return ErrSwitchDuplicatePeer - - } - - // Start peer - if sw.IsRunning() { - sw.startInitPeer(peer) - } - - // Add the peer to .peers. - // We start it first so that a peer in the list is safe to Stop. - // It should not err since we already checked peers.Has(). - if err := sw.peers.Add(peer); err != nil { - return err - } - - sw.Logger.Info("Added peer", "peer", peer) - return nil -} - -// FilterConnByAddr returns an error if connecting to the given address is forbidden. -func (sw *Switch) FilterConnByAddr(addr net.Addr) error { - if sw.filterConnByAddr != nil { - return sw.filterConnByAddr(addr) - } - return nil -} - -// FilterConnByPubKey returns an error if connecting to the given public key is forbidden. -func (sw *Switch) FilterConnByPubKey(pubkey crypto.PubKeyEd25519) error { - if sw.filterConnByPubKey != nil { - return sw.filterConnByPubKey(pubkey) - } - return nil - -} +//--------------------------------------------------------------------- +// Peers -// SetAddrFilter sets the function for filtering connections by address. -func (sw *Switch) SetAddrFilter(f func(net.Addr) error) { - sw.filterConnByAddr = f -} - -// SetPubKeyFilter sets the function for filtering connections by public key. -func (sw *Switch) SetPubKeyFilter(f func(crypto.PubKeyEd25519) error) { - sw.filterConnByPubKey = f -} - -func (sw *Switch) startInitPeer(peer *peer) { - err := peer.Start() // spawn send/recv routines - if err != nil { - // Should never happen - sw.Logger.Error("Error starting peer", "peer", peer, "err", err) - } - - for _, reactor := range sw.reactors { - reactor.AddPeer(peer) - } -} - -// DialSeeds dials a list of seeds asynchronously in random order. -func (sw *Switch) DialSeeds(addrBook *AddrBook, seeds []string) error { - netAddrs, errs := NewNetAddressStrings(seeds) - for _, err := range errs { - sw.Logger.Error("Error in seed's address", "err", err) - } - - if addrBook != nil { - // add seeds to `addrBook` - ourAddrS := sw.nodeInfo.ListenAddr - ourAddr, _ := NewNetAddressString(ourAddrS) - for _, netAddr := range netAddrs { - // do not add ourselves - if netAddr.Equals(ourAddr) { - continue - } - addrBook.AddAddress(netAddr, ourAddr) - } - addrBook.Save() - } - - // permute the list, dial them in random order. - perm := sw.rng.Perm(len(netAddrs)) - for i := 0; i < len(perm); i++ { - go func(i int) { - sw.randomSleep(0) - j := perm[i] - sw.dialSeed(netAddrs[j]) - }(i) - } - return nil -} - -// sleep for interval plus some random amount of ms on [0, dialRandomizerIntervalMilliseconds] -func (sw *Switch) randomSleep(interval time.Duration) { - r := time.Duration(sw.rng.Int63n(dialRandomizerIntervalMilliseconds)) * time.Millisecond - time.Sleep(r + interval) -} - -func (sw *Switch) dialSeed(addr *NetAddress) { - peer, err := sw.DialPeerWithAddress(addr, true) - if err != nil { - sw.Logger.Error("Error dialing seed", "err", err) - } else { - sw.Logger.Info("Connected to seed", "peer", peer) - } -} - -// DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects successfully. -// If `persistent == true`, the switch will always try to reconnect to this peer if the connection ever fails. -func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (Peer, error) { - sw.dialing.Set(addr.IP.String(), addr) - defer sw.dialing.Delete(addr.IP.String()) - - sw.Logger.Info("Dialing peer", "address", addr) - peer, err := newOutboundPeer(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, sw.peerConfig) - if err != nil { - sw.Logger.Error("Failed to dial peer", "address", addr, "err", err) - return nil, err - } - peer.SetLogger(sw.Logger.With("peer", addr)) - if persistent { - peer.makePersistent() - } - err = sw.addPeer(peer) - if err != nil { - sw.Logger.Error("Failed to add peer", "address", addr, "err", err) - peer.CloseConn() - return nil, err - } - sw.Logger.Info("Dialed and added peer", "address", addr, "peer", peer) - return peer, nil -} - -// IsDialing returns true if the switch is currently dialing the given address. -func (sw *Switch) IsDialing(addr *NetAddress) bool { - return sw.dialing.Has(addr.IP.String()) -} - -// Broadcast runs a go routine for each attempted send, which will block -// trying to send for defaultSendTimeoutSeconds. Returns a channel -// which receives success values for each attempted send (false if times out). +// Broadcast runs a go routine for each attempted send, which will block trying +// to send for defaultSendTimeoutSeconds. Returns a channel which receives +// success values for each attempted send (false if times out). Channel will be +// closed once msg send to all peers. +// // NOTE: Broadcast uses goroutines, so order of broadcast may not be preserved. -// TODO: Something more intelligent. func (sw *Switch) Broadcast(chID byte, msg interface{}) chan bool { successChan := make(chan bool, len(sw.peers.List())) sw.Logger.Debug("Broadcast", "channel", chID, "msg", msg) + var wg sync.WaitGroup for _, peer := range sw.peers.List() { + wg.Add(1) go func(peer Peer) { + defer wg.Done() success := peer.Send(chID, msg) successChan <- success }(peer) } + go func() { + wg.Wait() + close(successChan) + }() return successChan } @@ -442,12 +257,29 @@ func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) { } } +// StopPeerGracefully disconnects from a peer gracefully. +// TODO: handle graceful disconnects. +func (sw *Switch) StopPeerGracefully(peer Peer) { + sw.Logger.Info("Stopping peer gracefully") + sw.stopAndRemovePeer(peer, nil) +} + +func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) { + sw.peers.Remove(peer) + peer.Stop() + for _, reactor := range sw.reactors { + reactor.RemovePeer(peer, reason) + } +} + // reconnectToPeer tries to reconnect to the peer, first repeatedly // with a fixed interval, then with exponential backoff. // If no success after all that, it stops trying, and leaves it // to the PEX/Addrbook to find the peer again func (sw *Switch) reconnectToPeer(peer Peer) { - addr, _ := NewNetAddressString(peer.NodeInfo().RemoteAddr) + // NOTE this will connect to the self reported address, + // not necessarily the original we dialed + netAddr := peer.NodeInfo().NetAddress() start := time.Now() sw.Logger.Info("Reconnecting to peer", "peer", peer) for i := 0; i < reconnectAttempts; i++ { @@ -455,7 +287,7 @@ func (sw *Switch) reconnectToPeer(peer Peer) { return } - peer, err := sw.DialPeerWithAddress(addr, true) + peer, err := sw.DialPeerWithAddress(netAddr, true) if err != nil { sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "peer", peer) // sleep a set amount @@ -477,7 +309,7 @@ func (sw *Switch) reconnectToPeer(peer Peer) { // sleep an exponentially increasing amount sleepIntervalSeconds := math.Pow(reconnectBackOffBaseSeconds, float64(i)) sw.randomSleep(time.Duration(sleepIntervalSeconds) * time.Second) - peer, err := sw.DialPeerWithAddress(addr, true) + peer, err := sw.DialPeerWithAddress(netAddr, true) if err != nil { sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "peer", peer) continue @@ -489,21 +321,100 @@ func (sw *Switch) reconnectToPeer(peer Peer) { sw.Logger.Error("Failed to reconnect to peer. Giving up", "peer", peer, "elapsed", time.Since(start)) } -// StopPeerGracefully disconnects from a peer gracefully. -// TODO: handle graceful disconnects. -func (sw *Switch) StopPeerGracefully(peer Peer) { - sw.Logger.Info("Stopping peer gracefully") - sw.stopAndRemovePeer(peer, nil) +//--------------------------------------------------------------------- +// Dialing + +// IsDialing returns true if the switch is currently dialing the given ID. +func (sw *Switch) IsDialing(id ID) bool { + return sw.dialing.Has(string(id)) } -func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) { - sw.peers.Remove(peer) - peer.Stop() - for _, reactor := range sw.reactors { - reactor.RemovePeer(peer, reason) +// DialPeersAsync dials a list of peers asynchronously in random order (optionally, making them persistent). +func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent bool) error { + netAddrs, errs := NewNetAddressStrings(peers) + for _, err := range errs { + sw.Logger.Error("Error in peer's address", "err", err) } + + if addrBook != nil { + // add peers to `addrBook` + ourAddr := sw.nodeInfo.NetAddress() + for _, netAddr := range netAddrs { + // do not add our address or ID + if netAddr.Same(ourAddr) { + continue + } + // TODO: move this out of here ? + addrBook.AddAddress(netAddr, ourAddr) + } + // Persist some peers to disk right away. + // NOTE: integration tests depend on this + addrBook.Save() + } + + // permute the list, dial them in random order. + perm := sw.rng.Perm(len(netAddrs)) + for i := 0; i < len(perm); i++ { + go func(i int) { + sw.randomSleep(0) + j := perm[i] + peer, err := sw.DialPeerWithAddress(netAddrs[j], persistent) + if err != nil { + sw.Logger.Error("Error dialing peer", "err", err) + } else { + sw.Logger.Info("Connected to peer", "peer", peer) + } + }(i) + } + return nil +} + +// DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects and authenticates successfully. +// If `persistent == true`, the switch will always try to reconnect to this peer if the connection ever fails. +func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) (Peer, error) { + sw.dialing.Set(string(addr.ID), addr) + defer sw.dialing.Delete(string(addr.ID)) + return sw.addOutboundPeerWithConfig(addr, sw.peerConfig, persistent) } +// sleep for interval plus some random amount of ms on [0, dialRandomizerIntervalMilliseconds] +func (sw *Switch) randomSleep(interval time.Duration) { + r := time.Duration(sw.rng.Int63n(dialRandomizerIntervalMilliseconds)) * time.Millisecond + time.Sleep(r + interval) +} + +//------------------------------------------------------------------------------------ +// Connection filtering + +// FilterConnByAddr returns an error if connecting to the given address is forbidden. +func (sw *Switch) FilterConnByAddr(addr net.Addr) error { + if sw.filterConnByAddr != nil { + return sw.filterConnByAddr(addr) + } + return nil +} + +// FilterConnByPubKey returns an error if connecting to the given public key is forbidden. +func (sw *Switch) FilterConnByPubKey(pubkey crypto.PubKey) error { + if sw.filterConnByPubKey != nil { + return sw.filterConnByPubKey(pubkey) + } + return nil + +} + +// SetAddrFilter sets the function for filtering connections by address. +func (sw *Switch) SetAddrFilter(f func(net.Addr) error) { + sw.filterConnByAddr = f +} + +// SetPubKeyFilter sets the function for filtering connections by public key. +func (sw *Switch) SetPubKeyFilter(f func(crypto.PubKey) error) { + sw.filterConnByPubKey = f +} + +//------------------------------------------------------------------------------------ + func (sw *Switch) listenerRoutine(l Listener) { for { inConn, ok := <-l.Connections() @@ -519,131 +430,124 @@ func (sw *Switch) listenerRoutine(l Listener) { } // New inbound connection! - err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig) + err := sw.addInboundPeerWithConfig(inConn, sw.peerConfig) if err != nil { sw.Logger.Info("Ignoring inbound connection: error while adding peer", "address", inConn.RemoteAddr().String(), "err", err) continue } - - // NOTE: We don't yet have the listening port of the - // remote (if they have a listener at all). - // The peerHandshake will handle that. } // cleanup } -//------------------------------------------------------------------ -// Connects switches via arbitrary net.Conn. Used for testing. - -// MakeConnectedSwitches returns n switches, connected according to the connect func. -// If connect==Connect2Switches, the switches will be fully connected. -// initSwitch defines how the i'th switch should be initialized (ie. with what reactors). -// NOTE: panics if any switch fails to start. -func MakeConnectedSwitches(cfg *cfg.P2PConfig, n int, initSwitch func(int, *Switch) *Switch, connect func([]*Switch, int, int)) []*Switch { - switches := make([]*Switch, n) - for i := 0; i < n; i++ { - switches[i] = makeSwitch(cfg, i, "testing", "123.123.123", initSwitch) +func (sw *Switch) addInboundPeerWithConfig(conn net.Conn, config *PeerConfig) error { + peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, config) + if err != nil { + conn.Close() // peer is nil + return err + } + peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) + if err = sw.addPeer(peer); err != nil { + peer.CloseConn() + return err } - if err := StartSwitches(switches); err != nil { - panic(err) + return nil +} + +// dial the peer; make secret connection; authenticate against the dialed ID; +// add the peer. +func (sw *Switch) addOutboundPeerWithConfig(addr *NetAddress, config *PeerConfig, persistent bool) (Peer, error) { + sw.Logger.Info("Dialing peer", "address", addr) + peer, err := newOutboundPeer(addr, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, config, persistent) + if err != nil { + sw.Logger.Error("Failed to dial peer", "address", addr, "err", err) + return nil, err } + peer.SetLogger(sw.Logger.With("peer", addr)) - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - connect(switches, i, j) - } + // authenticate peer + if addr.ID == "" { + peer.Logger.Info("Dialed peer with unknown ID - unable to authenticate", "addr", addr) + } else if addr.ID != peer.ID() { + peer.CloseConn() + return nil, ErrSwitchAuthenticationFailure{addr, peer.ID()} } - return switches + err = sw.addPeer(peer) + if err != nil { + sw.Logger.Error("Failed to add peer", "address", addr, "err", err) + peer.CloseConn() + return nil, err + } + sw.Logger.Info("Dialed and added peer", "address", addr, "peer", peer) + return peer, nil } -// Connect2Switches will connect switches i and j via net.Pipe(). -// Blocks until a connection is established. -// NOTE: caller ensures i and j are within bounds. -func Connect2Switches(switches []*Switch, i, j int) { - switchI := switches[i] - switchJ := switches[j] - c1, c2 := netPipe() - doneCh := make(chan struct{}) - go func() { - err := switchI.addPeerWithConnection(c1) - if err != nil { - panic(err) - } - doneCh <- struct{}{} - }() - go func() { - err := switchJ.addPeerWithConnection(c2) - if err != nil { - panic(err) - } - doneCh <- struct{}{} - }() - <-doneCh - <-doneCh -} +// addPeer performs the Tendermint P2P handshake with a peer +// that already has a SecretConnection. If all goes well, +// it starts the peer and adds it to the switch. +// NOTE: This performs a blocking handshake before the peer is added. +// NOTE: If error is returned, caller is responsible for calling peer.CloseConn() +func (sw *Switch) addPeer(peer *peer) error { + // Avoid self + if sw.nodeKey.ID() == peer.ID() { + return ErrSwitchConnectToSelf + } + + // Avoid duplicate + if sw.peers.Has(peer.ID()) { + return ErrSwitchDuplicatePeer -// StartSwitches calls sw.Start() for each given switch. -// It returns the first encountered error. -func StartSwitches(switches []*Switch) error { - for _, s := range switches { - err := s.Start() // start switch and reactors - if err != nil { - return err - } } - return nil -} -func makeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { - privKey := crypto.GenPrivKeyEd25519() - // new switch, add reactors - // TODO: let the config be passed in? - s := initSwitch(i, NewSwitch(cfg)) - s.SetNodeInfo(&NodeInfo{ - PubKey: privKey.PubKey().Unwrap().(crypto.PubKeyEd25519), - Moniker: cmn.Fmt("switch%d", i), - Network: network, - Version: version, - RemoteAddr: cmn.Fmt("%v:%v", network, rand.Intn(64512)+1023), - ListenAddr: cmn.Fmt("%v:%v", network, rand.Intn(64512)+1023), - }) - s.SetNodePrivKey(privKey) - return s -} - -func (sw *Switch) addPeerWithConnection(conn net.Conn) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, sw.peerConfig) - if err != nil { - if err := conn.Close(); err != nil { - sw.Logger.Error("Error closing connection", "err", err) - } + // Filter peer against white list + if err := sw.FilterConnByAddr(peer.Addr()); err != nil { return err } - peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) - if err = sw.addPeer(peer); err != nil { - peer.CloseConn() + if err := sw.FilterConnByPubKey(peer.PubKey()); err != nil { return err } - return nil -} + // Exchange NodeInfo with the peer + if err := peer.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.peerConfig.HandshakeTimeout*time.Second)); err != nil { + return err + } -func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error { - peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config) - if err != nil { - if err := conn.Close(); err != nil { - sw.Logger.Error("Error closing connection", "err", err) - } + // Validate the peers nodeInfo against the pubkey + if err := peer.NodeInfo().Validate(peer.PubKey()); err != nil { return err } - peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) - if err = sw.addPeer(peer); err != nil { - peer.CloseConn() + + // Check version, chain id + if err := sw.nodeInfo.CompatibleWith(peer.NodeInfo()); err != nil { + return err + } + + // All good. Start peer + if sw.IsRunning() { + sw.startInitPeer(peer) + } + + // Add the peer to .peers. + // We start it first so that a peer in the list is safe to Stop. + // It should not err since we already checked peers.Has(). + if err := sw.peers.Add(peer); err != nil { return err } + sw.Logger.Info("Added peer", "peer", peer) return nil } + +func (sw *Switch) startInitPeer(peer *peer) { + err := peer.Start() // spawn send/recv routines + if err != nil { + // Should never happen + sw.Logger.Error("Error starting peer", "peer", peer, "err", err) + } + + for _, reactor := range sw.reactors { + reactor.AddPeer(peer) + } +} diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 72807d36a..745eb44e6 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -16,6 +16,7 @@ import ( "github.com/tendermint/tmlibs/log" cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/p2p/conn" ) var ( @@ -28,7 +29,7 @@ func init() { } type PeerMessage struct { - PeerKey string + PeerID ID Bytes []byte Counter int } @@ -37,7 +38,7 @@ type TestReactor struct { BaseReactor mtx sync.Mutex - channels []*ChannelDescriptor + channels []*conn.ChannelDescriptor peersAdded []Peer peersRemoved []Peer logMessages bool @@ -45,7 +46,7 @@ type TestReactor struct { msgsReceived map[byte][]PeerMessage } -func NewTestReactor(channels []*ChannelDescriptor, logMessages bool) *TestReactor { +func NewTestReactor(channels []*conn.ChannelDescriptor, logMessages bool) *TestReactor { tr := &TestReactor{ channels: channels, logMessages: logMessages, @@ -56,7 +57,7 @@ func NewTestReactor(channels []*ChannelDescriptor, logMessages bool) *TestReacto return tr } -func (tr *TestReactor) GetChannels() []*ChannelDescriptor { +func (tr *TestReactor) GetChannels() []*conn.ChannelDescriptor { return tr.channels } @@ -77,7 +78,7 @@ func (tr *TestReactor) Receive(chID byte, peer Peer, msgBytes []byte) { tr.mtx.Lock() defer tr.mtx.Unlock() //fmt.Printf("Received: %X, %X\n", chID, msgBytes) - tr.msgsReceived[chID] = append(tr.msgsReceived[chID], PeerMessage{peer.Key(), msgBytes, tr.msgsCounter}) + tr.msgsReceived[chID] = append(tr.msgsReceived[chID], PeerMessage{peer.ID(), msgBytes, tr.msgsCounter}) tr.msgsCounter++ } } @@ -92,7 +93,7 @@ func (tr *TestReactor) getMsgs(chID byte) []PeerMessage { // convenience method for creating two switches connected to each other. // XXX: note this uses net.Pipe and not a proper TCP conn -func makeSwitchPair(t testing.TB, initSwitch func(int, *Switch) *Switch) (*Switch, *Switch) { +func MakeSwitchPair(t testing.TB, initSwitch func(int, *Switch) *Switch) (*Switch, *Switch) { // Create two switches that will be interconnected. switches := MakeConnectedSwitches(config, 2, initSwitch, Connect2Switches) return switches[0], switches[1] @@ -100,11 +101,11 @@ func makeSwitchPair(t testing.TB, initSwitch func(int, *Switch) *Switch) (*Switc func initSwitchFunc(i int, sw *Switch) *Switch { // Make two reactors of two channels each - sw.AddReactor("foo", NewTestReactor([]*ChannelDescriptor{ + sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ {ID: byte(0x00), Priority: 10}, {ID: byte(0x01), Priority: 10}, }, true)) - sw.AddReactor("bar", NewTestReactor([]*ChannelDescriptor{ + sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ {ID: byte(0x02), Priority: 10}, {ID: byte(0x03), Priority: 10}, }, true)) @@ -112,7 +113,7 @@ func initSwitchFunc(i int, sw *Switch) *Switch { } func TestSwitches(t *testing.T) { - s1, s2 := makeSwitchPair(t, initSwitchFunc) + s1, s2 := MakeSwitchPair(t, initSwitchFunc) defer s1.Stop() defer s2.Stop() @@ -156,12 +157,12 @@ func assertMsgReceivedWithTimeout(t *testing.T, msg string, channel byte, reacto } func TestConnAddrFilter(t *testing.T) { - s1 := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) - s2 := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + s1 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + s2 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) defer s1.Stop() defer s2.Stop() - c1, c2 := netPipe() + c1, c2 := conn.NetPipe() s1.SetAddrFilter(func(addr net.Addr) error { if addr.String() == c1.RemoteAddr().String() { @@ -192,15 +193,15 @@ func assertNoPeersAfterTimeout(t *testing.T, sw *Switch, timeout time.Duration) } func TestConnPubKeyFilter(t *testing.T) { - s1 := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) - s2 := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + s1 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + s2 := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) defer s1.Stop() defer s2.Stop() - c1, c2 := netPipe() + c1, c2 := conn.NetPipe() // set pubkey filter - s1.SetPubKeyFilter(func(pubkey crypto.PubKeyEd25519) error { + s1.SetPubKeyFilter(func(pubkey crypto.PubKey) error { if bytes.Equal(pubkey.Bytes(), s2.nodeInfo.PubKey.Bytes()) { return fmt.Errorf("Error: pipe is blacklisted") } @@ -224,7 +225,7 @@ func TestConnPubKeyFilter(t *testing.T) { func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { assert, require := assert.New(t), require.New(t) - sw := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + sw := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) err := sw.Start() if err != nil { t.Error(err) @@ -232,11 +233,11 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { defer sw.Stop() // simulate remote peer - rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519().Wrap(), Config: DefaultPeerConfig()} rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, DefaultPeerConfig()) + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig(), false) require.Nil(err) err = sw.addPeer(peer) require.Nil(err) @@ -251,7 +252,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { func TestSwitchReconnectsToPersistentPeer(t *testing.T) { assert, require := assert.New(t), require.New(t) - sw := makeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) + sw := MakeSwitch(config, 1, "testing", "123.123.123", initSwitchFunc) err := sw.Start() if err != nil { t.Error(err) @@ -259,12 +260,11 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { defer sw.Stop() // simulate remote peer - rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: DefaultPeerConfig()} + rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519().Wrap(), Config: DefaultPeerConfig()} rp.Start() defer rp.Stop() - peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, DefaultPeerConfig()) - peer.makePersistent() + peer, err := newOutboundPeer(rp.Addr(), sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, DefaultPeerConfig(), true) require.Nil(err) err = sw.addPeer(peer) require.Nil(err) @@ -300,16 +300,14 @@ func TestSwitchFullConnectivity(t *testing.T) { } } -func BenchmarkSwitches(b *testing.B) { - b.StopTimer() - - s1, s2 := makeSwitchPair(b, func(i int, sw *Switch) *Switch { +func BenchmarkSwitchBroadcast(b *testing.B) { + s1, s2 := MakeSwitchPair(b, func(i int, sw *Switch) *Switch { // Make bar reactors of bar channels each - sw.AddReactor("foo", NewTestReactor([]*ChannelDescriptor{ + sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ {ID: byte(0x00), Priority: 10}, {ID: byte(0x01), Priority: 10}, }, false)) - sw.AddReactor("bar", NewTestReactor([]*ChannelDescriptor{ + sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ {ID: byte(0x02), Priority: 10}, {ID: byte(0x03), Priority: 10}, }, false)) @@ -320,7 +318,8 @@ func BenchmarkSwitches(b *testing.B) { // Allow time for goroutines to boot up time.Sleep(1 * time.Second) - b.StartTimer() + + b.ResetTimer() numSuccess, numFailure := 0, 0 @@ -338,7 +337,4 @@ func BenchmarkSwitches(b *testing.B) { } b.Logf("success: %v, failure: %v", numSuccess, numFailure) - - // Allow everything to flush before stopping switches & closing connections. - b.StopTimer() } diff --git a/p2p/test_util.go b/p2p/test_util.go new file mode 100644 index 000000000..dea48dfd3 --- /dev/null +++ b/p2p/test_util.go @@ -0,0 +1,151 @@ +package p2p + +import ( + "math/rand" + "net" + + crypto "github.com/tendermint/go-crypto" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/p2p/conn" +) + +func AddPeerToSwitch(sw *Switch, peer Peer) { + sw.peers.Add(peer) +} + +func CreateRandomPeer(outbound bool) *peer { + addr, netAddr := CreateRoutableAddr() + p := &peer{ + nodeInfo: NodeInfo{ + ListenAddr: netAddr.DialString(), + PubKey: crypto.GenPrivKeyEd25519().Wrap().PubKey(), + }, + outbound: outbound, + mconn: &conn.MConnection{}, + } + p.SetLogger(log.TestingLogger().With("peer", addr)) + return p +} + +func CreateRoutableAddr() (addr string, netAddr *NetAddress) { + for { + var err error + addr = cmn.Fmt("%X@%v.%v.%v.%v:46656", cmn.RandBytes(20), rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256) + netAddr, err = NewNetAddressString(addr) + if err != nil { + panic(err) + } + if netAddr.Routable() { + break + } + } + return +} + +//------------------------------------------------------------------ +// Connects switches via arbitrary net.Conn. Used for testing. + +// MakeConnectedSwitches returns n switches, connected according to the connect func. +// If connect==Connect2Switches, the switches will be fully connected. +// initSwitch defines how the i'th switch should be initialized (ie. with what reactors). +// NOTE: panics if any switch fails to start. +func MakeConnectedSwitches(cfg *cfg.P2PConfig, n int, initSwitch func(int, *Switch) *Switch, connect func([]*Switch, int, int)) []*Switch { + switches := make([]*Switch, n) + for i := 0; i < n; i++ { + switches[i] = MakeSwitch(cfg, i, "testing", "123.123.123", initSwitch) + } + + if err := StartSwitches(switches); err != nil { + panic(err) + } + + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + connect(switches, i, j) + } + } + + return switches +} + +// Connect2Switches will connect switches i and j via net.Pipe(). +// Blocks until a connection is established. +// NOTE: caller ensures i and j are within bounds. +func Connect2Switches(switches []*Switch, i, j int) { + switchI := switches[i] + switchJ := switches[j] + c1, c2 := conn.NetPipe() + doneCh := make(chan struct{}) + go func() { + err := switchI.addPeerWithConnection(c1) + if err != nil { + panic(err) + } + doneCh <- struct{}{} + }() + go func() { + err := switchJ.addPeerWithConnection(c2) + if err != nil { + panic(err) + } + doneCh <- struct{}{} + }() + <-doneCh + <-doneCh +} + +func (sw *Switch) addPeerWithConnection(conn net.Conn) error { + peer, err := newInboundPeer(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey, sw.peerConfig) + if err != nil { + if err := conn.Close(); err != nil { + sw.Logger.Error("Error closing connection", "err", err) + } + return err + } + peer.SetLogger(sw.Logger.With("peer", conn.RemoteAddr())) + if err = sw.addPeer(peer); err != nil { + peer.CloseConn() + return err + } + + return nil +} + +// StartSwitches calls sw.Start() for each given switch. +// It returns the first encountered error. +func StartSwitches(switches []*Switch) error { + for _, s := range switches { + err := s.Start() // start switch and reactors + if err != nil { + return err + } + } + return nil +} + +func MakeSwitch(cfg *cfg.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch { + // new switch, add reactors + // TODO: let the config be passed in? + nodeKey := &NodeKey{ + PrivKey: crypto.GenPrivKeyEd25519().Wrap(), + } + sw := NewSwitch(cfg) + sw.SetLogger(log.TestingLogger()) + sw = initSwitch(i, sw) + ni := NodeInfo{ + PubKey: nodeKey.PubKey(), + Moniker: cmn.Fmt("switch%d", i), + Network: network, + Version: version, + ListenAddr: cmn.Fmt("%v:%v", network, rand.Intn(64512)+1023), + } + for ch, _ := range sw.reactorsByCh { + ni.Channels = append(ni.Channels, ch) + } + sw.SetNodeInfo(ni) + sw.SetNodeKey(nodeKey) + return sw +} diff --git a/p2p/trust/metric.go b/p2p/trust/metric.go index bf6ddb5e2..5770b4208 100644 --- a/p2p/trust/metric.go +++ b/p2p/trust/metric.go @@ -118,7 +118,7 @@ func (tm *TrustMetric) OnStart() error { } // OnStop implements Service -// Nothing to do since the goroutine shuts down by itself via BaseService.Quit +// Nothing to do since the goroutine shuts down by itself via BaseService.Quit() func (tm *TrustMetric) OnStop() {} // Returns a snapshot of the trust metric history data @@ -298,7 +298,7 @@ loop: select { case <-tick: tm.NextTimeInterval() - case <-tm.Quit: + case <-tm.Quit(): // Stop all further tracking for this metric break loop } diff --git a/p2p/trust/metric_test.go b/p2p/trust/metric_test.go index 00219a19e..98ea99ab4 100644 --- a/p2p/trust/metric_test.go +++ b/p2p/trust/metric_test.go @@ -56,7 +56,8 @@ func TestTrustMetricConfig(t *testing.T) { tm.Wait() } -func TestTrustMetricStopPause(t *testing.T) { +// XXX: This test fails non-deterministically +func _TestTrustMetricStopPause(t *testing.T) { // The TestTicker will provide manual control over // the passing of time within the metric tt := NewTestTicker() @@ -68,7 +69,9 @@ func TestTrustMetricStopPause(t *testing.T) { tt.NextTick() tm.Pause() + // could be 1 or 2 because Pause and NextTick race first := tm.Copy().numIntervals + // Allow more time to pass and check the intervals are unchanged tt.NextTick() tt.NextTick() @@ -87,6 +90,8 @@ func TestTrustMetricStopPause(t *testing.T) { // and check that the number of intervals match tm.NextTimeInterval() tm.NextTimeInterval() + // XXX: fails non-deterministically: + // expected 5, got 6 assert.Equal(t, second+2, tm.Copy().numIntervals) if first > second { diff --git a/p2p/trust/store.go b/p2p/trust/store.go index 0e61b0650..bbb4592a4 100644 --- a/p2p/trust/store.go +++ b/p2p/trust/store.go @@ -200,7 +200,7 @@ loop: select { case <-t.C: tms.SaveToDB() - case <-tms.Quit: + case <-tms.Quit(): break loop } } diff --git a/p2p/trust/ticker.go b/p2p/trust/ticker.go index bce9fcc24..3f0f30919 100644 --- a/p2p/trust/ticker.go +++ b/p2p/trust/ticker.go @@ -24,7 +24,7 @@ type TestTicker struct { // NewTestTicker returns our ticker used within test routines func NewTestTicker() *TestTicker { - c := make(chan time.Time, 1) + c := make(chan time.Time) return &TestTicker{ C: c, } diff --git a/p2p/types.go b/p2p/types.go index 4e0994b71..b11765bb5 100644 --- a/p2p/types.go +++ b/p2p/types.go @@ -1,81 +1,8 @@ package p2p import ( - "fmt" - "net" - "strconv" - "strings" - - crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/p2p/conn" ) -const maxNodeInfoSize = 10240 // 10Kb - -type NodeInfo struct { - PubKey crypto.PubKeyEd25519 `json:"pub_key"` - Moniker string `json:"moniker"` - Network string `json:"network"` - RemoteAddr string `json:"remote_addr"` - ListenAddr string `json:"listen_addr"` - Version string `json:"version"` // major.minor.revision - Other []string `json:"other"` // other application specific data -} - -// CONTRACT: two nodes are compatible if the major/minor versions match and network match -func (info *NodeInfo) CompatibleWith(other *NodeInfo) error { - iMajor, iMinor, _, iErr := splitVersion(info.Version) - oMajor, oMinor, _, oErr := splitVersion(other.Version) - - // if our own version number is not formatted right, we messed up - if iErr != nil { - return iErr - } - - // version number must be formatted correctly ("x.x.x") - if oErr != nil { - return oErr - } - - // major version must match - if iMajor != oMajor { - return fmt.Errorf("Peer is on a different major version. Got %v, expected %v", oMajor, iMajor) - } - - // minor version must match - if iMinor != oMinor { - return fmt.Errorf("Peer is on a different minor version. Got %v, expected %v", oMinor, iMinor) - } - - // nodes must be on the same network - if info.Network != other.Network { - return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network, info.Network) - } - - return nil -} - -func (info *NodeInfo) ListenHost() string { - host, _, _ := net.SplitHostPort(info.ListenAddr) // nolint: errcheck, gas - return host -} - -func (info *NodeInfo) ListenPort() int { - _, port, _ := net.SplitHostPort(info.ListenAddr) // nolint: errcheck, gas - port_i, err := strconv.Atoi(port) - if err != nil { - return -1 - } - return port_i -} - -func (info NodeInfo) String() string { - return fmt.Sprintf("NodeInfo{pk: %v, moniker: %v, network: %v [remote %v, listen %v], version: %v (%v)}", info.PubKey, info.Moniker, info.Network, info.RemoteAddr, info.ListenAddr, info.Version, info.Other) -} - -func splitVersion(version string) (string, string, string, error) { - spl := strings.Split(version, ".") - if len(spl) != 3 { - return "", "", "", fmt.Errorf("Invalid version format %v", version) - } - return spl[0], spl[1], spl[2], nil -} +type ChannelDescriptor = conn.ChannelDescriptor +type ConnectionStatus = conn.ConnectionStatus diff --git a/p2p/util.go b/p2p/util.go deleted file mode 100644 index a4c3ad58b..000000000 --- a/p2p/util.go +++ /dev/null @@ -1,15 +0,0 @@ -package p2p - -import ( - "crypto/sha256" -) - -// doubleSha256 calculates sha256(sha256(b)) and returns the resulting bytes. -func doubleSha256(b []byte) []byte { - hasher := sha256.New() - hasher.Write(b) // nolint: errcheck, gas - sum := hasher.Sum(nil) - hasher.Reset() - hasher.Write(sum) // nolint: errcheck, gas - return hasher.Sum(nil) -} diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 838fa5249..bc6cf759e 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -7,7 +7,6 @@ import ( "github.com/pkg/errors" - data "github.com/tendermint/go-wire/data" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpcclient "github.com/tendermint/tendermint/rpc/lib/client" "github.com/tendermint/tendermint/types" @@ -64,11 +63,11 @@ func (c *HTTP) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return result, nil } -func (c *HTTP) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (c *HTTP) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) } -func (c *HTTP) ABCIQueryWithOptions(path string, data data.Bytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (c *HTTP) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { result := new(ctypes.ResultABCIQuery) _, err := c.rpc.Call("abci_query", map[string]interface{}{"path": path, "data": data, "height": opts.Height, "trusted": opts.Trusted}, @@ -339,7 +338,7 @@ func (w *WSEvents) eventListener() { ch <- result.Data } w.mtx.RUnlock() - case <-w.Quit: + case <-w.Quit(): return } } diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 70cb4d951..6715b64b5 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -20,7 +20,6 @@ implementation. package client import ( - data "github.com/tendermint/go-wire/data" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" @@ -32,8 +31,9 @@ import ( type ABCIClient interface { // reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) - ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) - ABCIQueryWithOptions(path string, data data.Bytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) + ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) + ABCIQueryWithOptions(path string, data cmn.HexBytes, + opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) // writing to abci app BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 71f25ef23..be989ee1c 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -3,11 +3,11 @@ package client import ( "context" - data "github.com/tendermint/go-wire/data" nm "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/rpc/core" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" tmpubsub "github.com/tendermint/tmlibs/pubsub" ) @@ -56,11 +56,11 @@ func (Local) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return core.ABCIInfo() } -func (c *Local) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (c *Local) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) } -func (Local) ABCIQueryWithOptions(path string, data data.Bytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (Local) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { return core.ABCIQuery(path, data, opts.Height, opts.Trusted) } @@ -88,6 +88,10 @@ func (Local) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { return core.UnsafeDialSeeds(seeds) } +func (Local) DialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { + return core.UnsafeDialPeers(peers, persistent) +} + func (Local) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { return core.BlockchainInfo(minHeight, maxHeight) } diff --git a/rpc/client/mock/abci.go b/rpc/client/mock/abci.go index 4593d059a..7f4c45df3 100644 --- a/rpc/client/mock/abci.go +++ b/rpc/client/mock/abci.go @@ -2,11 +2,11 @@ package mock import ( abci "github.com/tendermint/abci/types" - data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/version" + cmn "github.com/tendermint/tmlibs/common" ) // ABCIApp will send all abci related request to the named app, @@ -26,11 +26,11 @@ func (a ABCIApp) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return &ctypes.ResultABCIInfo{a.App.Info(abci.RequestInfo{version.Version})}, nil } -func (a ABCIApp) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (a ABCIApp) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return a.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) } -func (a ABCIApp) ABCIQueryWithOptions(path string, data data.Bytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (a ABCIApp) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { q := a.App.Query(abci.RequestQuery{data, path, opts.Height, opts.Trusted}) return &ctypes.ResultABCIQuery{q}, nil } @@ -81,11 +81,11 @@ func (m ABCIMock) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return &ctypes.ResultABCIInfo{res.(abci.ResponseInfo)}, nil } -func (m ABCIMock) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (m ABCIMock) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return m.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) } -func (m ABCIMock) ABCIQueryWithOptions(path string, data data.Bytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (m ABCIMock) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { res, err := m.Query.GetResponse(QueryArgs{path, data, opts.Height, opts.Trusted}) if err != nil { return nil, err @@ -134,7 +134,7 @@ func NewABCIRecorder(client client.ABCIClient) *ABCIRecorder { type QueryArgs struct { Path string - Data data.Bytes + Data cmn.HexBytes Height int64 Trusted bool } @@ -153,11 +153,11 @@ func (r *ABCIRecorder) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return res, err } -func (r *ABCIRecorder) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (r *ABCIRecorder) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return r.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) } -func (r *ABCIRecorder) ABCIQueryWithOptions(path string, data data.Bytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (r *ABCIRecorder) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { res, err := r.Client.ABCIQueryWithOptions(path, data, opts) r.addCall(Call{ Name: "abci_query", diff --git a/rpc/client/mock/abci_test.go b/rpc/client/mock/abci_test.go index 0f83cc5f2..5a66c997a 100644 --- a/rpc/client/mock/abci_test.go +++ b/rpc/client/mock/abci_test.go @@ -11,11 +11,11 @@ import ( "github.com/tendermint/abci/example/dummy" abci "github.com/tendermint/abci/types" - data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tendermint/rpc/client/mock" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) func TestABCIMock(t *testing.T) { @@ -37,8 +37,8 @@ func TestABCIMock(t *testing.T) { BroadcastCommit: mock.Call{ Args: goodTx, Response: &ctypes.ResultBroadcastTxCommit{ - CheckTx: abci.ResponseCheckTx{Data: data.Bytes("stand")}, - DeliverTx: abci.ResponseDeliverTx{Data: data.Bytes("deliver")}, + CheckTx: abci.ResponseCheckTx{Data: cmn.HexBytes("stand")}, + DeliverTx: abci.ResponseDeliverTx{Data: cmn.HexBytes("deliver")}, }, Error: errors.New("bad tx"), }, @@ -98,7 +98,7 @@ func TestABCIRecorder(t *testing.T) { _, err := r.ABCIInfo() assert.Nil(err, "expected no err on info") - _, err = r.ABCIQueryWithOptions("path", data.Bytes("data"), client.ABCIQueryOptions{Trusted: false}) + _, err = r.ABCIQueryWithOptions("path", cmn.HexBytes("data"), client.ABCIQueryOptions{Trusted: false}) assert.NotNil(err, "expected error on query") require.Equal(2, len(r.Calls)) @@ -174,7 +174,7 @@ func TestABCIApp(t *testing.T) { assert.True(res.DeliverTx.IsOK()) // check the key - _qres, err := m.ABCIQueryWithOptions("/key", data.Bytes(key), client.ABCIQueryOptions{Trusted: true}) + _qres, err := m.ABCIQueryWithOptions("/key", cmn.HexBytes(key), client.ABCIQueryOptions{Trusted: true}) qres := _qres.Response require.Nil(err) assert.EqualValues(value, qres.Value) diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go index dc75e04cb..6af9abb27 100644 --- a/rpc/client/mock/client.go +++ b/rpc/client/mock/client.go @@ -16,7 +16,6 @@ package mock import ( "reflect" - data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tendermint/rpc/core" ctypes "github.com/tendermint/tendermint/rpc/core/types" @@ -83,11 +82,11 @@ func (c Client) ABCIInfo() (*ctypes.ResultABCIInfo, error) { return core.ABCIInfo() } -func (c Client) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { +func (c Client) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return c.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) } -func (c Client) ABCIQueryWithOptions(path string, data data.Bytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (c Client) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { return core.ABCIQuery(path, data, opts.Height, opts.Trusted) } @@ -111,6 +110,10 @@ func (c Client) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { return core.UnsafeDialSeeds(seeds) } +func (c Client) DialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { + return core.UnsafeDialPeers(peers, persistent) +} + func (c Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { return core.BlockchainInfo(minHeight, maxHeight) } diff --git a/rpc/client/mock/status_test.go b/rpc/client/mock/status_test.go index 80ffe7b7d..b3827fb13 100644 --- a/rpc/client/mock/status_test.go +++ b/rpc/client/mock/status_test.go @@ -6,9 +6,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - data "github.com/tendermint/go-wire/data" "github.com/tendermint/tendermint/rpc/client/mock" ctypes "github.com/tendermint/tendermint/rpc/core/types" + cmn "github.com/tendermint/tmlibs/common" ) func TestStatus(t *testing.T) { @@ -17,8 +17,8 @@ func TestStatus(t *testing.T) { m := &mock.StatusMock{ Call: mock.Call{ Response: &ctypes.ResultStatus{ - LatestBlockHash: data.Bytes("block"), - LatestAppHash: data.Bytes("app"), + LatestBlockHash: cmn.HexBytes("block"), + LatestAppHash: cmn.HexBytes("app"), LatestBlockHeight: 10, }}, } diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 9b956803e..165e4ec26 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" - "github.com/tendermint/iavl" "github.com/tendermint/tendermint/rpc/client" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -204,16 +203,8 @@ func TestAppCalls(t *testing.T) { // and we got a proof that works! _pres, err := c.ABCIQueryWithOptions("/key", k, client.ABCIQueryOptions{Trusted: false}) pres := _pres.Response - if assert.Nil(err) && assert.True(pres.IsOK()) { - proof, err := iavl.ReadKeyExistsProof(pres.Proof) - if assert.Nil(err) { - key := pres.Key - value := pres.Value - assert.EqualValues(appHash, proof.RootHash) - valid := proof.Verify(key, value, appHash) - assert.Nil(valid) - } - } + assert.Nil(err) + assert.True(pres.IsOK()) } } diff --git a/rpc/core/abci.go b/rpc/core/abci.go index a49b52b6e..874becae3 100644 --- a/rpc/core/abci.go +++ b/rpc/core/abci.go @@ -2,9 +2,9 @@ package core import ( abci "github.com/tendermint/abci/types" - data "github.com/tendermint/go-wire/data" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/version" + cmn "github.com/tendermint/tmlibs/common" ) // Query the application for some information. @@ -47,7 +47,7 @@ import ( // | data | []byte | false | true | Data | // | height | int64 | 0 | false | Height (0 means latest) | // | trusted | bool | false | false | Does not include a proof of the data inclusion | -func ABCIQuery(path string, data data.Bytes, height int64, trusted bool) (*ctypes.ResultABCIQuery, error) { +func ABCIQuery(path string, data cmn.HexBytes, height int64, trusted bool) (*ctypes.ResultABCIQuery, error) { resQuery, err := proxyAppQuery.QuerySync(abci.RequestQuery{ Path: path, Data: data, diff --git a/rpc/core/consensus.go b/rpc/core/consensus.go index 65c9fc364..25b67925c 100644 --- a/rpc/core/consensus.go +++ b/rpc/core/consensus.go @@ -3,6 +3,7 @@ package core import ( cm "github.com/tendermint/tendermint/consensus" cstypes "github.com/tendermint/tendermint/consensus/types" + p2p "github.com/tendermint/tendermint/p2p" ctypes "github.com/tendermint/tendermint/rpc/core/types" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" @@ -82,11 +83,11 @@ func Validators(heightPtr *int64) (*ctypes.ResultValidators, error) { // } // ``` func DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { - peerRoundStates := make(map[string]*cstypes.PeerRoundState) + peerRoundStates := make(map[p2p.ID]*cstypes.PeerRoundState) for _, peer := range p2pSwitch.Peers().List() { peerState := peer.Get(types.PeerStateKey).(*cm.PeerState) peerRoundState := peerState.GetRoundState() - peerRoundStates[peer.Key()] = peerRoundState + peerRoundStates[peer.ID()] = peerRoundState } return &ctypes.ResultDumpConsensusState{consensusState.GetRoundState(), peerRoundStates}, nil } diff --git a/rpc/core/doc.go b/rpc/core/doc.go index a72cec020..5f3e2dced 100644 --- a/rpc/core/doc.go +++ b/rpc/core/doc.go @@ -11,7 +11,7 @@ Tendermint RPC is built using [our own RPC library](https://github.com/tendermin ## Configuration -Set the `laddr` config parameter under `[rpc]` table in the `$TMHOME/config.toml` file or the `--rpc.laddr` command-line flag to the desired protocol://host:port setting. Default: `tcp://0.0.0.0:46657`. +Set the `laddr` config parameter under `[rpc]` table in the `$TMHOME/config/config.toml` file or the `--rpc.laddr` command-line flag to the desired protocol://host:port setting. Default: `tcp://0.0.0.0:46657`. ## Arguments @@ -95,6 +95,7 @@ Endpoints that require arguments: /broadcast_tx_sync?tx=_ /commit?height=_ /dial_seeds?seeds=_ +/dial_persistent_peers?persistent_peers=_ /subscribe?event=_ /tx?hash=_&prove=_ /unsafe_start_cpu_profiler?filename=_ diff --git a/rpc/core/events.go b/rpc/core/events.go index 538134b0f..9353ace6a 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -13,11 +13,57 @@ import ( // Subscribe for events via WebSocket. // +// To tell which events you want, you need to provide a query. query is a +// string, which has a form: "condition AND condition ..." (no OR at the +// moment). condition has a form: "key operation operand". key is a string with +// a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). +// operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a +// string (escaped with single quotes), number, date or time. +// +// Examples: +// tm.event = 'NewBlock' # new blocks +// tm.event = 'CompleteProposal' # node got a complete proposal +// tm.event = 'Tx' AND tx.hash = 'XYZ' # single transaction +// tm.event = 'Tx' AND tx.height = 5 # all txs of the fifth block +// tx.height = 5 # all txs of the fifth block +// +// Tendermint provides a few predefined keys: tm.event, tx.hash and tx.height. +// Note for transactions, you can define additional keys by providing tags with +// DeliverTx response. +// +// DeliverTx{ +// Tags: []*KVPair{ +// "agent.name": "K", +// } +// } +// +// tm.event = 'Tx' AND agent.name = 'K' +// tm.event = 'Tx' AND account.created_at >= TIME 2013-05-03T14:45:00Z +// tm.event = 'Tx' AND contract.sign_date = DATE 2017-01-01 +// tm.event = 'Tx' AND account.owner CONTAINS 'Igor' +// +// See list of all possible events here +// https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants +// +// For complete query syntax, check out +// https://godoc.org/github.com/tendermint/tmlibs/pubsub/query. +// // ```go +// import "github.com/tendermint/tmlibs/pubsub/query" // import "github.com/tendermint/tendermint/types" // // client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") -// result, err := client.AddListenerForEvent(types.EventStringNewBlock()) +// ctx, cancel := context.WithTimeout(context.Background(), timeout) +// defer cancel() +// query := query.MustParse("tm.event = 'Tx' AND tx.height = 3") +// txs := make(chan interface{}) +// err := client.Subscribe(ctx, "test-client", query, txs) +// +// go func() { +// for e := range txs { +// fmt.Println("got ", e.(types.TMEventData).Unwrap().(types.EventDataTx)) +// } +// }() // ``` // // > The above command returns JSON structured like this: @@ -35,7 +81,7 @@ import ( // // | Parameter | Type | Default | Required | Description | // |-----------+--------+---------+----------+-------------| -// | event | string | "" | true | Event name | +// | query | string | "" | true | Query | // // func Subscribe(wsCtx rpctypes.WSRPCContext, query string) (*ctypes.ResultSubscribe, error) { @@ -68,10 +114,8 @@ func Subscribe(wsCtx rpctypes.WSRPCContext, query string) (*ctypes.ResultSubscri // Unsubscribe from events via WebSocket. // // ```go -// import 'github.com/tendermint/tendermint/types' -// // client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") -// result, err := client.RemoveListenerForEvent(types.EventStringNewBlock()) +// err := client.Unsubscribe("test-client", query) // ``` // // > The above command returns JSON structured like this: @@ -89,7 +133,7 @@ func Subscribe(wsCtx rpctypes.WSRPCContext, query string) (*ctypes.ResultSubscri // // | Parameter | Type | Default | Required | Description | // |-----------+--------+---------+----------+-------------| -// | event | string | "" | true | Event name | +// | query | string | "" | true | Query | // // func Unsubscribe(wsCtx rpctypes.WSRPCContext, query string) (*ctypes.ResultUnsubscribe, error) { @@ -106,6 +150,25 @@ func Unsubscribe(wsCtx rpctypes.WSRPCContext, query string) (*ctypes.ResultUnsub return &ctypes.ResultUnsubscribe{}, nil } +// Unsubscribe from all events via WebSocket. +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") +// err := client.UnsubscribeAll("test-client") +// ``` +// +// > The above command returns JSON structured like this: +// +// ```json +// { +// "error": "", +// "result": {}, +// "id": "", +// "jsonrpc": "2.0" +// } +// ``` +// +// func UnsubscribeAll(wsCtx rpctypes.WSRPCContext) (*ctypes.ResultUnsubscribe, error) { addr := wsCtx.GetRemoteAddr() logger.Info("Unsubscribe from all", "remote", addr) diff --git a/rpc/core/mempool.go b/rpc/core/mempool.go index 3f663c376..1dbdd8012 100644 --- a/rpc/core/mempool.go +++ b/rpc/core/mempool.go @@ -8,9 +8,9 @@ import ( "github.com/pkg/errors" abci "github.com/tendermint/abci/types" - data "github.com/tendermint/go-wire/data" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) //----------------------------------------------------------------------------- @@ -192,7 +192,7 @@ func BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { deliverTxRes := deliverTxResMsg.(types.TMEventData).Unwrap().(types.EventDataTx) // The tx was included in a block. deliverTxR := deliverTxRes.Result - logger.Info("DeliverTx passed ", "tx", data.Bytes(tx), "response", deliverTxR) + logger.Info("DeliverTx passed ", "tx", cmn.HexBytes(tx), "response", deliverTxR) return &ctypes.ResultBroadcastTxCommit{ CheckTx: *checkTxR, DeliverTx: deliverTxR, diff --git a/rpc/core/net.go b/rpc/core/net.go index b3f1c7ce5..14e7389d5 100644 --- a/rpc/core/net.go +++ b/rpc/core/net.go @@ -1,8 +1,7 @@ package core import ( - "fmt" - + "github.com/pkg/errors" ctypes "github.com/tendermint/tendermint/rpc/core/types" ) @@ -42,7 +41,7 @@ func NetInfo() (*ctypes.ResultNetInfo, error) { peers := []ctypes.Peer{} for _, peer := range p2pSwitch.Peers().List() { peers = append(peers, ctypes.Peer{ - NodeInfo: *peer.NodeInfo(), + NodeInfo: peer.NodeInfo(), IsOutbound: peer.IsOutbound(), ConnectionStatus: peer.Status(), }) @@ -55,19 +54,31 @@ func NetInfo() (*ctypes.ResultNetInfo, error) { } func UnsafeDialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { - if len(seeds) == 0 { - return &ctypes.ResultDialSeeds{}, fmt.Errorf("No seeds provided") + return &ctypes.ResultDialSeeds{}, errors.New("No seeds provided") } - // starts go routines to dial each seed after random delays + // starts go routines to dial each peer after random delays logger.Info("DialSeeds", "addrBook", addrBook, "seeds", seeds) - err := p2pSwitch.DialSeeds(addrBook, seeds) + err := p2pSwitch.DialPeersAsync(addrBook, seeds, false) if err != nil { return &ctypes.ResultDialSeeds{}, err } return &ctypes.ResultDialSeeds{"Dialing seeds in progress. See /net_info for details"}, nil } +func UnsafeDialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { + if len(peers) == 0 { + return &ctypes.ResultDialPeers{}, errors.New("No peers provided") + } + // starts go routines to dial each peer after random delays + logger.Info("DialPeers", "addrBook", addrBook, "peers", peers, "persistent", persistent) + err := p2pSwitch.DialPeersAsync(addrBook, peers, persistent) + if err != nil { + return &ctypes.ResultDialPeers{}, err + } + return &ctypes.ResultDialPeers{"Dialing peers in progress. See /net_info for details"}, nil +} + // Get genesis file. // // ```shell diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 927d7ccad..2edb3f3d1 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -30,9 +30,9 @@ type P2P interface { Listeners() []p2p.Listener Peers() p2p.IPeerSet NumPeers() (outbound, inbound, dialig int) - NodeInfo() *p2p.NodeInfo + NodeInfo() p2p.NodeInfo IsListening() bool - DialSeeds(*p2p.AddrBook, []string) error + DialPeersAsync(p2p.AddrBook, []string, bool) error } //---------------------------------------------- @@ -54,7 +54,7 @@ var ( // objects pubKey crypto.PubKey genDoc *types.GenesisDoc // cache the genesis structure - addrBook *p2p.AddrBook + addrBook p2p.AddrBook txIndexer txindex.TxIndexer consensusReactor *consensus.ConsensusReactor eventBus *types.EventBus // thread safe @@ -94,7 +94,7 @@ func SetGenesisDoc(doc *types.GenesisDoc) { genDoc = doc } -func SetAddrBook(book *p2p.AddrBook) { +func SetAddrBook(book p2p.AddrBook) { addrBook = book } diff --git a/rpc/core/routes.go b/rpc/core/routes.go index fb5a1fd36..3ea7aa08c 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -39,6 +39,7 @@ var Routes = map[string]*rpc.RPCFunc{ func AddUnsafeRoutes() { // control API Routes["dial_seeds"] = rpc.NewRPCFunc(UnsafeDialSeeds, "seeds") + Routes["dial_peers"] = rpc.NewRPCFunc(UnsafeDialPeers, "peers,persistent") Routes["unsafe_flush_mempool"] = rpc.NewRPCFunc(UnsafeFlushMempool, "") // profiler API diff --git a/rpc/core/status.go b/rpc/core/status.go index 653c37f50..a8771c8f7 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -3,9 +3,9 @@ package core import ( "time" - data "github.com/tendermint/go-wire/data" ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" ) // Get Tendermint status including node info, pubkey, latest block @@ -59,8 +59,8 @@ func Status() (*ctypes.ResultStatus, error) { latestHeight := blockStore.Height() var ( latestBlockMeta *types.BlockMeta - latestBlockHash data.Bytes - latestAppHash data.Bytes + latestBlockHash cmn.HexBytes + latestAppHash cmn.HexBytes latestBlockTimeNano int64 ) if latestHeight != 0 { diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index dae7c0046..e13edc843 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -6,7 +6,8 @@ import ( abci "github.com/tendermint/abci/types" crypto "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" + cstypes "github.com/tendermint/tendermint/consensus/types" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/state" @@ -54,17 +55,17 @@ func NewResultCommit(header *types.Header, commit *types.Commit, } type ResultStatus struct { - NodeInfo *p2p.NodeInfo `json:"node_info"` + NodeInfo p2p.NodeInfo `json:"node_info"` PubKey crypto.PubKey `json:"pub_key"` - LatestBlockHash data.Bytes `json:"latest_block_hash"` - LatestAppHash data.Bytes `json:"latest_app_hash"` + LatestBlockHash cmn.HexBytes `json:"latest_block_hash"` + LatestAppHash cmn.HexBytes `json:"latest_app_hash"` LatestBlockHeight int64 `json:"latest_block_height"` LatestBlockTime time.Time `json:"latest_block_time"` Syncing bool `json:"syncing"` } func (s *ResultStatus) TxIndexEnabled() bool { - if s == nil || s.NodeInfo == nil { + if s == nil { return false } for _, s := range s.NodeInfo.Other { @@ -86,6 +87,10 @@ type ResultDialSeeds struct { Log string `json:"log"` } +type ResultDialPeers struct { + Log string `json:"log"` +} + type Peer struct { p2p.NodeInfo `json:"node_info"` IsOutbound bool `json:"is_outbound"` @@ -99,21 +104,21 @@ type ResultValidators struct { type ResultDumpConsensusState struct { RoundState *cstypes.RoundState `json:"round_state"` - PeerRoundStates map[string]*cstypes.PeerRoundState `json:"peer_round_states"` + PeerRoundStates map[p2p.ID]*cstypes.PeerRoundState `json:"peer_round_states"` } type ResultBroadcastTx struct { - Code uint32 `json:"code"` - Data data.Bytes `json:"data"` - Log string `json:"log"` + Code uint32 `json:"code"` + Data cmn.HexBytes `json:"data"` + Log string `json:"log"` - Hash data.Bytes `json:"hash"` + Hash cmn.HexBytes `json:"hash"` } type ResultBroadcastTxCommit struct { CheckTx abci.ResponseCheckTx `json:"check_tx"` DeliverTx abci.ResponseDeliverTx `json:"deliver_tx"` - Hash data.Bytes `json:"hash"` + Hash cmn.HexBytes `json:"hash"` Height int64 `json:"height"` } diff --git a/rpc/core/types/responses_test.go b/rpc/core/types/responses_test.go index fa0da3fdf..e410d47ae 100644 --- a/rpc/core/types/responses_test.go +++ b/rpc/core/types/responses_test.go @@ -17,7 +17,7 @@ func TestStatusIndexer(t *testing.T) { status = &ResultStatus{} assert.False(status.TxIndexEnabled()) - status.NodeInfo = &p2p.NodeInfo{} + status.NodeInfo = p2p.NodeInfo{} assert.False(status.TxIndexEnabled()) cases := []struct { diff --git a/rpc/lib/client/ws_client.go b/rpc/lib/client/ws_client.go index 79e3f63f4..ca75ad561 100644 --- a/rpc/lib/client/ws_client.go +++ b/rpc/lib/client/ws_client.go @@ -335,7 +335,7 @@ func (c *WSClient) reconnectRoutine() { c.startReadWriteRoutines() } } - case <-c.Quit: + case <-c.Quit(): return } } @@ -394,7 +394,7 @@ func (c *WSClient) writeRoutine() { c.Logger.Debug("sent ping") case <-c.readRoutineQuit: return - case <-c.Quit: + case <-c.Quit(): if err := c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { c.Logger.Error("failed to write message", "err", err) } @@ -455,7 +455,7 @@ func (c *WSClient) readRoutine() { // c.wg.Wait() in c.Stop(). Note we rely on Quit being closed so that it sends unlimited Quit signals to stop // both readRoutine and writeRoutine select { - case <-c.Quit: + case <-c.Quit(): case c.ResponsesCh <- response: } } diff --git a/rpc/lib/client/ws_client_test.go b/rpc/lib/client/ws_client_test.go index cc7897286..73f671609 100644 --- a/rpc/lib/client/ws_client_test.go +++ b/rpc/lib/client/ws_client_test.go @@ -132,7 +132,7 @@ func TestWSClientReconnectFailure(t *testing.T) { for { select { case <-c.ResponsesCh: - case <-c.Quit: + case <-c.Quit(): return } } @@ -217,7 +217,7 @@ func callWgDoneOnResult(t *testing.T, c *WSClient, wg *sync.WaitGroup) { if resp.Result != nil { wg.Done() } - case <-c.Quit: + case <-c.Quit(): return } } diff --git a/rpc/lib/rpc_test.go b/rpc/lib/rpc_test.go index be170985f..cadc68f8e 100644 --- a/rpc/lib/rpc_test.go +++ b/rpc/lib/rpc_test.go @@ -17,11 +17,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + client "github.com/tendermint/tendermint/rpc/lib/client" server "github.com/tendermint/tendermint/rpc/lib/server" types "github.com/tendermint/tendermint/rpc/lib/types" - "github.com/tendermint/tmlibs/log" ) // Client and Server should work over tcp or unix sockets @@ -47,7 +48,7 @@ type ResultEchoBytes struct { } type ResultEchoDataBytes struct { - Value data.Bytes `json:"value"` + Value cmn.HexBytes `json:"value"` } // Define some routes @@ -75,7 +76,7 @@ func EchoBytesResult(v []byte) (*ResultEchoBytes, error) { return &ResultEchoBytes{v}, nil } -func EchoDataBytesResult(v data.Bytes) (*ResultEchoDataBytes, error) { +func EchoDataBytesResult(v cmn.HexBytes) (*ResultEchoDataBytes, error) { return &ResultEchoDataBytes{v}, nil } @@ -174,7 +175,7 @@ func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { return result.Value, nil } -func echoDataBytesViaHTTP(cl client.HTTPClient, bytes data.Bytes) (data.Bytes, error) { +func echoDataBytesViaHTTP(cl client.HTTPClient, bytes cmn.HexBytes) (cmn.HexBytes, error) { params := map[string]interface{}{ "arg": bytes, } @@ -196,7 +197,7 @@ func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { require.Nil(t, err) assert.Equal(t, got2, val2) - val3 := data.Bytes(randBytes(t)) + val3 := cmn.HexBytes(randBytes(t)) got3, err := echoDataBytesViaHTTP(cl, val3) require.Nil(t, err) assert.Equal(t, got3, val3) diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 1e14ea9a0..1bac625be 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -484,7 +484,7 @@ func (wsc *wsConnection) GetEventSubscriber() types.EventSubscriber { // It implements WSRPCConnection. It is Goroutine-safe. func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { select { - case <-wsc.Quit: + case <-wsc.Quit(): return case wsc.writeChan <- resp: } @@ -494,7 +494,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { // It implements WSRPCConnection. It is Goroutine-safe func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { select { - case <-wsc.Quit: + case <-wsc.Quit(): return false case wsc.writeChan <- resp: return true @@ -525,7 +525,7 @@ func (wsc *wsConnection) readRoutine() { for { select { - case <-wsc.Quit: + case <-wsc.Quit(): return default: // reset deadline for every type of message (control or data) @@ -643,7 +643,7 @@ func (wsc *wsConnection) writeRoutine() { return } } - case <-wsc.Quit: + case <-wsc.Quit(): return } } diff --git a/rpc/lib/server/parse_test.go b/rpc/lib/server/parse_test.go index a86226f2c..0703d50ed 100644 --- a/rpc/lib/server/parse_test.go +++ b/rpc/lib/server/parse_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" ) func TestParseJSONMap(t *testing.T) { @@ -31,7 +31,7 @@ func TestParseJSONMap(t *testing.T) { // preloading map with values doesn't help tmp := 0 p2 := map[string]interface{}{ - "value": &data.Bytes{}, + "value": &cmn.HexBytes{}, "height": &tmp, } err = json.Unmarshal(input, &p2) @@ -54,7 +54,7 @@ func TestParseJSONMap(t *testing.T) { Height interface{} `json:"height"` }{ Height: &tmp, - Value: &data.Bytes{}, + Value: &cmn.HexBytes{}, } err = json.Unmarshal(input, &p3) if assert.Nil(err) { @@ -62,7 +62,7 @@ func TestParseJSONMap(t *testing.T) { if assert.True(ok, "%#v", p3.Height) { assert.Equal(22, *h) } - v, ok := p3.Value.(*data.Bytes) + v, ok := p3.Value.(*cmn.HexBytes) if assert.True(ok, "%#v", p3.Value) { assert.EqualValues([]byte{0x12, 0x34}, *v) } @@ -70,8 +70,8 @@ func TestParseJSONMap(t *testing.T) { // simplest solution, but hard-coded p4 := struct { - Value data.Bytes `json:"value"` - Height int `json:"height"` + Value cmn.HexBytes `json:"value"` + Height int `json:"height"` }{} err = json.Unmarshal(input, &p4) if assert.Nil(err) { @@ -90,10 +90,10 @@ func TestParseJSONMap(t *testing.T) { assert.Equal(22, h) } - var v data.Bytes + var v cmn.HexBytes err = json.Unmarshal(*p5["value"], &v) if assert.Nil(err) { - assert.Equal(data.Bytes{0x12, 0x34}, v) + assert.Equal(cmn.HexBytes{0x12, 0x34}, v) } } } @@ -119,10 +119,10 @@ func TestParseJSONArray(t *testing.T) { // preloading map with values helps here (unlike map - p2 above) tmp := 0 - p2 := []interface{}{&data.Bytes{}, &tmp} + p2 := []interface{}{&cmn.HexBytes{}, &tmp} err = json.Unmarshal(input, &p2) if assert.Nil(err) { - v, ok := p2[0].(*data.Bytes) + v, ok := p2[0].(*cmn.HexBytes) if assert.True(ok, "%#v", p2[0]) { assert.EqualValues([]byte{0x12, 0x34}, *v) } diff --git a/scripts/debora/unsafe_reset_net.sh b/scripts/debora/unsafe_reset_net.sh index c6767427d..3698e5ace 100755 --- a/scripts/debora/unsafe_reset_net.sh +++ b/scripts/debora/unsafe_reset_net.sh @@ -3,7 +3,7 @@ set -euo pipefail IFS=$'\n\t' debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; killall tendermint; killall logjack" -debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint unsafe_reset_priv_validator; rm -rf ~/.tendermint/data; rm ~/.tendermint/genesis.json; rm ~/.tendermint/logs/*" +debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint unsafe_reset_priv_validator; rm -rf ~/.tendermint/data; rm ~/.tendermint/config/genesis.json; rm ~/.tendermint/logs/*" debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; git pull origin develop; make" debora run -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; mkdir -p ~/.tendermint/logs" debora run --bg --label tendermint -- bash -c "cd \$GOPATH/src/github.com/tendermint/tendermint; tendermint node 2>&1 | stdinwriter -outpath ~/.tendermint/logs/tendermint.log" diff --git a/scripts/dist.sh b/scripts/dist.sh index c144c7fdd..40aa71e98 100755 --- a/scripts/dist.sh +++ b/scripts/dist.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -e +# WARN: non hermetic build (people must run this script inside docker to +# produce deterministic binaries). + # Get the version from the environment, or try to figure it out. if [ -z $VERSION ]; then VERSION=$(awk -F\" '/Version =/ { print $2; exit }' < version/version.go) @@ -11,22 +14,51 @@ if [ -z "$VERSION" ]; then fi echo "==> Building version $VERSION..." -# Get the parent directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" - -# Change into that dir because we expect that. -cd "$DIR" - # Delete the old dir echo "==> Removing old directory..." rm -rf build/pkg mkdir -p build/pkg -# Do a hermetic build inside a Docker container. -docker build -t tendermint/tendermint-builder scripts/tendermint-builder/ -docker run --rm -e "BUILD_TAGS=$BUILD_TAGS" -v "$(pwd)":/go/src/github.com/tendermint/tendermint tendermint/tendermint-builder ./scripts/dist_build.sh +# Get the git commit +GIT_COMMIT="$(git rev-parse --short=8 HEAD)" +GIT_IMPORT="github.com/tendermint/tendermint/version" + +# Determine the arch/os combos we're building for +XC_ARCH=${XC_ARCH:-"386 amd64 arm"} +XC_OS=${XC_OS:-"solaris darwin freebsd linux windows"} +XC_EXCLUDE=${XC_EXCLUDE:-" darwin/arm solaris/amd64 solaris/386 solaris/arm freebsd/amd64 windows/arm "} + +# Make sure build tools are available. +make get_tools + +# Get VENDORED dependencies +make get_vendor_deps + +# Build! +# ldflags: -s Omit the symbol table and debug information. +# -w Omit the DWARF symbol table. +echo "==> Building..." +IFS=' ' read -ra arch_list <<< "$XC_ARCH" +IFS=' ' read -ra os_list <<< "$XC_OS" +for arch in "${arch_list[@]}"; do + for os in "${os_list[@]}"; do + if [[ "$XC_EXCLUDE" != *" $os/$arch "* ]]; then + echo "--> $os/$arch" + GOOS=${os} GOARCH=${arch} go build -ldflags "-s -w -X ${GIT_IMPORT}.GitCommit=${GIT_COMMIT}" -tags="${BUILD_TAGS}" -o "build/pkg/${os}_${arch}/tendermint" ./cmd/tendermint + fi + done +done + +# Zip all the files. +echo "==> Packaging..." +for PLATFORM in $(find ./build/pkg -mindepth 1 -maxdepth 1 -type d); do + OSARCH=$(basename "${PLATFORM}") + echo "--> ${OSARCH}" + + pushd "$PLATFORM" >/dev/null 2>&1 + zip "../${OSARCH}.zip" ./* + popd >/dev/null 2>&1 +done # Add "tendermint" and $VERSION prefix to package name. rm -rf ./build/dist diff --git a/scripts/dist_build.sh b/scripts/dist_build.sh deleted file mode 100755 index 587199e02..000000000 --- a/scripts/dist_build.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Get the parent directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" - -# Change into that dir because we expect that. -cd "$DIR" - -# Get the git commit -GIT_COMMIT="$(git rev-parse --short HEAD)" -GIT_IMPORT="github.com/tendermint/tendermint/version" - -# Determine the arch/os combos we're building for -XC_ARCH=${XC_ARCH:-"386 amd64 arm"} -XC_OS=${XC_OS:-"solaris darwin freebsd linux windows"} - -# Make sure build tools are available. -make tools - -# Get VENDORED dependencies -make get_vendor_deps - -# Build! -# ldflags: -s Omit the symbol table and debug information. -# -w Omit the DWARF symbol table. -echo "==> Building..." -"$(which gox)" \ - -os="${XC_OS}" \ - -arch="${XC_ARCH}" \ - -osarch="!darwin/arm !solaris/amd64 !freebsd/amd64" \ - -ldflags "-s -w -X ${GIT_IMPORT}.GitCommit=${GIT_COMMIT}" \ - -output "build/pkg/{{.OS}}_{{.Arch}}/tendermint" \ - -tags="${BUILD_TAGS}" \ - github.com/tendermint/tendermint/cmd/tendermint - -# Zip all the files. -echo "==> Packaging..." -for PLATFORM in $(find ./build/pkg -mindepth 1 -maxdepth 1 -type d); do - OSARCH=$(basename "${PLATFORM}") - echo "--> ${OSARCH}" - - pushd "$PLATFORM" >/dev/null 2>&1 - zip "../${OSARCH}.zip" ./* - popd >/dev/null 2>&1 -done - -exit 0 diff --git a/scripts/release.sh b/scripts/release.sh index 02899ad5d..9a4e508e1 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -25,9 +25,9 @@ sh -c "'$DIR/scripts/dist.sh'" # Pushing binaries to S3 sh -c "'$DIR/scripts/publish.sh'" -echo "==> Crafting a Github release" -today=$(date +"%B-%d-%Y") -ghr -b "https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#${VERSION//.}-${today,}" "v$VERSION" "$DIR/build/dist" +# echo "==> Crafting a Github release" +# today=$(date +"%B-%d-%Y") +# ghr -b "https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#${VERSION//.}-${today,}" "v$VERSION" "$DIR/build/dist" # Build and push Docker image diff --git a/scripts/tendermint-builder/Dockerfile b/scripts/tendermint-builder/Dockerfile deleted file mode 100644 index 2d3c0ef5d..000000000 --- a/scripts/tendermint-builder/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.9.2 - -RUN apt-get update && apt-get install -y --no-install-recommends \ - zip \ - && rm -rf /var/lib/apt/lists/* - -# We want to ensure that release builds never have any cgo dependencies so we -# switch that off at the highest level. -ENV CGO_ENABLED 0 - -RUN mkdir -p $GOPATH/src/github.com/tendermint/tendermint -WORKDIR $GOPATH/src/github.com/tendermint/tendermint diff --git a/state/execution.go b/state/execution.go index 921799b80..64db9f31a 100644 --- a/state/execution.go +++ b/state/execution.go @@ -1,7 +1,6 @@ package state import ( - "errors" "fmt" fail "github.com/ebuchman/fail-test" @@ -127,21 +126,26 @@ func (blockExec *BlockExecutor) Commit(block *types.Block) ([]byte, error) { blockExec.mempool.Lock() defer blockExec.mempool.Unlock() + // while mempool is Locked, flush to ensure all async requests have completed + // in the ABCI app before Commit. + err := blockExec.mempool.FlushAppConn() + if err != nil { + blockExec.logger.Error("Client error during mempool.FlushAppConn", "err", err) + return nil, err + } + // Commit block, get hash back res, err := blockExec.proxyApp.CommitSync() if err != nil { blockExec.logger.Error("Client error during proxyAppConn.CommitSync", "err", err) return nil, err } - if res.IsErr() { - blockExec.logger.Error("Error in proxyAppConn.CommitSync", "err", res) - return nil, res - } - if res.Log != "" { - blockExec.logger.Debug("Commit.Log: " + res.Log) - } + // ResponseCommit has no error code - just data - blockExec.logger.Info("Committed state", "height", block.Height, "txs", block.NumTxs, "appHash", res.Data) + blockExec.logger.Info("Committed state", + "height", block.Height, + "txs", block.NumTxs, + "appHash", fmt.Sprintf("%X", res.Data)) // Update mempool. if err := blockExec.mempool.Update(block.Height, block.Txs); err != nil { @@ -191,9 +195,9 @@ func execBlockOnProxyApp(logger log.Logger, proxyAppConn proxy.AppConnConsensus, } // TODO: determine which validators were byzantine - byzantineVals := make([]*abci.Evidence, len(block.Evidence.Evidence)) + byzantineVals := make([]abci.Evidence, len(block.Evidence.Evidence)) for i, ev := range block.Evidence.Evidence { - byzantineVals[i] = &abci.Evidence{ + byzantineVals[i] = abci.Evidence{ PubKey: ev.Address(), // XXX Height: ev.Height(), } @@ -236,18 +240,10 @@ func execBlockOnProxyApp(logger log.Logger, proxyAppConn proxy.AppConnConsensus, return abciResponses, nil } -func updateValidators(currentSet *types.ValidatorSet, updates []*abci.Validator) error { - // If more or equal than 1/3 of total voting power changed in one block, then - // a light client could never prove the transition externally. See - // ./lite/doc.go for details on how a light client tracks validators. - vp23, err := changeInVotingPowerMoreOrEqualToOneThird(currentSet, updates) - if err != nil { - return err - } - if vp23 { - return errors.New("the change in voting power must be strictly less than 1/3") - } - +// If more or equal than 1/3 of total voting power changed in one block, then +// a light client could never prove the transition externally. See +// ./lite/doc.go for details on how a light client tracks validators. +func updateValidators(currentSet *types.ValidatorSet, updates []abci.Validator) error { for _, v := range updates { pubkey, err := crypto.PubKeyFromBytes(v.PubKey) // NOTE: expects go-wire encoded pubkey if err != nil { @@ -286,42 +282,6 @@ func updateValidators(currentSet *types.ValidatorSet, updates []*abci.Validator) return nil } -func changeInVotingPowerMoreOrEqualToOneThird(currentSet *types.ValidatorSet, updates []*abci.Validator) (bool, error) { - threshold := currentSet.TotalVotingPower() * 1 / 3 - acc := int64(0) - - for _, v := range updates { - pubkey, err := crypto.PubKeyFromBytes(v.PubKey) // NOTE: expects go-wire encoded pubkey - if err != nil { - return false, err - } - - address := pubkey.Address() - power := int64(v.Power) - // mind the overflow from int64 - if power < 0 { - return false, fmt.Errorf("Power (%d) overflows int64", v.Power) - } - - _, val := currentSet.GetByAddress(address) - if val == nil { - acc += power - } else { - np := val.VotingPower - power - if np < 0 { - np = -np - } - acc += np - } - - if acc >= threshold { - return true, nil - } - } - - return false, nil -} - // updateState returns a new State updated according to the header and responses. func updateState(s State, blockID types.BlockID, header *types.Header, abciResponses *ABCIResponses) (State, error) { @@ -417,12 +377,6 @@ func ExecCommitBlock(appConnConsensus proxy.AppConnConsensus, block *types.Block logger.Error("Client error during proxyAppConn.CommitSync", "err", res) return nil, err } - if res.IsErr() { - logger.Error("Error in proxyAppConn.CommitSync", "err", res) - return nil, res - } - if res.Log != "" { - logger.Info("Commit.Log: " + res.Log) - } + // ResponseCommit has no error or log, just data return res.Data, nil } diff --git a/state/execution_test.go b/state/execution_test.go index ffb10f17b..25d88fe41 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -12,6 +12,7 @@ import ( crypto "github.com/tendermint/go-crypto" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" ) @@ -105,11 +106,11 @@ func TestBeginBlockByzantineValidators(t *testing.T) { testCases := []struct { desc string evidence []types.Evidence - expectedByzantineValidators []*abci.Evidence + expectedByzantineValidators []abci.Evidence }{ - {"none byzantine", []types.Evidence{}, []*abci.Evidence{}}, - {"one byzantine", []types.Evidence{ev1}, []*abci.Evidence{{ev1.Address(), ev1.Height()}}}, - {"multiple byzantine", []types.Evidence{ev1, ev2}, []*abci.Evidence{ + {"none byzantine", []types.Evidence{}, []abci.Evidence{}}, + {"one byzantine", []types.Evidence{ev1}, []abci.Evidence{{ev1.Address(), ev1.Height()}}}, + {"multiple byzantine", []types.Evidence{ev1, ev2}, []abci.Evidence{ {ev1.Address(), ev1.Height()}, {ev2.Address(), ev2.Height()}}}, } @@ -161,7 +162,7 @@ type testApp struct { abci.BaseApplication AbsentValidators []int32 - ByzantineValidators []*abci.Evidence + ByzantineValidators []abci.Evidence } func NewDummyApplication() *testApp { @@ -179,7 +180,7 @@ func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlo } func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []*abci.KVPair{}} + return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} } func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { diff --git a/state/state_test.go b/state/state_test.go index 61b3167b3..02c94253e 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -22,7 +22,8 @@ import ( // setupTestCase does setup common to all test cases func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, State) { config := cfg.ResetTestRoot("state_") - stateDB := dbm.NewDB("state", config.DBBackend, config.DBDir()) + dbType := dbm.DBBackendType(config.DBBackend) + stateDB := dbm.NewDB("state", dbType, config.DBDir()) state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) assert.NoError(t, err, "expected no error on LoadStateFromDBOrGenesisFile") @@ -77,9 +78,9 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // build mock responses block := makeBlock(state, 2) abciResponses := NewABCIResponses(block) - abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: []*abci.KVPair{}} - abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: []*abci.KVPair{}} - abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{ + abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: []cmn.KVPair{}} + abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: []cmn.KVPair{}} + abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.Validator{ { PubKey: crypto.GenPrivKeyEd25519().PubKey().Bytes(), Power: 10, @@ -122,9 +123,9 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { []*abci.ResponseDeliverTx{ {Code: 383}, {Data: []byte("Gotcha!"), - Tags: []*abci.KVPair{ - abci.KVPairInt("a", 1), - abci.KVPairString("build", "stuff"), + Tags: []cmn.KVPair{ + cmn.KVPair{[]byte("a"), []byte{1}}, + cmn.KVPair{[]byte("build"), []byte("stuff")}, }}, }, types.ABCIResults{ @@ -374,73 +375,6 @@ func makeParams(blockBytes, blockTx, blockGas, txBytes, } } -func TestLessThanOneThirdOfVotingPowerPerBlockEnforced(t *testing.T) { - testCases := []struct { - initialValSetSize int - shouldErr bool - valUpdatesFn func(vals *types.ValidatorSet) []*abci.Validator - }{ - ///////////// 1 val (vp: 10) => less than 3 is ok //////////////////////// - // adding 1 validator => 10 - 0: {1, false, func(vals *types.ValidatorSet) []*abci.Validator { - return []*abci.Validator{ - {PubKey: pk(), Power: 2}, - } - }}, - 1: {1, true, func(vals *types.ValidatorSet) []*abci.Validator { - return []*abci.Validator{ - {PubKey: pk(), Power: 3}, - } - }}, - 2: {1, true, func(vals *types.ValidatorSet) []*abci.Validator { - return []*abci.Validator{ - {PubKey: pk(), Power: 100}, - } - }}, - - ///////////// 3 val (vp: 30) => less than 10 is ok //////////////////////// - // adding and removing validator => 20 - 3: {3, true, func(vals *types.ValidatorSet) []*abci.Validator { - _, firstVal := vals.GetByIndex(0) - return []*abci.Validator{ - {PubKey: firstVal.PubKey.Bytes(), Power: 0}, - {PubKey: pk(), Power: 10}, - } - }}, - // adding 1 validator => 10 - 4: {3, true, func(vals *types.ValidatorSet) []*abci.Validator { - return []*abci.Validator{ - {PubKey: pk(), Power: 10}, - } - }}, - // adding 2 validators => 8 - 5: {3, false, func(vals *types.ValidatorSet) []*abci.Validator { - return []*abci.Validator{ - {PubKey: pk(), Power: 4}, - {PubKey: pk(), Power: 4}, - } - }}, - } - - for i, tc := range testCases { - tearDown, stateDB, state := setupTestCase(t) - state.Validators = genValSet(tc.initialValSetSize) - SaveState(stateDB, state) - height := state.LastBlockHeight + 1 - block := makeBlock(state, height) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: tc.valUpdatesFn(state.Validators)}, - } - state, err := updateState(state, types.BlockID{block.Hash(), types.PartSetHeader{}}, block.Header, abciResponses) - if tc.shouldErr { - assert.Error(t, err, "#%d", i) - } else { - assert.NoError(t, err, "#%d", i) - } - tearDown(t) - } -} - func pk() []byte { return crypto.GenPrivKeyEd25519().PubKey().Bytes() } @@ -450,20 +384,20 @@ func TestApplyUpdates(t *testing.T) { cases := [...]struct { init types.ConsensusParams - updates *abci.ConsensusParams + updates abci.ConsensusParams expected types.ConsensusParams }{ - 0: {initParams, nil, initParams}, - 1: {initParams, &abci.ConsensusParams{}, initParams}, + 0: {initParams, abci.ConsensusParams{}, initParams}, + 1: {initParams, abci.ConsensusParams{}, initParams}, 2: {initParams, - &abci.ConsensusParams{ + abci.ConsensusParams{ TxSize: &abci.TxSize{ MaxBytes: 123, }, }, makeParams(1, 2, 3, 123, 5, 6)}, 3: {initParams, - &abci.ConsensusParams{ + abci.ConsensusParams{ BlockSize: &abci.BlockSize{ MaxTxs: 44, MaxGas: 55, @@ -471,7 +405,7 @@ func TestApplyUpdates(t *testing.T) { }, makeParams(1, 44, 55, 4, 5, 6)}, 4: {initParams, - &abci.ConsensusParams{ + abci.ConsensusParams{ BlockSize: &abci.BlockSize{ MaxTxs: 789, }, @@ -486,7 +420,7 @@ func TestApplyUpdates(t *testing.T) { } for i, tc := range cases { - res := tc.init.Update(tc.updates) + res := tc.init.Update(&(tc.updates)) assert.Equal(t, tc.expected, res, "case %d", i) } } @@ -496,14 +430,14 @@ func makeHeaderPartsResponsesValPubKeyChange(state State, height int64, block := makeBlock(state, height) abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{}}, + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.Validator{}}, } // if the pubkey is new, remove the old and add the new _, val := state.Validators.GetByIndex(0) if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []*abci.Validator{ + ValidatorUpdates: []abci.Validator{ {val.PubKey.Bytes(), 0}, {pubkey.Bytes(), 10}, }, @@ -518,14 +452,14 @@ func makeHeaderPartsResponsesValPowerChange(state State, height int64, block := makeBlock(state, height) abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{}}, + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.Validator{}}, } // if the pubkey is new, remove the old and add the new _, val := state.Validators.GetByIndex(0) if val.VotingPower != power { abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []*abci.Validator{ + ValidatorUpdates: []abci.Validator{ {val.PubKey.Bytes(), power}, }, } diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index b70f3699f..ed4fcc8a6 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -10,13 +10,13 @@ import ( "github.com/pkg/errors" - abci "github.com/tendermint/abci/types" wire "github.com/tendermint/go-wire" - "github.com/tendermint/tendermint/state/txindex" - "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" - db "github.com/tendermint/tmlibs/db" + dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/pubsub/query" + + "github.com/tendermint/tendermint/state/txindex" + "github.com/tendermint/tendermint/types" ) const ( @@ -27,13 +27,13 @@ var _ txindex.TxIndexer = (*TxIndex)(nil) // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB). type TxIndex struct { - store db.DB + store dbm.DB tagsToIndex []string indexAllTags bool } // NewTxIndex creates new KV indexer. -func NewTxIndex(store db.DB, options ...func(*TxIndex)) *TxIndex { +func NewTxIndex(store dbm.DB, options ...func(*TxIndex)) *TxIndex { txi := &TxIndex{store: store, tagsToIndex: make([]string, 0), indexAllTags: false} for _, o := range options { o(txi) @@ -87,7 +87,7 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { // index tx by tags for _, tag := range result.Result.Tags { - if txi.indexAllTags || cmn.StringInSlice(tag.Key, txi.tagsToIndex) { + if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) { storeBatch.Set(keyForTag(tag, result), hash) } } @@ -109,7 +109,7 @@ func (txi *TxIndex) Index(result *types.TxResult) error { // index tx by tags for _, tag := range result.Result.Tags { - if txi.indexAllTags || cmn.StringInSlice(tag.Key, txi.tagsToIndex) { + if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) { b.Set(keyForTag(tag, result), hash) } } @@ -270,18 +270,18 @@ func isRangeOperation(op query.Operator) bool { func (txi *TxIndex) match(c query.Condition, startKey []byte) (hashes [][]byte) { if c.Op == query.OpEqual { - it := txi.store.IteratorPrefix(startKey) - defer it.Release() - for it.Next() { + it := dbm.IteratePrefix(txi.store, startKey) + defer it.Close() + for ; it.Valid(); it.Next() { hashes = append(hashes, it.Value()) } } else if c.Op == query.OpContains { // XXX: doing full scan because startKey does not apply here // For example, if startKey = "account.owner=an" and search query = "accoutn.owner CONSISTS an" // we can't iterate with prefix "account.owner=an" because we might miss keys like "account.owner=Ulan" - it := txi.store.Iterator() - defer it.Release() - for it.Next() { + it := txi.store.Iterator(nil, nil) + defer it.Close() + for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } @@ -296,10 +296,10 @@ func (txi *TxIndex) match(c query.Condition, startKey []byte) (hashes [][]byte) } func (txi *TxIndex) matchRange(r queryRange, startKey []byte) (hashes [][]byte) { - it := txi.store.IteratorPrefix(startKey) - defer it.Release() + it := dbm.IteratePrefix(txi.store, startKey) + defer it.Close() LOOP: - for it.Next() { + for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } @@ -376,17 +376,8 @@ func extractValueFromKey(key []byte) string { return parts[1] } -func keyForTag(tag *abci.KVPair, result *types.TxResult) []byte { - switch tag.ValueType { - case abci.KVPair_STRING: - return []byte(fmt.Sprintf("%s/%v/%d/%d", tag.Key, tag.ValueString, result.Height, result.Index)) - case abci.KVPair_INT: - return []byte(fmt.Sprintf("%s/%v/%d/%d", tag.Key, tag.ValueInt, result.Height, result.Index)) - // case abci.KVPair_TIME: - // return []byte(fmt.Sprintf("%s/%d/%d/%d", tag.Key, tag.ValueTime.Unix(), result.Height, result.Index)) - default: - panic(fmt.Sprintf("Undefined value type: %v", tag.ValueType)) - } +func keyForTag(tag cmn.KVPair, result *types.TxResult) []byte { + return []byte(fmt.Sprintf("%s/%s/%d/%d", tag.Key, tag.Value, result.Height, result.Index)) } /////////////////////////////////////////////////////////////////////////////// diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index 0eac17607..ba2830e81 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -11,6 +11,7 @@ import ( abci "github.com/tendermint/abci/types" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" db "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/pubsub/query" ) @@ -19,7 +20,7 @@ func TestTxIndex(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) tx := types.Tx("HELLO WORLD") - txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []*abci.KVPair{}}} + txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} hash := tx.Hash() batch := txindex.NewBatch(1) @@ -34,7 +35,7 @@ func TestTxIndex(t *testing.T) { assert.Equal(t, txResult, loadedTxResult) tx2 := types.Tx("BYE BYE WORLD") - txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []*abci.KVPair{}}} + txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} hash2 := tx2.Hash() err = indexer.Index(txResult2) @@ -49,10 +50,10 @@ func TestTxSearch(t *testing.T) { allowedTags := []string{"account.number", "account.owner", "account.date"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) - txResult := txResultWithTags([]*abci.KVPair{ - {Key: "account.number", ValueType: abci.KVPair_INT, ValueInt: 1}, - {Key: "account.owner", ValueType: abci.KVPair_STRING, ValueString: "Ivan"}, - {Key: "not_allowed", ValueType: abci.KVPair_STRING, ValueString: "Vlad"}, + txResult := txResultWithTags([]cmn.KVPair{ + {Key: []byte("account.number"), Value: []byte("1")}, + {Key: []byte("account.owner"), Value: []byte("Ivan")}, + {Key: []byte("not_allowed"), Value: []byte("Vlad")}, }) hash := txResult.Tx.Hash() @@ -106,9 +107,9 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { allowedTags := []string{"account.number"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) - txResult := txResultWithTags([]*abci.KVPair{ - {Key: "account.number", ValueType: abci.KVPair_INT, ValueInt: 1}, - {Key: "account.number", ValueType: abci.KVPair_INT, ValueInt: 2}, + txResult := txResultWithTags([]cmn.KVPair{ + {Key: []byte("account.number"), Value: []byte("1")}, + {Key: []byte("account.number"), Value: []byte("2")}, }) err := indexer.Index(txResult) @@ -124,9 +125,9 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { func TestIndexAllTags(t *testing.T) { indexer := NewTxIndex(db.NewMemDB(), IndexAllTags()) - txResult := txResultWithTags([]*abci.KVPair{ - abci.KVPairString("account.owner", "Ivan"), - abci.KVPairInt("account.number", 1), + txResult := txResultWithTags([]cmn.KVPair{ + cmn.KVPair{[]byte("account.owner"), []byte("Ivan")}, + cmn.KVPair{[]byte("account.number"), []byte("1")}, }) err := indexer.Index(txResult) @@ -143,14 +144,14 @@ func TestIndexAllTags(t *testing.T) { assert.Equal(t, []*types.TxResult{txResult}, results) } -func txResultWithTags(tags []*abci.KVPair) *types.TxResult { +func txResultWithTags(tags []cmn.KVPair) *types.TxResult { tx := types.Tx("HELLO WORLD") return &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: tags}} } func benchmarkTxIndex(txsCount int, b *testing.B) { tx := types.Tx("HELLO WORLD") - txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []*abci.KVPair{}}} + txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: []cmn.KVPair{}}} dir, err := ioutil.TempDir("", "tx_index_db") if err != nil { diff --git a/test/app/grpc_client b/test/app/grpc_client new file mode 100755 index 000000000..ff72d1858 Binary files /dev/null and b/test/app/grpc_client differ diff --git a/test/p2p/README.md b/test/p2p/README.md index e2a577cfa..b68f7a819 100644 --- a/test/p2p/README.md +++ b/test/p2p/README.md @@ -38,7 +38,7 @@ for i in $(seq 1 4); do --name local_testnet_$i \ --entrypoint tendermint \ -e TMHOME=/go/src/github.com/tendermint/tendermint/test/p2p/data/mach$i/core \ - tendermint_tester node --p2p.seeds 172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 --proxy_app=dummy + tendermint_tester node --p2p.persistent_peers 172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 --proxy_app=dummy done ``` diff --git a/test/p2p/data/mach1/core/genesis.json b/test/p2p/data/mach1/core/config/genesis.json similarity index 100% rename from test/p2p/data/mach1/core/genesis.json rename to test/p2p/data/mach1/core/config/genesis.json diff --git a/test/p2p/data/mach1/core/priv_validator.json b/test/p2p/data/mach1/core/config/priv_validator.json similarity index 100% rename from test/p2p/data/mach1/core/priv_validator.json rename to test/p2p/data/mach1/core/config/priv_validator.json diff --git a/test/p2p/data/mach2/core/genesis.json b/test/p2p/data/mach2/core/config/genesis.json similarity index 100% rename from test/p2p/data/mach2/core/genesis.json rename to test/p2p/data/mach2/core/config/genesis.json diff --git a/test/p2p/data/mach2/core/priv_validator.json b/test/p2p/data/mach2/core/config/priv_validator.json similarity index 100% rename from test/p2p/data/mach2/core/priv_validator.json rename to test/p2p/data/mach2/core/config/priv_validator.json diff --git a/test/p2p/data/mach3/core/genesis.json b/test/p2p/data/mach3/core/config/genesis.json similarity index 100% rename from test/p2p/data/mach3/core/genesis.json rename to test/p2p/data/mach3/core/config/genesis.json diff --git a/test/p2p/data/mach3/core/priv_validator.json b/test/p2p/data/mach3/core/config/priv_validator.json similarity index 100% rename from test/p2p/data/mach3/core/priv_validator.json rename to test/p2p/data/mach3/core/config/priv_validator.json diff --git a/test/p2p/data/mach4/core/genesis.json b/test/p2p/data/mach4/core/config/genesis.json similarity index 100% rename from test/p2p/data/mach4/core/genesis.json rename to test/p2p/data/mach4/core/config/genesis.json diff --git a/test/p2p/data/mach4/core/priv_validator.json b/test/p2p/data/mach4/core/config/priv_validator.json similarity index 100% rename from test/p2p/data/mach4/core/priv_validator.json rename to test/p2p/data/mach4/core/config/priv_validator.json diff --git a/test/p2p/fast_sync/test_peer.sh b/test/p2p/fast_sync/test_peer.sh index 8174be0e7..1f341bf5d 100644 --- a/test/p2p/fast_sync/test_peer.sh +++ b/test/p2p/fast_sync/test_peer.sh @@ -23,11 +23,11 @@ docker rm -vf local_testnet_$ID set -e # restart peer - should have an empty blockchain -SEEDS="$(test/p2p/ip.sh 1):46656" +PERSISTENT_PEERS="$(test/p2p/ip.sh 1):46656" for j in `seq 2 $N`; do - SEEDS="$SEEDS,$(test/p2p/ip.sh $j):46656" + PERSISTENT_PEERS="$PERSISTENT_PEERS,$(test/p2p/ip.sh $j):46656" done -bash test/p2p/peer.sh $DOCKER_IMAGE $NETWORK_NAME $ID $PROXY_APP "--p2p.seeds $SEEDS --p2p.pex --rpc.unsafe" +bash test/p2p/peer.sh $DOCKER_IMAGE $NETWORK_NAME $ID $PROXY_APP "--p2p.persistent_peers $PERSISTENT_PEERS --p2p.pex --rpc.unsafe" # wait for peer to sync and check the app hash bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME fs_$ID "test/p2p/fast_sync/check_peer.sh $ID" diff --git a/test/p2p/local_testnet_start.sh b/test/p2p/local_testnet_start.sh index f70bdf04c..25b3c6d3e 100644 --- a/test/p2p/local_testnet_start.sh +++ b/test/p2p/local_testnet_start.sh @@ -7,10 +7,10 @@ N=$3 APP_PROXY=$4 set +u -SEEDS=$5 -if [[ "$SEEDS" != "" ]]; then - echo "Seeds: $SEEDS" - SEEDS="--p2p.seeds $SEEDS" +PERSISTENT_PEERS=$5 +if [[ "$PERSISTENT_PEERS" != "" ]]; then + echo "PersistentPeers: $PERSISTENT_PEERS" + PERSISTENT_PEERS="--p2p.persistent_peers $PERSISTENT_PEERS" fi set -u @@ -20,5 +20,5 @@ cd "$GOPATH/src/github.com/tendermint/tendermint" docker network create --driver bridge --subnet 172.57.0.0/16 "$NETWORK_NAME" for i in $(seq 1 "$N"); do - bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$i" "$APP_PROXY" "$SEEDS --p2p.pex --rpc.unsafe" + bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$i" "$APP_PROXY" "$PERSISTENT_PEERS --p2p.pex --rpc.unsafe" done diff --git a/test/p2p/persistent_peers.sh b/test/p2p/persistent_peers.sh new file mode 100644 index 000000000..4ad55bc03 --- /dev/null +++ b/test/p2p/persistent_peers.sh @@ -0,0 +1,12 @@ +#! /bin/bash +set -eu + +N=$1 + +cd "$GOPATH/src/github.com/tendermint/tendermint" + +persistent_peers="$(test/p2p/ip.sh 1):46656" +for i in $(seq 2 $N); do + persistent_peers="$persistent_peers,$(test/p2p/ip.sh $i):46656" +done +echo "$persistent_peers" diff --git a/test/p2p/pex/dial_seeds.sh b/test/p2p/pex/dial_peers.sh similarity index 63% rename from test/p2p/pex/dial_seeds.sh rename to test/p2p/pex/dial_peers.sh index 15c22af67..ddda7dbe7 100644 --- a/test/p2p/pex/dial_seeds.sh +++ b/test/p2p/pex/dial_peers.sh @@ -1,4 +1,4 @@ -#! /bin/bash +#! /bin/bash set -u N=$1 @@ -11,7 +11,7 @@ for i in `seq 1 $N`; do curl -s $addr/status > /dev/null ERR=$? while [ "$ERR" != 0 ]; do - sleep 1 + sleep 1 curl -s $addr/status > /dev/null ERR=$? done @@ -19,13 +19,14 @@ for i in `seq 1 $N`; do done set -e -# seeds need quotes -seeds="\"$(test/p2p/ip.sh 1):46656\"" +# peers need quotes +peers="\"$(test/p2p/ip.sh 1):46656\"" for i in `seq 2 $N`; do - seeds="$seeds,\"$(test/p2p/ip.sh $i):46656\"" + peers="$peers,\"$(test/p2p/ip.sh $i):46656\"" done -echo $seeds +echo $peers -echo $seeds +echo $peers IP=$(test/p2p/ip.sh 1) -curl --data-urlencode "seeds=[$seeds]" "$IP:46657/dial_seeds" +curl "$IP:46657/dial_peers?persistent=true&peers=\[$peers\]" + diff --git a/test/p2p/pex/test.sh b/test/p2p/pex/test.sh index d54d81350..ffecd6510 100644 --- a/test/p2p/pex/test.sh +++ b/test/p2p/pex/test.sh @@ -6,10 +6,10 @@ NETWORK_NAME=$2 N=$3 PROXY_APP=$4 -cd $GOPATH/src/github.com/tendermint/tendermint +cd "$GOPATH/src/github.com/tendermint/tendermint" echo "Test reconnecting from the address book" -bash test/p2p/pex/test_addrbook.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP +bash test/p2p/pex/test_addrbook.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$N" "$PROXY_APP" -echo "Test connecting via /dial_seeds" -bash test/p2p/pex/test_dial_seeds.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP +echo "Test connecting via /dial_peers" +bash test/p2p/pex/test_dial_peers.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$N" "$PROXY_APP" diff --git a/test/p2p/pex/test_addrbook.sh b/test/p2p/pex/test_addrbook.sh index 35dcb89d3..d54bcf428 100644 --- a/test/p2p/pex/test_addrbook.sh +++ b/test/p2p/pex/test_addrbook.sh @@ -9,7 +9,7 @@ PROXY_APP=$4 ID=1 echo "----------------------------------------------------------------------" -echo "Testing pex creates the addrbook and uses it if seeds are not provided" +echo "Testing pex creates the addrbook and uses it if persistent_peers are not provided" echo "(assuming peers are started with pex enabled)" CLIENT_NAME="pex_addrbook_$ID" @@ -17,25 +17,25 @@ CLIENT_NAME="pex_addrbook_$ID" echo "1. restart peer $ID" docker stop "local_testnet_$ID" # preserve addrbook.json -docker cp "local_testnet_$ID:/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/addrbook.json" "/tmp/addrbook.json" +docker cp "local_testnet_$ID:/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/config/addrbook.json" "/tmp/addrbook.json" set +e #CIRCLE docker rm -vf "local_testnet_$ID" set -e -# NOTE that we do not provide seeds +# NOTE that we do not provide persistent_peers bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$ID" "$PROXY_APP" "--p2p.pex --rpc.unsafe" -docker cp "/tmp/addrbook.json" "local_testnet_$ID:/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/addrbook.json" +docker cp "/tmp/addrbook.json" "local_testnet_$ID:/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/config/addrbook.json" echo "with the following addrbook:" cat /tmp/addrbook.json # exec doesn't work on circle -# docker exec "local_testnet_$ID" cat "/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/addrbook.json" +# docker exec "local_testnet_$ID" cat "/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/config/addrbook.json" echo "" # if the client runs forever, it means addrbook wasn't saved or was empty bash test/p2p/client.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$CLIENT_NAME" "test/p2p/pex/check_peer.sh $ID $N" echo "----------------------------------------------------------------------" -echo "Testing other peers connect to us if we have neither seeds nor the addrbook" +echo "Testing other peers connect to us if we have neither persistent_peers nor the addrbook" echo "(assuming peers are started with pex enabled)" CLIENT_NAME="pex_no_addrbook_$ID" @@ -44,9 +44,9 @@ echo "1. restart peer $ID" docker stop "local_testnet_$ID" set +e #CIRCLE docker rm -vf "local_testnet_$ID" -set -e +set -e -# NOTE that we do not provide seeds +# NOTE that we do not provide persistent_peers bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$ID" "$PROXY_APP" "--p2p.pex --rpc.unsafe" # if the client runs forever, it means other peers have removed us from their books (which should not happen) diff --git a/test/p2p/pex/test_dial_seeds.sh b/test/p2p/pex/test_dial_peers.sh similarity index 77% rename from test/p2p/pex/test_dial_seeds.sh rename to test/p2p/pex/test_dial_peers.sh index ea72004db..d0b042342 100644 --- a/test/p2p/pex/test_dial_seeds.sh +++ b/test/p2p/pex/test_dial_peers.sh @@ -11,7 +11,7 @@ ID=1 cd $GOPATH/src/github.com/tendermint/tendermint echo "----------------------------------------------------------------------" -echo "Testing full network connection using one /dial_seeds call" +echo "Testing full network connection using one /dial_peers call" echo "(assuming peers are started with pex enabled)" # stop the existing testnet and remove local network @@ -21,16 +21,16 @@ set -e # start the testnet on a local network # NOTE we re-use the same network for all tests -SEEDS="" -bash test/p2p/local_testnet_start.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP $SEEDS +PERSISTENT_PEERS="" +bash test/p2p/local_testnet_start.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP $PERSISTENT_PEERS -# dial seeds from one node -CLIENT_NAME="dial_seeds" -bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/pex/dial_seeds.sh $N" +# dial peers from one node +CLIENT_NAME="dial_peers" +bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/pex/dial_peers.sh $N" # test basic connectivity and consensus # start client container and check the num peers and height for all nodes -CLIENT_NAME="dial_seeds_basic" +CLIENT_NAME="dial_peers_basic" bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/basic/test.sh $N" diff --git a/test/p2p/seeds.sh b/test/p2p/seeds.sh deleted file mode 100644 index 4bf866cba..000000000 --- a/test/p2p/seeds.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash -set -eu - -N=$1 - -cd "$GOPATH/src/github.com/tendermint/tendermint" - -seeds="$(test/p2p/ip.sh 1):46656" -for i in $(seq 2 $N); do - seeds="$seeds,$(test/p2p/ip.sh $i):46656" -done -echo "$seeds" diff --git a/test/p2p/test.sh b/test/p2p/test.sh index 6a5537b98..c95f69733 100644 --- a/test/p2p/test.sh +++ b/test/p2p/test.sh @@ -13,11 +13,11 @@ set +e bash test/p2p/local_testnet_stop.sh "$NETWORK_NAME" "$N" set -e -SEEDS=$(bash test/p2p/seeds.sh $N) +PERSISTENT_PEERS=$(bash test/p2p/persistent_peers.sh $N) # start the testnet on a local network # NOTE we re-use the same network for all tests -bash test/p2p/local_testnet_start.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$N" "$PROXY_APP" "$SEEDS" +bash test/p2p/local_testnet_start.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$N" "$PROXY_APP" "$PERSISTENT_PEERS" # test basic connectivity and consensus # start client container and check the num peers and height for all nodes diff --git a/test/run_test.sh b/test/run_test.sh index ae2ff6b41..b505126ea 100644 --- a/test/run_test.sh +++ b/test/run_test.sh @@ -6,9 +6,6 @@ pwd BRANCH=$(git rev-parse --abbrev-ref HEAD) echo "Current branch: $BRANCH" -# run the linter -make metalinter - # run the go unit tests with coverage bash test/test_cover.sh diff --git a/types/block.go b/types/block.go index 6aa97c2d6..3e800e722 100644 --- a/types/block.go +++ b/types/block.go @@ -9,9 +9,9 @@ import ( "time" wire "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" + "golang.org/x/crypto/ripemd160" ) // Block defines the atomic unit of a Tendermint blockchain. @@ -85,7 +85,7 @@ func (b *Block) FillHeader() { // Hash computes and returns the block hash. // If the block is incomplete, block hash is nil for safety. -func (b *Block) Hash() data.Bytes { +func (b *Block) Hash() cmn.HexBytes { if b == nil || b.Header == nil || b.Data == nil || b.LastCommit == nil { return nil } @@ -160,39 +160,39 @@ type Header struct { TotalTxs int64 `json:"total_txs"` // hashes of block data - LastCommitHash data.Bytes `json:"last_commit_hash"` // commit from validators from the last block - DataHash data.Bytes `json:"data_hash"` // transactions + LastCommitHash cmn.HexBytes `json:"last_commit_hash"` // commit from validators from the last block + DataHash cmn.HexBytes `json:"data_hash"` // transactions // hashes from the app output from the prev block - ValidatorsHash data.Bytes `json:"validators_hash"` // validators for the current block - ConsensusHash data.Bytes `json:"consensus_hash"` // consensus params for current block - AppHash data.Bytes `json:"app_hash"` // state after txs from the previous block - LastResultsHash data.Bytes `json:"last_results_hash"` // root hash of all results from the txs from the previous block + ValidatorsHash cmn.HexBytes `json:"validators_hash"` // validators for the current block + ConsensusHash cmn.HexBytes `json:"consensus_hash"` // consensus params for current block + AppHash cmn.HexBytes `json:"app_hash"` // state after txs from the previous block + LastResultsHash cmn.HexBytes `json:"last_results_hash"` // root hash of all results from the txs from the previous block // consensus info - EvidenceHash data.Bytes `json:"evidence_hash"` // evidence included in the block + EvidenceHash cmn.HexBytes `json:"evidence_hash"` // evidence included in the block } // Hash returns the hash of the header. // Returns nil if ValidatorHash is missing. -func (h *Header) Hash() data.Bytes { +func (h *Header) Hash() cmn.HexBytes { if len(h.ValidatorsHash) == 0 { return nil } - return merkle.SimpleHashFromMap(map[string]interface{}{ - "ChainID": h.ChainID, - "Height": h.Height, - "Time": h.Time, - "NumTxs": h.NumTxs, - "TotalTxs": h.TotalTxs, - "LastBlockID": h.LastBlockID, - "LastCommit": h.LastCommitHash, - "Data": h.DataHash, - "Validators": h.ValidatorsHash, - "App": h.AppHash, - "Consensus": h.ConsensusHash, - "Results": h.LastResultsHash, - "Evidence": h.EvidenceHash, + return merkle.SimpleHashFromMap(map[string]merkle.Hasher{ + "ChainID": wireHasher(h.ChainID), + "Height": wireHasher(h.Height), + "Time": wireHasher(h.Time), + "NumTxs": wireHasher(h.NumTxs), + "TotalTxs": wireHasher(h.TotalTxs), + "LastBlockID": wireHasher(h.LastBlockID), + "LastCommit": wireHasher(h.LastCommitHash), + "Data": wireHasher(h.DataHash), + "Validators": wireHasher(h.ValidatorsHash), + "App": wireHasher(h.AppHash), + "Consensus": wireHasher(h.ConsensusHash), + "Results": wireHasher(h.LastResultsHash), + "Evidence": wireHasher(h.EvidenceHash), }) } @@ -245,7 +245,7 @@ type Commit struct { // Volatile firstPrecommit *Vote - hash data.Bytes + hash cmn.HexBytes bitArray *cmn.BitArray } @@ -354,13 +354,13 @@ func (commit *Commit) ValidateBasic() error { } // Hash returns the hash of the commit -func (commit *Commit) Hash() data.Bytes { +func (commit *Commit) Hash() cmn.HexBytes { if commit.hash == nil { - bs := make([]interface{}, len(commit.Precommits)) + bs := make([]merkle.Hasher, len(commit.Precommits)) for i, precommit := range commit.Precommits { - bs[i] = precommit + bs[i] = wireHasher(precommit) } - commit.hash = merkle.SimpleHashFromBinaries(bs) + commit.hash = merkle.SimpleHashFromHashers(bs) } return commit.hash } @@ -402,11 +402,11 @@ type Data struct { Txs Txs `json:"txs"` // Volatile - hash data.Bytes + hash cmn.HexBytes } // Hash returns the hash of the data -func (data *Data) Hash() data.Bytes { +func (data *Data) Hash() cmn.HexBytes { if data.hash == nil { data.hash = data.Txs.Hash() // NOTE: leaves of merkle tree are TxIDs } @@ -440,11 +440,11 @@ type EvidenceData struct { Evidence EvidenceList `json:"evidence"` // Volatile - hash data.Bytes + hash cmn.HexBytes } // Hash returns the hash of the data. -func (data *EvidenceData) Hash() data.Bytes { +func (data *EvidenceData) Hash() cmn.HexBytes { if data.hash == nil { data.hash = data.Evidence.Hash() } @@ -476,7 +476,7 @@ func (data *EvidenceData) StringIndented(indent string) string { // BlockID defines the unique ID of a block as its Hash and its PartSetHeader type BlockID struct { - Hash data.Bytes `json:"hash"` + Hash cmn.HexBytes `json:"hash"` PartsHeader PartSetHeader `json:"parts"` } @@ -510,3 +510,23 @@ func (blockID BlockID) WriteSignBytes(w io.Writer, n *int, err *error) { func (blockID BlockID) String() string { return fmt.Sprintf(`%v:%v`, blockID.Hash, blockID.PartsHeader) } + +//------------------------------------------------------- + +type hasher struct { + item interface{} +} + +func (h hasher) Hash() []byte { + hasher, n, err := ripemd160.New(), new(int), new(error) + wire.WriteBinary(h.item, hasher, n, err) + if *err != nil { + panic(err) + } + return hasher.Sum(nil) + +} + +func wireHasher(item interface{}) merkle.Hasher { + return hasher{item} +} diff --git a/types/canonical_json.go b/types/canonical_json.go index 41c67c24b..45d12b45f 100644 --- a/types/canonical_json.go +++ b/types/canonical_json.go @@ -4,7 +4,7 @@ import ( "time" wire "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" ) // canonical json is go-wire's json for structs with fields in alphabetical order @@ -13,13 +13,13 @@ import ( const timeFormat = wire.RFC3339Millis type CanonicalJSONBlockID struct { - Hash data.Bytes `json:"hash,omitempty"` + Hash cmn.HexBytes `json:"hash,omitempty"` PartsHeader CanonicalJSONPartSetHeader `json:"parts,omitempty"` } type CanonicalJSONPartSetHeader struct { - Hash data.Bytes `json:"hash"` - Total int `json:"total"` + Hash cmn.HexBytes `json:"hash"` + Total int `json:"total"` } type CanonicalJSONProposal struct { @@ -40,11 +40,11 @@ type CanonicalJSONVote struct { } type CanonicalJSONHeartbeat struct { - Height int64 `json:"height"` - Round int `json:"round"` - Sequence int `json:"sequence"` - ValidatorAddress data.Bytes `json:"validator_address"` - ValidatorIndex int `json:"validator_index"` + Height int64 `json:"height"` + Round int `json:"round"` + Sequence int `json:"sequence"` + ValidatorAddress Address `json:"validator_address"` + ValidatorIndex int `json:"validator_index"` } //------------------------------------ diff --git a/types/event_buffer.go b/types/event_buffer.go index 6f236e8ef..18b41014e 100644 --- a/types/event_buffer.go +++ b/types/event_buffer.go @@ -41,6 +41,10 @@ func (b *TxEventBuffer) Flush() error { return err } } - b.events = make([]EventDataTx, 0, b.capacity) + + // Clear out the elements and set the length to 0 + // but maintain the underlying slice's capacity. + // See Issue https://github.com/tendermint/tendermint/issues/1189 + b.events = b.events[:0] return nil } diff --git a/types/event_bus.go b/types/event_bus.go index 6b6069b90..4edaea588 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - abci "github.com/tendermint/abci/types" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" tmpubsub "github.com/tendermint/tmlibs/pubsub" @@ -98,17 +97,11 @@ func (b *EventBus) PublishEventTx(event EventDataTx) error { // validate and fill tags from tx result for _, tag := range event.Result.Tags { // basic validation - if tag.Key == "" { + if len(tag.Key) == 0 { b.Logger.Info("Got tag with an empty key (skipping)", "tag", tag, "tx", event.Tx) continue } - - switch tag.ValueType { - case abci.KVPair_STRING: - tags[tag.Key] = tag.ValueString - case abci.KVPair_INT: - tags[tag.Key] = tag.ValueInt - } + tags[string(tag.Key)] = tag.Value } // add predefined tags diff --git a/types/evidence.go b/types/evidence.go index 3ae3e40b1..9973c62e6 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -120,7 +120,7 @@ func (dve *DuplicateVoteEvidence) Index() int { // Hash returns the hash of the evidence. func (dve *DuplicateVoteEvidence) Hash() []byte { - return merkle.SimpleHashFromBinary(dve) + return wireHasher(dve).Hash() } // Verify returns an error if the two votes aren't conflicting. @@ -165,7 +165,9 @@ func (dve *DuplicateVoteEvidence) Equal(ev Evidence) bool { } // just check their hashes - return bytes.Equal(merkle.SimpleHashFromBinary(dve), merkle.SimpleHashFromBinary(ev)) + dveHash := wireHasher(dve).Hash() + evHash := wireHasher(ev).Hash() + return bytes.Equal(dveHash, evHash) } //----------------------------------------------------------------- diff --git a/types/genesis.go b/types/genesis.go index e33f60258..b7e4f6452 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" crypto "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) @@ -28,7 +27,7 @@ type GenesisDoc struct { ChainID string `json:"chain_id"` ConsensusParams *ConsensusParams `json:"consensus_params,omitempty"` Validators []GenesisValidator `json:"validators"` - AppHash data.Bytes `json:"app_hash"` + AppHash cmn.HexBytes `json:"app_hash"` AppOptions interface{} `json:"app_options,omitempty"` } diff --git a/types/heartbeat.go b/types/heartbeat.go index da9b342b4..a4ff0d217 100644 --- a/types/heartbeat.go +++ b/types/heartbeat.go @@ -6,7 +6,6 @@ import ( "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) @@ -16,7 +15,7 @@ import ( // json field tags because we always want the JSON // representation to be in its canonical form. type Heartbeat struct { - ValidatorAddress data.Bytes `json:"validator_address"` + ValidatorAddress Address `json:"validator_address"` ValidatorIndex int `json:"validator_index"` Height int64 `json:"height"` Round int `json:"round"` diff --git a/types/params.go b/types/params.go index 90ab5f659..0e8ac577f 100644 --- a/types/params.go +++ b/types/params.go @@ -106,13 +106,13 @@ func (params *ConsensusParams) Validate() error { // Hash returns a merkle hash of the parameters to store // in the block header func (params *ConsensusParams) Hash() []byte { - return merkle.SimpleHashFromMap(map[string]interface{}{ - "block_gossip_part_size_bytes": params.BlockGossip.BlockPartSizeBytes, - "block_size_max_bytes": params.BlockSize.MaxBytes, - "block_size_max_gas": params.BlockSize.MaxGas, - "block_size_max_txs": params.BlockSize.MaxTxs, - "tx_size_max_bytes": params.TxSize.MaxBytes, - "tx_size_max_gas": params.TxSize.MaxGas, + return merkle.SimpleHashFromMap(map[string]merkle.Hasher{ + "block_gossip_part_size_bytes": wireHasher(params.BlockGossip.BlockPartSizeBytes), + "block_size_max_bytes": wireHasher(params.BlockSize.MaxBytes), + "block_size_max_gas": wireHasher(params.BlockSize.MaxGas), + "block_size_max_txs": wireHasher(params.BlockSize.MaxTxs), + "tx_size_max_bytes": wireHasher(params.TxSize.MaxBytes), + "tx_size_max_gas": wireHasher(params.TxSize.MaxGas), }) } diff --git a/types/part_set.go b/types/part_set.go index e8a0997c0..ff11f7d35 100644 --- a/types/part_set.go +++ b/types/part_set.go @@ -10,7 +10,6 @@ import ( "golang.org/x/crypto/ripemd160" "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" ) @@ -22,7 +21,7 @@ var ( type Part struct { Index int `json:"index"` - Bytes data.Bytes `json:"bytes"` + Bytes cmn.HexBytes `json:"bytes"` Proof merkle.SimpleProof `json:"proof"` // Cache @@ -58,8 +57,8 @@ func (part *Part) StringIndented(indent string) string { //------------------------------------- type PartSetHeader struct { - Total int `json:"total"` - Hash data.Bytes `json:"hash"` + Total int `json:"total"` + Hash cmn.HexBytes `json:"hash"` } func (psh PartSetHeader) String() string { @@ -96,7 +95,7 @@ func NewPartSetFromData(data []byte, partSize int) *PartSet { // divide data into 4kb parts. total := (len(data) + partSize - 1) / partSize parts := make([]*Part, total) - parts_ := make([]merkle.Hashable, total) + parts_ := make([]merkle.Hasher, total) partsBitArray := cmn.NewBitArray(total) for i := 0; i < total; i++ { part := &Part{ @@ -108,7 +107,7 @@ func NewPartSetFromData(data []byte, partSize int) *PartSet { partsBitArray.SetIndex(i, true) } // Compute merkle proofs - root, proofs := merkle.SimpleProofsFromHashables(parts_) + root, proofs := merkle.SimpleProofsFromHashers(parts_) for i := 0; i < total; i++ { parts[i].Proof = *proofs[i] } diff --git a/types/priv_validator.go b/types/priv_validator.go index 31c65eeb4..062fe09d2 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -11,16 +11,15 @@ import ( "time" crypto "github.com/tendermint/go-crypto" - data "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) // TODO: type ? const ( - stepNone = 0 // Used to distinguish the initial state - stepPropose = 1 - stepPrevote = 2 - stepPrecommit = 3 + stepNone int8 = 0 // Used to distinguish the initial state + stepPropose int8 = 1 + stepPrevote int8 = 2 + stepPrecommit int8 = 3 ) func voteToStep(vote *Vote) int8 { @@ -38,7 +37,7 @@ func voteToStep(vote *Vote) int8 { // PrivValidator defines the functionality of a local Tendermint validator // that signs votes, proposals, and heartbeats, and never double signs. type PrivValidator interface { - GetAddress() data.Bytes // redundant since .PubKey().Address() + GetAddress() Address // redundant since .PubKey().Address() GetPubKey() crypto.PubKey SignVote(chainID string, vote *Vote) error @@ -50,13 +49,13 @@ type PrivValidator interface { // to prevent double signing. The Signer itself can be mutated to use // something besides the default, for instance a hardware signer. type PrivValidatorFS struct { - Address data.Bytes `json:"address"` + Address Address `json:"address"` PubKey crypto.PubKey `json:"pub_key"` LastHeight int64 `json:"last_height"` LastRound int `json:"last_round"` LastStep int8 `json:"last_step"` LastSignature crypto.Signature `json:"last_signature,omitempty"` // so we dont lose signatures - LastSignBytes data.Bytes `json:"last_signbytes,omitempty"` // so we dont lose signatures + LastSignBytes cmn.HexBytes `json:"last_signbytes,omitempty"` // so we dont lose signatures // PrivKey should be empty if a Signer other than the default is being used. PrivKey crypto.PrivKey `json:"priv_key"` @@ -96,7 +95,7 @@ func (ds *DefaultSigner) Sign(msg []byte) (crypto.Signature, error) { // GetAddress returns the address of the validator. // Implements PrivValidator. -func (pv *PrivValidatorFS) GetAddress() data.Bytes { +func (pv *PrivValidatorFS) GetAddress() Address { return pv.Address } @@ -199,12 +198,9 @@ func (privVal *PrivValidatorFS) Reset() { func (privVal *PrivValidatorFS) SignVote(chainID string, vote *Vote) error { privVal.mtx.Lock() defer privVal.mtx.Unlock() - signature, err := privVal.signBytesHRS(vote.Height, vote.Round, voteToStep(vote), - SignBytes(chainID, vote), checkVotesOnlyDifferByTimestamp) - if err != nil { + if err := privVal.signVote(chainID, vote); err != nil { return errors.New(cmn.Fmt("Error signing vote: %v", err)) } - vote.Signature = signature return nil } @@ -213,12 +209,9 @@ func (privVal *PrivValidatorFS) SignVote(chainID string, vote *Vote) error { func (privVal *PrivValidatorFS) SignProposal(chainID string, proposal *Proposal) error { privVal.mtx.Lock() defer privVal.mtx.Unlock() - signature, err := privVal.signBytesHRS(proposal.Height, proposal.Round, stepPropose, - SignBytes(chainID, proposal), checkProposalsOnlyDifferByTimestamp) - if err != nil { + if err := privVal.signProposal(chainID, proposal); err != nil { return fmt.Errorf("Error signing proposal: %v", err) } - proposal.Signature = signature return nil } @@ -250,36 +243,82 @@ func (privVal *PrivValidatorFS) checkHRS(height int64, round int, step int8) (bo return false, nil } -// signBytesHRS signs the given signBytes if the height/round/step (HRS) are -// greater than the latest state. If the HRS are equal and the only thing changed is the timestamp, -// it returns the privValidator.LastSignature. Else it returns an error. -func (privVal *PrivValidatorFS) signBytesHRS(height int64, round int, step int8, - signBytes []byte, checkFn checkOnlyDifferByTimestamp) (crypto.Signature, error) { - sig := crypto.Signature{} +// signVote checks if the vote is good to sign and sets the vote signature. +// It may need to set the timestamp as well if the vote is otherwise the same as +// a previously signed vote (ie. we crashed after signing but before the vote hit the WAL). +func (privVal *PrivValidatorFS) signVote(chainID string, vote *Vote) error { + height, round, step := vote.Height, vote.Round, voteToStep(vote) + signBytes := SignBytes(chainID, vote) + + sameHRS, err := privVal.checkHRS(height, round, step) + if err != nil { + return err + } + + // We might crash before writing to the wal, + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error + if sameHRS { + if bytes.Equal(signBytes, privVal.LastSignBytes) { + vote.Signature = privVal.LastSignature + } else if timestamp, ok := checkVotesOnlyDifferByTimestamp(privVal.LastSignBytes, signBytes); ok { + vote.Timestamp = timestamp + vote.Signature = privVal.LastSignature + } else { + err = fmt.Errorf("Conflicting data") + } + return err + } + + // It passed the checks. Sign the vote + sig, err := privVal.Sign(signBytes) + if err != nil { + return err + } + privVal.saveSigned(height, round, step, signBytes, sig) + vote.Signature = sig + return nil +} + +// signProposal checks if the proposal is good to sign and sets the proposal signature. +// It may need to set the timestamp as well if the proposal is otherwise the same as +// a previously signed proposal ie. we crashed after signing but before the proposal hit the WAL). +func (privVal *PrivValidatorFS) signProposal(chainID string, proposal *Proposal) error { + height, round, step := proposal.Height, proposal.Round, stepPropose + signBytes := SignBytes(chainID, proposal) sameHRS, err := privVal.checkHRS(height, round, step) if err != nil { - return sig, err + return err } // We might crash before writing to the wal, - // causing us to try to re-sign for the same HRS + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error if sameHRS { - // if they're the same or only differ by timestamp, - // return the LastSignature. Otherwise, error - if bytes.Equal(signBytes, privVal.LastSignBytes) || - checkFn(privVal.LastSignBytes, signBytes) { - return privVal.LastSignature, nil + if bytes.Equal(signBytes, privVal.LastSignBytes) { + proposal.Signature = privVal.LastSignature + } else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(privVal.LastSignBytes, signBytes); ok { + proposal.Timestamp = timestamp + proposal.Signature = privVal.LastSignature + } else { + err = fmt.Errorf("Conflicting data") } - return sig, fmt.Errorf("Conflicting data") + return err } - sig, err = privVal.Sign(signBytes) + // It passed the checks. Sign the proposal + sig, err := privVal.Sign(signBytes) if err != nil { - return sig, err + return err } privVal.saveSigned(height, round, step, signBytes, sig) - return sig, nil + proposal.Signature = sig + return nil } // Persist height/round/step and signature @@ -329,10 +368,9 @@ func (pvs PrivValidatorsByAddress) Swap(i, j int) { //------------------------------------- -type checkOnlyDifferByTimestamp func([]byte, []byte) bool - -// returns true if the only difference in the votes is their timestamp -func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) bool { +// returns the timestamp from the lastSignBytes. +// returns true if the only difference in the votes is their timestamp. +func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { var lastVote, newVote CanonicalJSONOnceVote if err := json.Unmarshal(lastSignBytes, &lastVote); err != nil { panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err)) @@ -341,6 +379,11 @@ func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) bool { panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err)) } + lastTime, err := time.Parse(timeFormat, lastVote.Vote.Timestamp) + if err != nil { + panic(err) + } + // set the times to the same value and check equality now := CanonicalTime(time.Now()) lastVote.Vote.Timestamp = now @@ -348,11 +391,12 @@ func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) bool { lastVoteBytes, _ := json.Marshal(lastVote) newVoteBytes, _ := json.Marshal(newVote) - return bytes.Equal(newVoteBytes, lastVoteBytes) + return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes) } +// returns the timestamp from the lastSignBytes. // returns true if the only difference in the proposals is their timestamp -func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) bool { +func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { var lastProposal, newProposal CanonicalJSONOnceProposal if err := json.Unmarshal(lastSignBytes, &lastProposal); err != nil { panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err)) @@ -361,6 +405,11 @@ func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) boo panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err)) } + lastTime, err := time.Parse(timeFormat, lastProposal.Proposal.Timestamp) + if err != nil { + panic(err) + } + // set the times to the same value and check equality now := CanonicalTime(time.Now()) lastProposal.Proposal.Timestamp = now @@ -368,5 +417,5 @@ func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) boo lastProposalBytes, _ := json.Marshal(lastProposal) newProposalBytes, _ := json.Marshal(newProposal) - return bytes.Equal(newProposalBytes, lastProposalBytes) + return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes) } diff --git a/types/priv_validator_test.go b/types/priv_validator_test.go index 2fefee606..08b58273a 100644 --- a/types/priv_validator_test.go +++ b/types/priv_validator_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" crypto "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) @@ -173,7 +172,59 @@ func TestSignProposal(t *testing.T) { assert.Equal(sig, proposal.Signature) } -func newVote(addr data.Bytes, idx int, height int64, round int, typ byte, blockID BlockID) *Vote { +func TestDifferByTimestamp(t *testing.T) { + _, tempFilePath := cmn.Tempfile("priv_validator_") + privVal := GenPrivValidatorFS(tempFilePath) + + block1 := PartSetHeader{5, []byte{1, 2, 3}} + height, round := int64(10), 1 + chainID := "mychainid" + + // test proposal + { + proposal := newProposal(height, round, block1) + err := privVal.SignProposal(chainID, proposal) + assert.NoError(t, err, "expected no error signing proposal") + signBytes := SignBytes(chainID, proposal) + sig := proposal.Signature + timeStamp := clipToMS(proposal.Timestamp) + + // manipulate the timestamp. should get changed back + proposal.Timestamp = proposal.Timestamp.Add(time.Millisecond) + proposal.Signature = crypto.Signature{} + err = privVal.SignProposal("mychainid", proposal) + assert.NoError(t, err, "expected no error on signing same proposal") + + assert.Equal(t, timeStamp, proposal.Timestamp) + assert.Equal(t, signBytes, SignBytes(chainID, proposal)) + assert.Equal(t, sig, proposal.Signature) + } + + // test vote + { + voteType := VoteTypePrevote + blockID := BlockID{[]byte{1, 2, 3}, PartSetHeader{}} + vote := newVote(privVal.Address, 0, height, round, voteType, blockID) + err := privVal.SignVote("mychainid", vote) + assert.NoError(t, err, "expected no error signing vote") + + signBytes := SignBytes(chainID, vote) + sig := vote.Signature + timeStamp := clipToMS(vote.Timestamp) + + // manipulate the timestamp. should get changed back + vote.Timestamp = vote.Timestamp.Add(time.Millisecond) + vote.Signature = crypto.Signature{} + err = privVal.SignVote("mychainid", vote) + assert.NoError(t, err, "expected no error on signing same vote") + + assert.Equal(t, timeStamp, vote.Timestamp) + assert.Equal(t, signBytes, SignBytes(chainID, vote)) + assert.Equal(t, sig, vote.Signature) + } +} + +func newVote(addr Address, idx int, height int64, round int, typ byte, blockID BlockID) *Vote { return &Vote{ ValidatorAddress: addr, ValidatorIndex: idx, @@ -190,5 +241,13 @@ func newProposal(height int64, round int, partsHeader PartSetHeader) *Proposal { Height: height, Round: round, BlockPartsHeader: partsHeader, + Timestamp: time.Now().UTC(), } } + +func clipToMS(t time.Time) time.Time { + nano := t.UnixNano() + million := int64(1000000) + nano = (nano / million) * million + return time.Unix(0, nano).UTC() +} diff --git a/types/protobuf.go b/types/protobuf.go index 43c8f4505..e7ae20e39 100644 --- a/types/protobuf.go +++ b/types/protobuf.go @@ -10,8 +10,8 @@ var TM2PB = tm2pb{} type tm2pb struct{} -func (tm2pb) Header(header *Header) *types.Header { - return &types.Header{ +func (tm2pb) Header(header *Header) types.Header { + return types.Header{ ChainID: header.ChainID, Height: header.Height, Time: header.Time.Unix(), @@ -23,29 +23,29 @@ func (tm2pb) Header(header *Header) *types.Header { } } -func (tm2pb) BlockID(blockID BlockID) *types.BlockID { - return &types.BlockID{ +func (tm2pb) BlockID(blockID BlockID) types.BlockID { + return types.BlockID{ Hash: blockID.Hash, Parts: TM2PB.PartSetHeader(blockID.PartsHeader), } } -func (tm2pb) PartSetHeader(partSetHeader PartSetHeader) *types.PartSetHeader { - return &types.PartSetHeader{ +func (tm2pb) PartSetHeader(partSetHeader PartSetHeader) types.PartSetHeader { + return types.PartSetHeader{ Total: int32(partSetHeader.Total), // XXX: overflow Hash: partSetHeader.Hash, } } -func (tm2pb) Validator(val *Validator) *types.Validator { - return &types.Validator{ +func (tm2pb) Validator(val *Validator) types.Validator { + return types.Validator{ PubKey: val.PubKey.Bytes(), Power: val.VotingPower, } } -func (tm2pb) Validators(vals *ValidatorSet) []*types.Validator { - validators := make([]*types.Validator, len(vals.Validators)) +func (tm2pb) Validators(vals *ValidatorSet) []types.Validator { + validators := make([]types.Validator, len(vals.Validators)) for i, val := range vals.Validators { validators[i] = TM2PB.Validator(val) } diff --git a/types/results.go b/types/results.go index 29420fbc0..4a02f2857 100644 --- a/types/results.go +++ b/types/results.go @@ -3,7 +3,7 @@ package types import ( abci "github.com/tendermint/abci/types" wire "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" ) @@ -12,8 +12,8 @@ import ( // ABCIResult is the deterministic component of a ResponseDeliverTx. // TODO: add Tags type ABCIResult struct { - Code uint32 `json:"code"` - Data data.Bytes `json:"data"` + Code uint32 `json:"code"` + Data cmn.HexBytes `json:"data"` } // Hash returns the canonical hash of the ABCIResult @@ -47,20 +47,20 @@ func (a ABCIResults) Bytes() []byte { // Hash returns a merkle hash of all results func (a ABCIResults) Hash() []byte { - return merkle.SimpleHashFromHashables(a.toHashables()) + return merkle.SimpleHashFromHashers(a.toHashers()) } // ProveResult returns a merkle proof of one result from the set func (a ABCIResults) ProveResult(i int) merkle.SimpleProof { - _, proofs := merkle.SimpleProofsFromHashables(a.toHashables()) + _, proofs := merkle.SimpleProofsFromHashers(a.toHashers()) return *proofs[i] } -func (a ABCIResults) toHashables() []merkle.Hashable { +func (a ABCIResults) toHashers() []merkle.Hasher { l := len(a) - hashables := make([]merkle.Hashable, l) + hashers := make([]merkle.Hasher, l) for i := 0; i < l; i++ { - hashables[i] = a[i] + hashers[i] = a[i] } - return hashables + return hashers } diff --git a/types/services.go b/types/services.go index 6900fae7d..6b2be8a5c 100644 --- a/types/services.go +++ b/types/services.go @@ -27,6 +27,7 @@ type Mempool interface { Reap(int) Txs Update(height int64, txs Txs) error Flush() + FlushAppConn() error TxsAvailable() <-chan int64 EnableTxsAvailable() @@ -44,6 +45,7 @@ func (m MockMempool) CheckTx(tx Tx, cb func(*abci.Response)) error { return nil func (m MockMempool) Reap(n int) Txs { return Txs{} } func (m MockMempool) Update(height int64, txs Txs) error { return nil } func (m MockMempool) Flush() {} +func (m MockMempool) FlushAppConn() error { return nil } func (m MockMempool) TxsAvailable() <-chan int64 { return make(chan int64) } func (m MockMempool) EnableTxsAvailable() {} diff --git a/types/signable.go b/types/signable.go index 2134f6300..bfdf9faa1 100644 --- a/types/signable.go +++ b/types/signable.go @@ -5,7 +5,6 @@ import ( "io" cmn "github.com/tendermint/tmlibs/common" - "github.com/tendermint/tmlibs/merkle" ) // Signable is an interface for all signable things. @@ -23,8 +22,3 @@ func SignBytes(chainID string, o Signable) []byte { } return buf.Bytes() } - -// HashSignBytes is a convenience method for getting the hash of the bytes of a signable -func HashSignBytes(chainID string, o Signable) []byte { - return merkle.SimpleHashFromBinary(SignBytes(chainID, o)) -} diff --git a/types/tx.go b/types/tx.go index 4cf5843ae..d9e2e2ebd 100644 --- a/types/tx.go +++ b/types/tx.go @@ -6,7 +6,7 @@ import ( "fmt" abci "github.com/tendermint/abci/types" - "github.com/tendermint/go-wire/data" + cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/merkle" ) @@ -18,7 +18,7 @@ type Tx []byte // Hash computes the RIPEMD160 hash of the go-wire encoded transaction. func (tx Tx) Hash() []byte { - return merkle.SimpleHashFromBinary(tx) + return wireHasher(tx).Hash() } // String returns the hex-encoded transaction as a string. @@ -72,11 +72,11 @@ func (txs Txs) IndexByHash(hash []byte) int { // TODO: optimize this! func (txs Txs) Proof(i int) TxProof { l := len(txs) - hashables := make([]merkle.Hashable, l) + hashers := make([]merkle.Hasher, l) for i := 0; i < l; i++ { - hashables[i] = txs[i] + hashers[i] = txs[i] } - root, proofs := merkle.SimpleProofsFromHashables(hashables) + root, proofs := merkle.SimpleProofsFromHashers(hashers) return TxProof{ Index: i, @@ -90,7 +90,7 @@ func (txs Txs) Proof(i int) TxProof { // TxProof represents a Merkle proof of the presence of a transaction in the Merkle tree. type TxProof struct { Index, Total int - RootHash data.Bytes + RootHash cmn.HexBytes Data Tx Proof merkle.SimpleProof } diff --git a/types/validator.go b/types/validator.go index c5d064e01..dfe575515 100644 --- a/types/validator.go +++ b/types/validator.go @@ -7,7 +7,6 @@ import ( "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) @@ -15,9 +14,9 @@ import ( // NOTE: The Accum is not included in Validator.Hash(); // make sure to update that method if changes are made here type Validator struct { - Address data.Bytes `json:"address"` - PubKey crypto.PubKey `json:"pub_key"` - VotingPower int64 `json:"voting_power"` + Address Address `json:"address"` + PubKey crypto.PubKey `json:"pub_key"` + VotingPower int64 `json:"voting_power"` Accum int64 `json:"accum"` } @@ -74,7 +73,7 @@ func (v *Validator) String() string { // It excludes the Accum value, which changes with every round. func (v *Validator) Hash() []byte { return wire.BinaryRipemd160(struct { - Address data.Bytes + Address Address PubKey crypto.PubKey VotingPower int64 }{ diff --git a/types/validator_set.go b/types/validator_set.go index 134e4e06e..0d6c3249e 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -3,6 +3,7 @@ package types import ( "bytes" "fmt" + "math" "sort" "strings" @@ -20,7 +21,6 @@ import ( // upon calling .IncrementAccum(). // NOTE: Not goroutine-safe. // NOTE: All get/set to validators should copy the value for safety. -// TODO: consider validator Accum overflow type ValidatorSet struct { // NOTE: persisted via reflect, must be exported. Validators []*Validator `json:"validators"` @@ -48,13 +48,13 @@ func NewValidatorSet(vals []*Validator) *ValidatorSet { } // incrementAccum and update the proposer -// TODO: mind the overflow when times and votingPower shares too large. func (valSet *ValidatorSet) IncrementAccum(times int) { // Add VotingPower * times to each validator and order into heap. validatorsHeap := cmn.NewHeap() for _, val := range valSet.Validators { - val.Accum += val.VotingPower * int64(times) // TODO: mind overflow - validatorsHeap.Push(val, accumComparable{val}) + // check for overflow both multiplication and sum + val.Accum = safeAddClip(val.Accum, safeMulClip(val.VotingPower, int64(times))) + validatorsHeap.PushComparable(val, accumComparable{val}) } // Decrement the validator with most accum times times @@ -63,7 +63,9 @@ func (valSet *ValidatorSet) IncrementAccum(times int) { if i == times-1 { valSet.Proposer = mostest } - mostest.Accum -= int64(valSet.TotalVotingPower()) + + // mind underflow + mostest.Accum = safeSubClip(mostest.Accum, valSet.TotalVotingPower()) validatorsHeap.Update(mostest, accumComparable{mostest}) } } @@ -117,7 +119,8 @@ func (valSet *ValidatorSet) Size() int { func (valSet *ValidatorSet) TotalVotingPower() int64 { if valSet.totalVotingPower == 0 { for _, val := range valSet.Validators { - valSet.totalVotingPower += val.VotingPower + // mind overflow + valSet.totalVotingPower = safeAddClip(valSet.totalVotingPower, val.VotingPower) } } return valSet.totalVotingPower @@ -147,11 +150,11 @@ func (valSet *ValidatorSet) Hash() []byte { if len(valSet.Validators) == 0 { return nil } - hashables := make([]merkle.Hashable, len(valSet.Validators)) + hashers := make([]merkle.Hasher, len(valSet.Validators)) for i, val := range valSet.Validators { - hashables[i] = val + hashers[i] = val } - return merkle.SimpleHashFromHashables(hashables) + return merkle.SimpleHashFromHashers(hashers) } func (valSet *ValidatorSet) Add(val *Validator) (added bool) { @@ -425,3 +428,77 @@ func RandValidatorSet(numValidators int, votingPower int64) (*ValidatorSet, []*P sort.Sort(PrivValidatorsByAddress(privValidators)) return valSet, privValidators } + +/////////////////////////////////////////////////////////////////////////////// +// Safe multiplication and addition/subtraction + +func safeMul(a, b int64) (int64, bool) { + if a == 0 || b == 0 { + return 0, false + } + if a == 1 { + return b, false + } + if b == 1 { + return a, false + } + if a == math.MinInt64 || b == math.MinInt64 { + return -1, true + } + c := a * b + return c, c/b != a +} + +func safeAdd(a, b int64) (int64, bool) { + if b > 0 && a > math.MaxInt64-b { + return -1, true + } else if b < 0 && a < math.MinInt64-b { + return -1, true + } + return a + b, false +} + +func safeSub(a, b int64) (int64, bool) { + if b > 0 && a < math.MinInt64+b { + return -1, true + } else if b < 0 && a > math.MaxInt64+b { + return -1, true + } + return a - b, false +} + +func safeMulClip(a, b int64) int64 { + c, overflow := safeMul(a, b) + if overflow { + if (a < 0 || b < 0) && !(a < 0 && b < 0) { + return math.MinInt64 + } else { + return math.MaxInt64 + } + } + return c +} + +func safeAddClip(a, b int64) int64 { + c, overflow := safeAdd(a, b) + if overflow { + if b < 0 { + return math.MinInt64 + } else { + return math.MaxInt64 + } + } + return c +} + +func safeSubClip(a, b int64) int64 { + c, overflow := safeSub(a, b) + if overflow { + if b > 0 { + return math.MinInt64 + } else { + return math.MaxInt64 + } + } + return c +} diff --git a/types/validator_set_test.go b/types/validator_set_test.go index 572b7b007..9c7512378 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -2,11 +2,14 @@ package types import ( "bytes" + "math" "strings" "testing" + "testing/quick" - "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire" + "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" cmn "github.com/tendermint/tmlibs/common" ) @@ -190,6 +193,85 @@ func TestProposerSelection3(t *testing.T) { } } +func TestValidatorSetTotalVotingPowerOverflows(t *testing.T) { + vset := NewValidatorSet([]*Validator{ + {Address: []byte("a"), VotingPower: math.MaxInt64, Accum: 0}, + {Address: []byte("b"), VotingPower: math.MaxInt64, Accum: 0}, + {Address: []byte("c"), VotingPower: math.MaxInt64, Accum: 0}, + }) + + assert.EqualValues(t, math.MaxInt64, vset.TotalVotingPower()) +} + +func TestValidatorSetIncrementAccumOverflows(t *testing.T) { + // NewValidatorSet calls IncrementAccum(1) + vset := NewValidatorSet([]*Validator{ + // too much voting power + 0: {Address: []byte("a"), VotingPower: math.MaxInt64, Accum: 0}, + // too big accum + 1: {Address: []byte("b"), VotingPower: 10, Accum: math.MaxInt64}, + // almost too big accum + 2: {Address: []byte("c"), VotingPower: 10, Accum: math.MaxInt64 - 5}, + }) + + assert.Equal(t, int64(0), vset.Validators[0].Accum, "0") // because we decrement val with most voting power + assert.EqualValues(t, math.MaxInt64, vset.Validators[1].Accum, "1") + assert.EqualValues(t, math.MaxInt64, vset.Validators[2].Accum, "2") +} + +func TestValidatorSetIncrementAccumUnderflows(t *testing.T) { + // NewValidatorSet calls IncrementAccum(1) + vset := NewValidatorSet([]*Validator{ + 0: {Address: []byte("a"), VotingPower: math.MaxInt64, Accum: math.MinInt64}, + 1: {Address: []byte("b"), VotingPower: 1, Accum: math.MinInt64}, + }) + + vset.IncrementAccum(5) + + assert.EqualValues(t, math.MinInt64, vset.Validators[0].Accum, "0") + assert.EqualValues(t, math.MinInt64, vset.Validators[1].Accum, "1") +} + +func TestSafeMul(t *testing.T) { + f := func(a, b int64) bool { + c, overflow := safeMul(a, b) + return overflow || (!overflow && c == a*b) + } + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestSafeAdd(t *testing.T) { + f := func(a, b int64) bool { + c, overflow := safeAdd(a, b) + return overflow || (!overflow && c == a+b) + } + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestSafeMulClip(t *testing.T) { + assert.EqualValues(t, math.MaxInt64, safeMulClip(math.MinInt64, math.MinInt64)) + assert.EqualValues(t, math.MinInt64, safeMulClip(math.MaxInt64, math.MinInt64)) + assert.EqualValues(t, math.MinInt64, safeMulClip(math.MinInt64, math.MaxInt64)) + assert.EqualValues(t, math.MaxInt64, safeMulClip(math.MaxInt64, 2)) +} + +func TestSafeAddClip(t *testing.T) { + assert.EqualValues(t, math.MaxInt64, safeAddClip(math.MaxInt64, 10)) + assert.EqualValues(t, math.MaxInt64, safeAddClip(math.MaxInt64, math.MaxInt64)) + assert.EqualValues(t, math.MinInt64, safeAddClip(math.MinInt64, -10)) +} + +func TestSafeSubClip(t *testing.T) { + assert.EqualValues(t, math.MinInt64, safeSubClip(math.MinInt64, 10)) + assert.EqualValues(t, 0, safeSubClip(math.MinInt64, math.MinInt64)) + assert.EqualValues(t, math.MinInt64, safeSubClip(math.MinInt64, math.MaxInt64)) + assert.EqualValues(t, math.MaxInt64, safeSubClip(math.MaxInt64, -10)) +} + func BenchmarkValidatorSetCopy(b *testing.B) { b.StopTimer() vset := NewValidatorSet([]*Validator{}) diff --git a/types/vote.go b/types/vote.go index 4397152c7..7b069f2f6 100644 --- a/types/vote.go +++ b/types/vote.go @@ -9,7 +9,6 @@ import ( "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" - "github.com/tendermint/go-wire/data" cmn "github.com/tendermint/tmlibs/common" ) @@ -59,9 +58,12 @@ func IsVoteTypeValid(type_ byte) bool { } } +// Address is hex bytes. TODO: crypto.Address +type Address = cmn.HexBytes + // Represents a prevote, precommit, or commit vote from validators for consensus. type Vote struct { - ValidatorAddress data.Bytes `json:"validator_address"` + ValidatorAddress Address `json:"validator_address"` ValidatorIndex int `json:"validator_index"` Height int64 `json:"height"` Round int `json:"round"` diff --git a/types/vote_set.go b/types/vote_set.go index 34f989567..2b5ac6316 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" + "github.com/tendermint/tendermint/p2p" cmn "github.com/tendermint/tmlibs/common" ) @@ -58,7 +59,7 @@ type VoteSet struct { sum int64 // Sum of voting power for seen votes, discounting conflicts maj23 *BlockID // First 2/3 majority seen votesByBlock map[string]*blockVotes // string(blockHash|blockParts) -> blockVotes - peerMaj23s map[string]BlockID // Maj23 for each peer + peerMaj23s map[p2p.ID]BlockID // Maj23 for each peer } // Constructs a new VoteSet struct used to accumulate votes for given height/round. @@ -77,7 +78,7 @@ func NewVoteSet(chainID string, height int64, round int, type_ byte, valSet *Val sum: 0, maj23: nil, votesByBlock: make(map[string]*blockVotes, valSet.Size()), - peerMaj23s: make(map[string]BlockID), + peerMaj23s: make(map[p2p.ID]BlockID), } } @@ -171,7 +172,7 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { // Ensure that the signer has the right address if !bytes.Equal(valAddr, lookupAddr) { return false, errors.Wrapf(ErrVoteInvalidValidatorAddress, - "vote.ValidatorAddress (%X) does not match address (%X) for vote.ValidatorIndex (%d)", + "vote.ValidatorAddress (%X) does not match address (%X) for vote.ValidatorIndex (%d)\nEnsure the genesis file is correct across all validators.", valAddr, lookupAddr, valIndex) } @@ -290,7 +291,7 @@ func (voteSet *VoteSet) addVerifiedVote(vote *Vote, blockKey string, votingPower // this can cause memory issues. // TODO: implement ability to remove peers too // NOTE: VoteSet must not be nil -func (voteSet *VoteSet) SetPeerMaj23(peerID string, blockID BlockID) { +func (voteSet *VoteSet) SetPeerMaj23(peerID p2p.ID, blockID BlockID) error { if voteSet == nil { cmn.PanicSanity("SetPeerMaj23() on nil VoteSet") } @@ -302,9 +303,10 @@ func (voteSet *VoteSet) SetPeerMaj23(peerID string, blockID BlockID) { // Make sure peer hasn't already told us something. if existing, ok := voteSet.peerMaj23s[peerID]; ok { if existing.Equals(blockID) { - return // Nothing to do + return nil // Nothing to do } else { - return // TODO bad peer! + return fmt.Errorf("SetPeerMaj23: Received conflicting blockID from peer %v. Got %v, expected %v", + peerID, blockID, existing) } } voteSet.peerMaj23s[peerID] = blockID @@ -313,7 +315,7 @@ func (voteSet *VoteSet) SetPeerMaj23(peerID string, blockID BlockID) { votesByBlock, ok := voteSet.votesByBlock[blockKey] if ok { if votesByBlock.peerMaj23 { - return // Nothing to do + return nil // Nothing to do } else { votesByBlock.peerMaj23 = true // No need to copy votes, already there. @@ -323,6 +325,7 @@ func (voteSet *VoteSet) SetPeerMaj23(peerID string, blockID BlockID) { voteSet.votesByBlock[blockKey] = votesByBlock // No need to copy votes, no votes to copy over. } + return nil } func (voteSet *VoteSet) BitArray() *cmn.BitArray { diff --git a/version/version.go b/version/version.go index d328b41d8..a45095428 100644 --- a/version/version.go +++ b/version/version.go @@ -1,13 +1,13 @@ package version const Maj = "0" -const Min = "15" +const Min = "16" const Fix = "0" var ( // Version is the current version of Tendermint // Must be a string because scripts like dist.sh read this file. - Version = "0.15.0" + Version = "0.16.0" // GitCommit is the current HEAD set using ldflags. GitCommit string