Release 0.9.0pull/428/head v0.9.0
@ -0,0 +1,15 @@ | |||
# top-most EditorConfig file | |||
root = true | |||
# Unix-style newlines with a newline ending every file | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[Makefile] | |||
indent_style = tab | |||
[*.sh] | |||
indent_style = tab |
@ -0,0 +1,41 @@ | |||
<!-- Thanks for filing an issue! Before hitting the button, please answer these questions.--> | |||
**Is this a BUG REPORT or FEATURE REQUEST?** (choose one): | |||
<!-- | |||
If this is a BUG REPORT, please: | |||
- Fill in as much of the template below as you can. | |||
If this is a FEATURE REQUEST, please: | |||
- Describe *in detail* the feature/behavior/change you'd like to see. | |||
In both cases, be ready for followup questions, and please respond in a timely | |||
manner. We might ask you to provide additional logs and data (tendermint & app) | |||
in a case of bug. | |||
--> | |||
**Tendermint version** (use `tendermint version` or `git rev-parse --verify HEAD` if installed from source): | |||
**ABCI app** (name for built-in, URL for self-written if it's publicly available): | |||
**Merkleeyes version** (use `git rev-parse --verify HEAD`, skip if you don't use it): | |||
**Environment**: | |||
- **OS** (e.g. from /etc/os-release): | |||
- **Install tools**: | |||
- **Others**: | |||
**What happened**: | |||
**What you expected to happen**: | |||
**How to reproduce it** (as minimally and precisely as possible): | |||
**Anything else do we need to know**: |
@ -0,0 +1,220 @@ | |||
# Changelog | |||
## 0.9.0 (March 6, 2017) | |||
BREAKING CHANGES: | |||
- Update ABCI to v0.4.0, where Query is now `Query(RequestQuery) ResponseQuery`, enabling precise proofs at particular heights: | |||
``` | |||
message RequestQuery{ | |||
bytes data = 1; | |||
string path = 2; | |||
uint64 height = 3; | |||
bool prove = 4; | |||
} | |||
message ResponseQuery{ | |||
CodeType code = 1; | |||
int64 index = 2; | |||
bytes key = 3; | |||
bytes value = 4; | |||
bytes proof = 5; | |||
uint64 height = 6; | |||
string log = 7; | |||
} | |||
``` | |||
- `BlockMeta` data type unifies its Hash and PartSetHash under a `BlockID`: | |||
``` | |||
type BlockMeta struct { | |||
BlockID BlockID `json:"block_id"` // the block hash and partsethash | |||
Header *Header `json:"header"` // The block's Header | |||
} | |||
``` | |||
- `tendermint gen_validator` command output is now pure JSON | |||
- `ValidatorSet` data type: | |||
- expose a `Proposer` field. Note this means the `Proposer` is persisted with the `State`. | |||
- change `.Proposer()` to `.GetProposer()` | |||
FEATURES: | |||
- New RPC endpoint `/commit?height=X` returns header and commit for block at height `X` | |||
- Client API for each endpoint, including mocks for testing | |||
IMPROVEMENTS: | |||
- `Node` is now a `BaseService` | |||
- Simplified starting Tendermint in-process from another application | |||
- Better organized Makefile | |||
- Scripts for auto-building binaries across platforms | |||
- Docker image improved, slimmed down (using Alpine), and changed from tendermint/tmbase to tendermint/tendermint | |||
- New repo files: `CONTRIBUTING.md`, Github `ISSUE_TEMPLATE`, `CHANGELOG.md` | |||
- Improvements on CircleCI for managing build/test artifacts | |||
- Handshake replay is doen through the consensus package, possibly using a mockApp | |||
- Graceful shutdown of RPC listeners | |||
- Tests for the PEX reactor and DialSeeds | |||
BUG FIXES: | |||
- Check peer.Send for failure before updating PeerState in consensus | |||
- Fix panic in `/dial_seeds` with invalid addresses | |||
- Fix proposer selection logic in ValidatorSet by taking the address into account in the `accumComparable` | |||
- Fix inconcistencies with `ValidatorSet.Proposer` across restarts by persisting it in the `State` | |||
## 0.8.0 (January 13, 2017) | |||
BREAKING CHANGES: | |||
- New data type `BlockID` to represent blocks: | |||
``` | |||
type BlockID struct { | |||
Hash []byte `json:"hash"` | |||
PartsHeader PartSetHeader `json:"parts"` | |||
} | |||
``` | |||
- `Vote` data type now includes validator address and index: | |||
``` | |||
type Vote struct { | |||
ValidatorAddress []byte `json:"validator_address"` | |||
ValidatorIndex int `json:"validator_index"` | |||
Height int `json:"height"` | |||
Round int `json:"round"` | |||
Type byte `json:"type"` | |||
BlockID BlockID `json:"block_id"` // zero if vote is nil. | |||
Signature crypto.Signature `json:"signature"` | |||
} | |||
``` | |||
- Update TMSP to v0.3.0, where it is now called ABCI and AppendTx is DeliverTx | |||
- Hex strings in the RPC are now "0x" prefixed | |||
FEATURES: | |||
- New message type on the ConsensusReactor, `Maj23Msg`, for peers to alert others they've seen a Maj23, | |||
in order to track and handle conflicting votes intelligently to prevent Byzantine faults from causing halts: | |||
``` | |||
type VoteSetMaj23Message struct { | |||
Height int | |||
Round int | |||
Type byte | |||
BlockID types.BlockID | |||
} | |||
``` | |||
- Configurable block part set size | |||
- Validator set changes | |||
- Optionally skip TimeoutCommit if we have all the votes | |||
- Handshake between Tendermint and App on startup to sync latest state and ensure consistent recovery from crashes | |||
- GRPC server for BroadcastTx endpoint | |||
IMPROVEMENTS: | |||
- Less verbose logging | |||
- Better test coverage (37% -> 49%) | |||
- Canonical SignBytes for signable types | |||
- Write-Ahead Log for Mempool and Consensus via go-autofile | |||
- Better in-process testing for the consensus reactor and byzantine faults | |||
- Better crash/restart testing for individual nodes at preset failure points, and of networks at arbitrary points | |||
- Better abstraction over timeout mechanics | |||
BUG FIXES: | |||
- Fix memory leak in mempool peer | |||
- Fix panic on POLRound=-1 | |||
- Actually set the CommitTime | |||
- Actually send BeginBlock message | |||
- Fix a liveness issues caused by Byzantine proposals/votes. Uses the new `Maj23Msg`. | |||
## 0.7.4 (December 14, 2016) | |||
FEATURES: | |||
- Enable the Peer Exchange reactor with the `--pex` flag for more resilient gossip network (feature still in development, beware dragons) | |||
IMPROVEMENTS: | |||
- Remove restrictions on RPC endpoint `/dial_seeds` to enable manual network configuration | |||
## 0.7.3 (October 20, 2016) | |||
IMPROVEMENTS: | |||
- Type safe FireEvent | |||
- More WAL/replay tests | |||
- Cleanup some docs | |||
BUG FIXES: | |||
- Fix deadlock in mempool for synchronous apps | |||
- Replay handles non-empty blocks | |||
- Fix race condition in HeightVoteSet | |||
## 0.7.2 (September 11, 2016) | |||
BUG FIXES: | |||
- Set mustConnect=false so tendermint will retry connecting to the app | |||
## 0.7.1 (September 10, 2016) | |||
FEATURES: | |||
- New TMSP connection for Query/Info | |||
- New RPC endpoints: | |||
- `tmsp_query` | |||
- `tmsp_info` | |||
- Allow application to filter peers through Query (off by default) | |||
IMPROVEMENTS: | |||
- TMSP connection type enforced at compile time | |||
- All listen/client urls use a "tcp://" or "unix://" prefix | |||
BUG FIXES: | |||
- Save LastSignature/LastSignBytes to `priv_validator.json` for recovery | |||
- Fix event unsubscribe | |||
- Fix fastsync/blockchain reactor | |||
## 0.7.0 (August 7, 2016) | |||
BREAKING CHANGES: | |||
- Strict SemVer starting now! | |||
- Update to ABCI v0.2.0 | |||
- Validation types now called Commit | |||
- NewBlock event only returns the block header | |||
FEATURES: | |||
- TMSP and RPC support TCP and UNIX sockets | |||
- Addition config options including block size and consensus parameters | |||
- New WAL mode `cswal_light`; logs only the validator's own votes | |||
- New RPC endpoints: | |||
- for starting/stopping profilers, and for updating config | |||
- `/broadcast_tx_commit`, returns when tx is included in a block, else an error | |||
- `/unsafe_flush_mempool`, empties the mempool | |||
IMPROVEMENTS: | |||
- Various optimizations | |||
- Remove bad or invalidated transactions from the mempool cache (allows later duplicates) | |||
- More elaborate testing using CircleCI including benchmarking throughput on 4 digitalocean droplets | |||
BUG FIXES: | |||
- Various fixes to WAL and replay logic | |||
- Various race conditions |
@ -0,0 +1,16 @@ | |||
# Contributing guidelines | |||
**Thanks for considering making contributions to Tendermint!** | |||
Please follow standard github best practices: fork the repo, **branch from the | |||
tip of develop**, make some commits, test your code changes with `make test`, | |||
and submit a pull request to develop. | |||
See the [open issues](https://github.com/tendermint/tendermint/issues) for | |||
things we need help with! | |||
Please make sure to use `gofmt` before every commit - the easiest way to do | |||
this is have your editor run it for you upon saving a file. | |||
You can read the full guide [on our | |||
site](https://tendermint.com/docs/guides/contributing). |
@ -1,54 +1,45 @@ | |||
# Pull base image. | |||
FROM golang:1.6 | |||
FROM alpine:3.5 | |||
ENV USER tmuser | |||
ENV DATA_ROOT /data/tendermint | |||
# This is the release of tendermint to pull in. | |||
ENV TM_VERSION 0.8.0 | |||
# Set user right away for determinism | |||
RUN groupadd -r $USER \ | |||
&& useradd -r -s /bin/false -g $USER $USER | |||
# 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 | |||
# validator file into /tendermint. | |||
# | |||
# The /tendermint/data dir is used by tendermint to store state. | |||
ENV DATA_ROOT /tendermint | |||
ENV TMROOT $DATA_ROOT | |||
# Create home directory for USER | |||
# Needed for nodejs/nom | |||
RUN mkdir -p /home/$USER \ | |||
&& chown -R $USER:$USER /home/$USER | |||
# Set user right away for determinism | |||
RUN addgroup tmuser && \ | |||
adduser -S -G tmuser tmuser | |||
# Create directory for persistence and give our user ownership | |||
RUN mkdir -p $DATA_ROOT \ | |||
&& chown -R $USER:$USER $DATA_ROOT | |||
# Set the env variables to non-interactive | |||
ENV DEBIAN_FRONTEND noninteractive | |||
ENV DEBIAN_PRIORITY critical | |||
ENV DEBCONF_NOWARNINGS yes | |||
ENV TERM linux | |||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections | |||
# Grab deps (git) | |||
RUN apt-get update && \ | |||
apt-get install -y --no-install-recommends \ | |||
git && \ | |||
rm -rf /var/lib/apt/lists/* | |||
# Grab deps (node) | |||
RUN curl -sL https://deb.nodesource.com/setup_5.x | bash - | |||
RUN apt-get update && \ | |||
apt-get install -y --no-install-recommends \ | |||
nodejs && \ | |||
rm -rf /var/lib/apt/lists/* | |||
# Copy run.sh | |||
COPY ./run.sh $DATA_ROOT/run.sh | |||
RUN chmod +x $DATA_ROOT/run.sh | |||
# Persist data, set user | |||
WORKDIR $DATA_ROOT | |||
RUN mkdir -p $DATA_ROOT && \ | |||
chown -R tmuser:tmuser $DATA_ROOT | |||
# jq and curl used for extracting `pub_key` from private validator while | |||
# deploying tendermint with Kubernetes. It is nice to have bash so the users | |||
# could execute bash commands. | |||
RUN apk add --no-cache bash curl jq | |||
RUN apk add --no-cache openssl && \ | |||
wget https://s3-us-west-2.amazonaws.com/tendermint/${TM_VERSION}/tendermint_linux_amd64.zip && \ | |||
echo "83f6bd52055ebc93434a68263c6666a4de41e0e543d0b5a06ad461262c460f4c tendermint_linux_amd64.zip" | sha256sum -c && \ | |||
unzip -d /bin tendermint_linux_amd64.zip && \ | |||
apk del openssl && \ | |||
rm -f tendermint_linux_amd64.zip | |||
# Expose the data directory as a volume since there's mutable state in there | |||
VOLUME $DATA_ROOT | |||
USER $USER | |||
ENV TMROOT $DATA_ROOT | |||
# p2p port | |||
EXPOSE 46656 | |||
# rpc port | |||
EXPOSE 46657 | |||
# Run tendermint | |||
CMD ["./run.sh"] | |||
ENTRYPOINT ["tendermint"] | |||
# By default you'll get the dummy app | |||
CMD ["node", "--moniker=`hostname`", "--proxy_app=dummy"] |
@ -0,0 +1,35 @@ | |||
FROM alpine:3.5 | |||
ENV DATA_ROOT /tendermint | |||
ENV TMROOT $DATA_ROOT | |||
RUN addgroup tmuser && \ | |||
adduser -S -G tmuser tmuser | |||
RUN mkdir -p $DATA_ROOT && \ | |||
chown -R tmuser:tmuser $DATA_ROOT | |||
RUN apk add --no-cache bash curl jq | |||
ENV GOPATH /go | |||
ENV PATH "$PATH:/go/bin" | |||
RUN mkdir -p /go/src/github.com/tendermint/tendermint && \ | |||
apk add --no-cache go build-base git && \ | |||
cd /go/src/github.com/tendermint/tendermint && \ | |||
git clone https://github.com/tendermint/tendermint . && \ | |||
git checkout develop && \ | |||
make get_vendor_deps && \ | |||
make install && \ | |||
glide cc && \ | |||
cd - && \ | |||
rm -rf /go/src/github.com/tendermint/tendermint && \ | |||
apk del go build-base git | |||
VOLUME $DATA_ROOT | |||
EXPOSE 46656 | |||
EXPOSE 46657 | |||
ENTRYPOINT ["tendermint"] | |||
CMD ["node", "--moniker=`hostname`", "--proxy_app=dummy"] |
@ -0,0 +1,15 @@ | |||
build: | |||
# TAG=0.8.0 TAG_NO_PATCH=0.8 | |||
docker build -t "tendermint/tendermint" -t "tendermint/tendermint:$TAG" -t "tendermint/tendermint:$TAG_NO_PATCH" . | |||
push: | |||
# TAG=0.8.0 TAG_NO_PATCH=0.8 | |||
docker push "tendermint/tendermint" "tendermint/tendermint:$TAG" "tendermint/tendermint:$TAG_NO_PATCH" | |||
build_develop: | |||
docker build -t "tendermint/tendermint:develop" -f Dockerfile.develop . | |||
push_develop: | |||
docker push "tendermint/tendermint:develop" | |||
.PHONY: build build_develop push push_develop |
@ -1,24 +1,55 @@ | |||
# Docker | |||
# Supported tags and respective `Dockerfile` links | |||
Tendermint uses docker for deployment of testnets via the [mintnet](github.com/tendermint/mintnet) tool. | |||
- `0.8.0`, `0.8`, `latest` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/bf64dd21fdb193e54d8addaaaa2ecf7ac371de8c/DOCKER/Dockerfile) | |||
- `develop` [(Dockerfile)]() | |||
For faster development iterations (ie. to avoid docker builds), | |||
the dockerfile just sets up the OS, and tendermint is fetched/installed at runtime. | |||
`develop` tag points to the [develop](https://github.com/tendermint/tendermint/tree/develop) branch. | |||
For the deterministic docker builds used in testing, see the [tests directory](https://github.com/tendermint/tendermint/tree/master/test) | |||
# Tendermint | |||
# Build and run a docker image and container | |||
Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine, written in any programming language, and securely replicates it on many machines. | |||
These are notes for the dev team. | |||
For more background, see the [introduction](https://tendermint.com/intro). | |||
``` | |||
# Build base Docker image | |||
# Make sure ./run.sh exists. | |||
docker build -t tendermint/tmbase -f Dockerfile . | |||
To get started developing applications, see the [application developers guide](https://tendermint.com/docs/guides/app-development). | |||
# How to use this image | |||
## Start one instance of the Tendermint core with the `dummy` app | |||
# Log into dockerhub | |||
docker login | |||
A very simple example of a built-in app and Tendermint core in one container. | |||
# Push latest build to dockerhub | |||
docker push tendermint/tmbase | |||
``` | |||
docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint init | |||
docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint | |||
``` | |||
## mintnet-kubernetes | |||
If you want to see many containers talking to each other, consider using [mintnet-kubernetes](https://github.com/tendermint/mintnet-kubernetes), which is a tool for running Tendermint-based applications on a Kubernetes cluster. | |||
# Supported Docker versions | |||
This image is officially supported on Docker version 1.13.1. | |||
Support for older versions (down to 1.6) is provided on a best-effort basis. | |||
Please see [the Docker installation documentation](https://docs.docker.com/installation/) for details on how to upgrade your Docker daemon. | |||
# License | |||
View [license information](https://raw.githubusercontent.com/tendermint/tendermint/master/LICENSE) for the software contained in this image. | |||
# User Feedback | |||
## Issues | |||
If you have any problems with or questions about this image, please contact us through a [GitHub](https://github.com/tendermint/tendermint/issues) issue. If the issue is related to a CVE, please check for [a `cve-tracker` issue on the `official-images` repository](https://github.com/docker-library/official-images/issues?q=label%3Acve-tracker) first. | |||
You can also reach the image maintainers via [Slack](http://forum.tendermint.com:3000/). | |||
## Contributing | |||
You are invited to contribute new features, fixes, or updates, large or small; we are always thrilled to receive pull requests, and do our best to process them as fast as we can. | |||
Before you start to code, we recommend discussing your plans through a [GitHub](https://github.com/tendermint/tendermint/issues) issue, especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. |
@ -1,10 +0,0 @@ | |||
#! /bin/bash | |||
mkdir -p $GOPATH/src/$TMREPO | |||
cd $GOPATH/src/$TMREPO | |||
git clone https://$TMREPO.git . | |||
git fetch | |||
git reset --hard $TMHEAD | |||
go get -d $TMREPO/cmd/tendermint | |||
make | |||
tendermint node --seeds="$TMSEEDS" --moniker="$TMNAME" |
@ -1,59 +1,74 @@ | |||
.PHONY: get_deps build all list_deps install | |||
all: get_deps install test | |||
GOTOOLS = \ | |||
github.com/mitchellh/gox \ | |||
github.com/Masterminds/glide | |||
PACKAGES=$(shell go list ./... | grep -v '/vendor/') | |||
BUILD_TAGS?=tendermint | |||
TMROOT = $${TMROOT:-$$HOME/.tendermint} | |||
define NEWLINE | |||
all: install test | |||
endef | |||
NOVENDOR = go list github.com/tendermint/tendermint/... | grep -v /vendor/ | |||
install: get_deps | |||
go install github.com/tendermint/tendermint/cmd/tendermint | |||
install: get_vendor_deps | |||
@go install ./cmd/tendermint | |||
build: | |||
go build -o build/tendermint github.com/tendermint/tendermint/cmd/tendermint | |||
go build -o build/tendermint ./cmd/tendermint | |||
build_race: | |||
go build -race -o build/tendermint github.com/tendermint/tendermint/cmd/tendermint | |||
go build -race -o build/tendermint ./cmd/tendermint | |||
# dist builds binaries for all platforms and packages them for distribution | |||
dist: | |||
@BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/dist.sh'" | |||
test: build | |||
go test `${NOVENDOR}` | |||
test: | |||
@echo "--> Running go test" | |||
@go test $(PACKAGES) | |||
test_race: build | |||
go test -race `${NOVENDOR}` | |||
test_race: | |||
@echo "--> Running go test --race" | |||
@go test -v -race $(PACKAGES) | |||
test_integrations: | |||
bash ./test/test.sh | |||
@bash ./test/test.sh | |||
test100: build | |||
for i in {1..100}; do make test; done | |||
test100: | |||
@for i in {1..100}; do make test; done | |||
draw_deps: | |||
# requires brew install graphviz | |||
go get github.com/hirokidaichi/goviz | |||
goviz -i github.com/tendermint/tendermint/cmd/tendermint | dot -Tpng -o huge.png | |||
goviz -i ./cmd/tendermint | dot -Tpng -o huge.png | |||
list_deps: | |||
go list -f '{{join .Deps "\n"}}' github.com/tendermint/tendermint/... | \ | |||
@go list -f '{{join .Deps "\n"}}' ./... | \ | |||
grep -v /vendor/ | sort | uniq | \ | |||
xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | |||
xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | |||
get_deps: | |||
go get -d `${NOVENDOR}` | |||
go list -f '{{join .TestImports "\n"}}' github.com/tendermint/tendermint/... | \ | |||
@echo "--> Running go get" | |||
@go get -v -d $(PACKAGES) | |||
@go list -f '{{join .TestImports "\n"}}' ./... | \ | |||
grep -v /vendor/ | sort | uniq | \ | |||
xargs go get | |||
xargs go get -v -d | |||
get_vendor_deps: | |||
go get github.com/Masterminds/glide | |||
rm -rf vendor/ | |||
glide install | |||
get_vendor_deps: ensure_tools | |||
@rm -rf vendor/ | |||
@echo "--> Running glide install" | |||
@glide install | |||
update_deps: | |||
go get -d -u github.com/tendermint/tendermint/... | |||
update_deps: tools | |||
@echo "--> Updating dependencies" | |||
@go get -d -u ./... | |||
revision: | |||
-echo `git rev-parse --verify HEAD` > $(TMROOT)/revision | |||
-echo `git rev-parse --verify HEAD` >> $(TMROOT)/revision_history | |||
tools: | |||
go get -u -v $(GOTOOLS) | |||
ensure_tools: | |||
go get $(GOTOOLS) | |||
.PHONY: install build build_race dist test test_race test_integrations test100 draw_deps list_deps get_deps get_vendor_deps update_deps revision tools |
@ -1,38 +1,33 @@ | |||
--- | |||
machine: | |||
environment: | |||
MACH_PREFIX: tendermint-test-mach | |||
GOPATH: /home/ubuntu/.go_workspace | |||
REPO: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME | |||
DOCKER_VERSION: 1.10.0 | |||
DOCKER_MACHINE_VERSION: 0.6.0 | |||
DOCKER_MACHINE_VERSION: 0.9.0 | |||
GOPATH: "$HOME/.go_project" | |||
PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" | |||
PROJECT_PATH: "$PROJECT_PARENT_PATH/$CIRCLE_PROJECT_REPONAME" | |||
hosts: | |||
circlehost: 127.0.0.1 | |||
localhost: 127.0.0.1 | |||
checkout: | |||
post: | |||
- rm -rf $REPO | |||
- mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME | |||
- mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO | |||
# - git submodule sync | |||
# - git submodule update --init # use submodules | |||
dependencies: | |||
override: | |||
- echo $MACH_PREFIX $GOPATH $REPO $DOCKER_VERSION $DOCKER_MACHINE_VERSION | |||
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | sudo bash -s -- $DOCKER_VERSION | |||
- sudo curl -sSL -o /usr/bin/docker-machine https://github.com/docker/machine/releases/download/v$DOCKER_MACHINE_VERSION/docker-machine-linux-x86_64; sudo chmod 0755 /usr/bin/docker-machine | |||
- sudo start docker | |||
- sudo curl -sSL -o /usr/bin/docker-machine "https://github.com/docker/machine/releases/download/v$DOCKER_MACHINE_VERSION/docker-machine-`uname -s`-`uname -m`"; sudo chmod 0755 /usr/bin/docker-machine | |||
- mkdir -p "$PROJECT_PARENT_PATH" | |||
- ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$PROJECT_PATH" | |||
post: | |||
- go version | |||
- docker version | |||
- docker-machine version | |||
test: | |||
override: | |||
- "cd $REPO && set -o pipefail && make test_integrations | tee ~/test_integrations.log": | |||
- cd "$PROJECT_PATH" && set -o pipefail && make test_integrations 2>&1 | tee test_integrations.log: | |||
timeout: 1800 | |||
- "cp ~/test_integrations.log $CIRCLE_ARTIFACTS" | |||
post: | |||
- "cd $REPO && bash <(curl -s https://codecov.io/bash)" | |||
- 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" |
@ -1,24 +1,35 @@ | |||
package main | |||
import ( | |||
. "github.com/tendermint/go-common" | |||
"os" | |||
cmn "github.com/tendermint/go-common" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func init_files() { | |||
privValidator := types.GenPrivValidator() | |||
privValidator.SetFile(config.GetString("priv_validator_file")) | |||
privValidator.Save() | |||
privValFile := config.GetString("priv_validator_file") | |||
if _, err := os.Stat(privValFile); os.IsNotExist(err) { | |||
privValidator := types.GenPrivValidator() | |||
privValidator.SetFile(privValFile) | |||
privValidator.Save() | |||
genDoc := types.GenesisDoc{ | |||
ChainID: Fmt("test-chain-%v", RandStr(6)), | |||
} | |||
genDoc.Validators = []types.GenesisValidator{types.GenesisValidator{ | |||
PubKey: privValidator.PubKey, | |||
Amount: 10, | |||
}} | |||
genFile := config.GetString("genesis_file") | |||
genDoc.SaveAs(config.GetString("genesis_file")) | |||
if _, err := os.Stat(genFile); os.IsNotExist(err) { | |||
genDoc := types.GenesisDoc{ | |||
ChainID: cmn.Fmt("test-chain-%v", cmn.RandStr(6)), | |||
} | |||
genDoc.Validators = []types.GenesisValidator{types.GenesisValidator{ | |||
PubKey: privValidator.PubKey, | |||
Amount: 10, | |||
}} | |||
log.Notice("Initialized tendermint", "genesis", config.GetString("genesis_file"), "priv_validator", config.GetString("priv_validator_file")) | |||
genDoc.SaveAs(genFile) | |||
} | |||
log.Notice("Initialized tendermint", "genesis", config.GetString("genesis_file"), "priv_validator", config.GetString("priv_validator_file")) | |||
} else { | |||
log.Notice("Already initialized", "priv_validator", config.GetString("priv_validator_file")) | |||
} | |||
} |
@ -0,0 +1,59 @@ | |||
package main | |||
import ( | |||
"io/ioutil" | |||
"time" | |||
. "github.com/tendermint/go-common" | |||
cfg "github.com/tendermint/go-config" | |||
"github.com/tendermint/tendermint/node" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Users wishing to: | |||
// * Use an external signer for their validators | |||
// * Supply an in-proc abci app | |||
// should import github.com/tendermint/tendermint/node and implement | |||
// their own run_node to call node.NewNode (instead of node.NewNodeDefault) | |||
// with their custom priv validator and/or custom proxy.ClientCreator | |||
func run_node(config cfg.Config) { | |||
// Wait until the genesis doc becomes available | |||
// This is for Mintnet compatibility. | |||
// TODO: If Mintnet gets deprecated or genesis_file is | |||
// always available, remove. | |||
genDocFile := config.GetString("genesis_file") | |||
if !FileExists(genDocFile) { | |||
log.Notice(Fmt("Waiting for genesis file %v...", genDocFile)) | |||
for { | |||
time.Sleep(time.Second) | |||
if !FileExists(genDocFile) { | |||
continue | |||
} | |||
jsonBlob, err := ioutil.ReadFile(genDocFile) | |||
if err != nil { | |||
Exit(Fmt("Couldn't read GenesisDoc file: %v", err)) | |||
} | |||
genDoc, err := types.GenesisDocFromJSON(jsonBlob) | |||
if err != nil { | |||
Exit(Fmt("Error reading GenesisDoc: %v", err)) | |||
} | |||
if genDoc.ChainID == "" { | |||
Exit(Fmt("Genesis doc %v must include non-empty chain_id", genDocFile)) | |||
} | |||
config.Set("chain_id", genDoc.ChainID) | |||
} | |||
} | |||
// Create & start node | |||
n := node.NewNodeDefault(config) | |||
if _, err := n.Start(); err != nil { | |||
Exit(Fmt("Failed to start node: %v", err)) | |||
} else { | |||
log.Notice("Started node", "nodeInfo", n.Switch().NodeInfo()) | |||
} | |||
// Trap signal, run forever. | |||
n.RunForever() | |||
} |
@ -0,0 +1,271 @@ | |||
package consensus | |||
import ( | |||
"bufio" | |||
"errors" | |||
"fmt" | |||
"os" | |||
"strconv" | |||
"strings" | |||
. "github.com/tendermint/go-common" | |||
cfg "github.com/tendermint/go-config" | |||
dbm "github.com/tendermint/go-db" | |||
bc "github.com/tendermint/tendermint/blockchain" | |||
mempl "github.com/tendermint/tendermint/mempool" | |||
"github.com/tendermint/tendermint/proxy" | |||
sm "github.com/tendermint/tendermint/state" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
//-------------------------------------------------------- | |||
// replay messages interactively or all at once | |||
func RunReplayFile(config cfg.Config, walFile string, console bool) { | |||
consensusState := newConsensusStateForReplay(config) | |||
if err := consensusState.ReplayFile(walFile, console); err != nil { | |||
Exit(Fmt("Error during consensus replay: %v", err)) | |||
} | |||
} | |||
// Replay msgs in file or start the console | |||
func (cs *ConsensusState) ReplayFile(file string, console bool) error { | |||
if cs.IsRunning() { | |||
return errors.New("cs is already running, cannot replay") | |||
} | |||
if cs.wal != nil { | |||
return errors.New("cs wal is open, cannot replay") | |||
} | |||
cs.startForReplay() | |||
// ensure all new step events are regenerated as expected | |||
newStepCh := subscribeToEvent(cs.evsw, "replay-test", types.EventStringNewRoundStep(), 1) | |||
// just open the file for reading, no need to use wal | |||
fp, err := os.OpenFile(file, os.O_RDONLY, 0666) | |||
if err != nil { | |||
return err | |||
} | |||
pb := newPlayback(file, fp, cs, cs.state.Copy()) | |||
defer pb.fp.Close() | |||
var nextN int // apply N msgs in a row | |||
for pb.scanner.Scan() { | |||
if nextN == 0 && console { | |||
nextN = pb.replayConsoleLoop() | |||
} | |||
if err := pb.cs.readReplayMessage(pb.scanner.Bytes(), newStepCh); err != nil { | |||
return err | |||
} | |||
if nextN > 0 { | |||
nextN -= 1 | |||
} | |||
pb.count += 1 | |||
} | |||
return nil | |||
} | |||
//------------------------------------------------ | |||
// playback manager | |||
type playback struct { | |||
cs *ConsensusState | |||
fp *os.File | |||
scanner *bufio.Scanner | |||
count int // how many lines/msgs into the file are we | |||
// replays can be reset to beginning | |||
fileName string // so we can close/reopen the file | |||
genesisState *sm.State // so the replay session knows where to restart from | |||
} | |||
func newPlayback(fileName string, fp *os.File, cs *ConsensusState, genState *sm.State) *playback { | |||
return &playback{ | |||
cs: cs, | |||
fp: fp, | |||
fileName: fileName, | |||
genesisState: genState, | |||
scanner: bufio.NewScanner(fp), | |||
} | |||
} | |||
// go back count steps by resetting the state and running (pb.count - count) steps | |||
func (pb *playback) replayReset(count int, newStepCh chan interface{}) error { | |||
pb.cs.Stop() | |||
pb.cs.Wait() | |||
newCS := NewConsensusState(pb.cs.config, pb.genesisState.Copy(), pb.cs.proxyAppConn, pb.cs.blockStore, pb.cs.mempool) | |||
newCS.SetEventSwitch(pb.cs.evsw) | |||
newCS.startForReplay() | |||
pb.fp.Close() | |||
fp, err := os.OpenFile(pb.fileName, os.O_RDONLY, 0666) | |||
if err != nil { | |||
return err | |||
} | |||
pb.fp = fp | |||
pb.scanner = bufio.NewScanner(fp) | |||
count = pb.count - count | |||
log.Notice(Fmt("Reseting from %d to %d", pb.count, count)) | |||
pb.count = 0 | |||
pb.cs = newCS | |||
for i := 0; pb.scanner.Scan() && i < count; i++ { | |||
if err := pb.cs.readReplayMessage(pb.scanner.Bytes(), newStepCh); err != nil { | |||
return err | |||
} | |||
pb.count += 1 | |||
} | |||
return nil | |||
} | |||
func (cs *ConsensusState) startForReplay() { | |||
// don't want to start full cs | |||
cs.BaseService.OnStart() | |||
log.Warn("Replay commands are disabled until someone updates them and writes tests") | |||
/* TODO:! | |||
// since we replay tocks we just ignore ticks | |||
go func() { | |||
for { | |||
select { | |||
case <-cs.tickChan: | |||
case <-cs.Quit: | |||
return | |||
} | |||
} | |||
}()*/ | |||
} | |||
// console function for parsing input and running commands | |||
func (pb *playback) replayConsoleLoop() int { | |||
for { | |||
fmt.Printf("> ") | |||
bufReader := bufio.NewReader(os.Stdin) | |||
line, more, err := bufReader.ReadLine() | |||
if more { | |||
Exit("input is too long") | |||
} else if err != nil { | |||
Exit(err.Error()) | |||
} | |||
tokens := strings.Split(string(line), " ") | |||
if len(tokens) == 0 { | |||
continue | |||
} | |||
switch tokens[0] { | |||
case "next": | |||
// "next" -> replay next message | |||
// "next N" -> replay next N messages | |||
if len(tokens) == 1 { | |||
return 0 | |||
} else { | |||
i, err := strconv.Atoi(tokens[1]) | |||
if err != nil { | |||
fmt.Println("next takes an integer argument") | |||
} else { | |||
return i | |||
} | |||
} | |||
case "back": | |||
// "back" -> go back one message | |||
// "back N" -> go back N messages | |||
// NOTE: "back" is not supported in the state machine design, | |||
// so we restart and replay up to | |||
// ensure all new step events are regenerated as expected | |||
newStepCh := subscribeToEvent(pb.cs.evsw, "replay-test", types.EventStringNewRoundStep(), 1) | |||
if len(tokens) == 1 { | |||
pb.replayReset(1, newStepCh) | |||
} else { | |||
i, err := strconv.Atoi(tokens[1]) | |||
if err != nil { | |||
fmt.Println("back takes an integer argument") | |||
} else if i > pb.count { | |||
fmt.Printf("argument to back must not be larger than the current count (%d)\n", pb.count) | |||
} else { | |||
pb.replayReset(i, newStepCh) | |||
} | |||
} | |||
case "rs": | |||
// "rs" -> print entire round state | |||
// "rs short" -> print height/round/step | |||
// "rs <field>" -> print another field of the round state | |||
rs := pb.cs.RoundState | |||
if len(tokens) == 1 { | |||
fmt.Println(rs) | |||
} else { | |||
switch tokens[1] { | |||
case "short": | |||
fmt.Printf("%v/%v/%v\n", rs.Height, rs.Round, rs.Step) | |||
case "validators": | |||
fmt.Println(rs.Validators) | |||
case "proposal": | |||
fmt.Println(rs.Proposal) | |||
case "proposal_block": | |||
fmt.Printf("%v %v\n", rs.ProposalBlockParts.StringShort(), rs.ProposalBlock.StringShort()) | |||
case "locked_round": | |||
fmt.Println(rs.LockedRound) | |||
case "locked_block": | |||
fmt.Printf("%v %v\n", rs.LockedBlockParts.StringShort(), rs.LockedBlock.StringShort()) | |||
case "votes": | |||
fmt.Println(rs.Votes.StringIndented(" ")) | |||
default: | |||
fmt.Println("Unknown option", tokens[1]) | |||
} | |||
} | |||
case "n": | |||
fmt.Println(pb.count) | |||
} | |||
} | |||
return 0 | |||
} | |||
//-------------------------------------------------------------------------------- | |||
// convenience for replay mode | |||
func newConsensusStateForReplay(config cfg.Config) *ConsensusState { | |||
// Get BlockStore | |||
blockStoreDB := dbm.NewDB("blockstore", config.GetString("db_backend"), config.GetString("db_dir")) | |||
blockStore := bc.NewBlockStore(blockStoreDB) | |||
// Get State | |||
stateDB := dbm.NewDB("state", config.GetString("db_backend"), config.GetString("db_dir")) | |||
state := sm.MakeGenesisStateFromFile(stateDB, config.GetString("genesis_file")) | |||
// Create proxyAppConn connection (consensus, mempool, query) | |||
proxyApp := proxy.NewAppConns(config, proxy.DefaultClientCreator(config), NewHandshaker(config, state, blockStore)) | |||
_, err := proxyApp.Start() | |||
if err != nil { | |||
Exit(Fmt("Error starting proxy app conns: %v", err)) | |||
} | |||
// add the chainid to the global config | |||
config.Set("chain_id", state.ChainID) | |||
// Make event switch | |||
eventSwitch := types.NewEventSwitch() | |||
if _, err := eventSwitch.Start(); err != nil { | |||
Exit(Fmt("Failed to start event switch: %v", err)) | |||
} | |||
mempool := mempl.NewMempool(config, proxyApp.Mempool()) | |||
consensusState := NewConsensusState(config, state.Copy(), proxyApp.Consensus(), blockStore, mempool) | |||
consensusState.SetEventSwitch(eventSwitch) | |||
return consensusState | |||
} |
@ -0,0 +1,16 @@ | |||
# ABCI | |||
ABCI is an interface between the consensus/blockchain engine known as tendermint, and the application-specific business logic, known as an ABCi app. | |||
The tendermint core should run unchanged for all apps. Each app can customize it, the supported transactions, queries, even the validator sets and how to handle staking / slashing stake. This customization is achieved by implementing the ABCi app to send the proper information to the tendermint engine to perform as directed. | |||
To understand this decision better, think of the design of the tendermint engine. | |||
* A blockchain is simply consensus on a unique global ordering of events. | |||
* This consensus can efficiently be implemented using BFT and PoS | |||
* This code can be generalized to easily support a large number of blockchains | |||
* The block-chain specific code, the interpretation of the individual events, can be implemented by a 3rd party app without touching the consensus engine core | |||
* Use an efficient, language-agnostic layer to implement this (ABCi) | |||
Bucky, please make this doc real. |
@ -0,0 +1,16 @@ | |||
# Architecture Decision Records | |||
This is a location to record all high-level architecture decisions in the tendermin project. Not the implementation details, but the reasoning that happened. This should be refered to for guidance of the "right way" to extend the application. And if we notice that the original decisions were lacking, we should have another open discussion, record the new decisions here, and then modify the code to match. | |||
This is like our guide and mentor when Jae and Bucky are offline.... The concept comes from a [blog post](https://product.reverb.com/documenting-architecture-decisions-the-reverb-way-a3563bb24bd0#.78xhdix6t) that resonated among the team when Anton shared it. | |||
Each section of the code can have it's own markdown file in this directory, and please add a link to the readme. | |||
## Sections | |||
* [ABCI](./ABCI.md) | |||
* [go-merkle / merkleeyes](./merkle.md) | |||
* [Frey's thoughts on the data store](./merkle-frey.md) | |||
* basecoin | |||
* tendermint core (multiple sections) | |||
* ??? |
@ -0,0 +1,240 @@ | |||
# Merkle data stores - Frey's proposal | |||
## TL;DR | |||
To allow the efficient creation of an ABCi app, tendermint wishes to provide a reference implementation of a key-value store that provides merkle proofs of the data. These proofs then quickly allow the ABCi app to provide an app hash to the consensus engine, as well as a full proof to any client. | |||
This is equivalent to building a database, and I would propose designing it from the API first, then looking how to implement this (or make an adapter from the API to existing implementations). Once we agree on the functionality and the interface, we can implement the API bindings, and then work on building adapters to existence merkle-ized data stores, or modifying the stores to support this interface. | |||
We need to consider the API (both in-process and over the network), language bindings, maintaining handles to old state (and garbage collecting), persistence, security, providing merkle proofs, and general key-value store operations. To stay consistent with the blockchains "single global order of operations", this data store should only allow one connection at a time to have write access. | |||
## Overview | |||
* **State** | |||
* There are two concepts of state, "committed state" and "working state" | |||
* The working state is only accessible from the ABCi app, allows writing, but does not need to support proofs. | |||
* When we commit the "working state", it becomes a new "committed state" and has an immutable root hash, provides proofs, and can be exposed to external clients. | |||
* **Transactions** | |||
* The database always allows creating a read-only transaction at the last "committed state", this transaction can serve read queries and proofs. | |||
* The database maintains all data to serve these read transactions until they are closed by the client (or time out). This allows the client(s) to determine how much old info is needed | |||
* The database can only support *maximal* one writable transaction at a time. This makes it easy to enforce serializability, and attempting to start a second writable transaction may trigger a panic. | |||
* **Functionality** | |||
* It must support efficient key-value operations (get/set/delete) | |||
* It must support returning merkle proofs for any "committed state" | |||
* It should support range queries on subsets of the key space if possible (ie. if the db doesn't hash keys) | |||
* It should also support listening to changes to a desired key via pub-sub or similar method, so I can quickly notify you on a change to your balance without constant polling. | |||
* It may support other db-specific query types as an extension to this interface, as long as all specified actions maintain their meaning. | |||
* **Interface** | |||
* This interface should be domain-specific - ie. designed just for this use case | |||
* It should present a simple go interface for embedding the data store in-process | |||
* It should create a gRPC/protobuf API for calling from any client | |||
* It should provide and maintain client adapters from our in-process interface to gRPC client calls for at least golang and Java (maybe more languages?) | |||
* It should provide and maintain server adapters from our gRPC calls to the in-process interface for golang at least (unless there is another server we wish to support) | |||
* **Persistence** | |||
* It must support atomic persistence upon committing a new block. That is, upon crash recovery, the state is guaranteed to represent the state at the end of a complete block (along with a note of which height it was). | |||
* It must delay deletion of old data as long as there are open read-only transactions referring to it, thus we must maintain some sort of WAL to keep track of pending cleanup. | |||
* When a transaction is closed, or when we recover from a crash, it should clean up all no longer needed data to avoid memory/storage leaks. | |||
* **Security and Auth** | |||
* If we allow connections over gRPC, we must consider this issues and allow both encryption (SSL), and some basic auth rules to prevent undesired access to the DB | |||
* This is client-specific and does not need to be supported in the in-process, embedded version. | |||
## Details | |||
Here we go more in-depth in each of the sections, explaining the reasoning and more details on the desired behavior. This document is only the high-level architecture and should support multiple implementations. When building out a specific implementation, a similar document should be provided for that repo, showing how it implements these concepts, and details about memory usage, storage, efficiency, etc. | |||
### State | |||
The current ABCi interface avoids this question a bit and that has brought confusion. If I use `merkleeyes` to store data, which state is returned from `Query`? The current "working" state, which I would like to refer to in my ABCi application? Or the last committed state, which I would like to return to a client's query? Or an old state, which I may select based on height? | |||
Right now, `merkleeyes` implements `Query` like a normal ABCi app and only returns committed state, which has lead to problems and confusion. Thus, we need to be explicit about which state we want to view. Each viewer can then specify which state it wants to view. This allows the app to query the working state in DeliverTx, but the committed state in Query. | |||
We can easily provide two global references for "last committed" and "current working" states. However, if we want to also allow querying of older commits... then we need some way to keep track of which ones are still in use, so we can garbage collect the unneeded ones. There is a non-trivial overhead in holding references to all past states, but also a hard-coded solution (hold onto the last 5 commits) may not support all clients. We should let the client define this somehow. | |||
### Transactions | |||
Transactions (in the typical database sense) are a clean and established solution to this issue. We can look at the [isolations levels](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable) which attempt to provide us things like "repeatable reads". That means if we open a transaction, and query some data 100 times while other processes are writing to the db, we get the same result each time. This transaction has a reference to its own local state from the time the transaction started. (We are referring to the highest isolation levels here, which correlate well this the blockchain use case). | |||
If we implement a read-only transaction as a reference to state at the time of creation of that transaction, we can then hold these references to various snapshots, one per block that we are interested, and allow the client to multiplex queries and proofs from these various blocks. | |||
If we continue using these concepts (which have informed 30+ years of server side design), we can add a few nice features to our write transactions. The first of which is `Rollback` and `Commit`. That means all the changes we make in this transaction have no effect on the database until they are committed. And until they are committed, we can always abort if we detect an anomaly, returning to the last committed state with a rollback. | |||
There is also a nice extension to this available on some database servers, basically, "nested" transactions or "savepoints". This means that within one transaction, you can open a subtransaction/savepoint and continue work. Later you have the option to commit or rollback all work since the savepoint/subtransaction. And then continue with the main transaction. | |||
If you don't understand why this is useful, look at how basecoin needs to [hold cached state for AppTx](https://github.com/tendermint/basecoin/blob/master/state/execution.go#L126-L149), meaning that it rolls back all modifications if the AppTx returns an error. This was implemented as a wrapper in basecoin, but it is a reasonable thing to support in the DB interface itself (especially since the implementation becomes quite non-trivial as soon as you support range queries). | |||
To give a bit more reference to this concept in practice, read about [Savepoints in Postgresql](https://www.postgresql.org/docs/current/static/tutorial-transactions.html) ([reference](https://www.postgresql.org/docs/current/static/sql-savepoint.html)) or [Nesting transactions in SQL Server](http://dba-presents.com/index.php/databases/sql-server/43-nesting-transactions-and-save-transaction-command) (TL;DR: scroll to the bottom, section "Real nesting transactions with SAVE TRANSACTION") | |||
### Functionality | |||
Merkle trees work with key-value pairs, so we should most importantly focus on the basic Key-Value operations. That is `Get`, `Set`, and `Remove`. We also need to return a merkle proof for any key, along with a root hash of the tree for committing state to the blockchain. This is just the basic merkle-tree stuff. | |||
If it is possible with the implementation, it is nice to provide access to Range Queries. That is, return all values where the key is between X and Y. If you construct your keys wisely, it is possible to store lists (1:N) relations this way. Eg, storing blog posts and the key is blog:`poster_id`:`sequence`, then I could search for all blog posts by a given `poster_id`, or even return just posts 10-19 from the given poster. | |||
The construction of a tree that supports range queries was one of the [design decisions of go-merkle](https://github.com/tendermint/go-merkle/blob/master/README.md). It is also kind of possible with [ethereum's patricia trie](https://github.com/ethereum/wiki/wiki/Patricia-Tree) as long as the key is less than 32 bytes. | |||
In addition to range queries, there is one more nice feature that we could add to our data store - listening to events. Depending on your context, this is "reactive programming", "event emitters", "notifications", etc... But the basic concept is that a client can listen for all changes to a given key (or set of keys), and receive a notification when this happens. This is very important to avoid [repeated polling and wasted queries](http://resthooks.org/) when a client simply wants to [detect changes](https://www.rethinkdb.com/blog/realtime-web/). | |||
If the database provides access to some "listener" functionality, the app can choose to expose this to the external client via websockets, web hooks, http2 push events, android push notifications, etc, etc etc.... But if we want to support modern client functionality, let's add support for this reactive paradigm in our DB interface. | |||
**TODO** support for more advanced backends, eg. Bolt.... | |||
### Go Interface | |||
I will start with a simple go interface to illustrate the in-process interface. Once there is agreement on how this looks, we can work out the gRPC bindings to support calling out of process. These interfaces are not finalized code, but I think the demonstrate the concepts better than text and provide a strawman to get feedback. | |||
``` | |||
// DB represents the committed state of a merkle-ized key-value store | |||
type DB interface { | |||
// Snapshot returns a reference to last committed state to use for | |||
// providing proofs, you must close it at the end to garbage collect | |||
// the historical state we hold on to to make these proofs | |||
Snapshot() Prover | |||
// Start a transaction - only way to change state | |||
// This will return an error if there is an open Transaction | |||
Begin() (Transaction, error) | |||
// These callbacks are triggered when the Transaction is Committed | |||
// to the DB. They can be used to eg. notify clients via websockets when | |||
// their account balance changes. | |||
AddListener(key []byte, listener Listener) | |||
RemoveListener(listener Listener) | |||
} | |||
// DBReader represents a read-only connection to a snapshot of the db | |||
type DBReader interface { | |||
// Queries on my local view | |||
Has(key []byte) (bool, error) | |||
Get(key []byte) (Model, error) | |||
GetRange(start, end []byte, ascending bool, limit int) ([]Model, error) | |||
Closer | |||
} | |||
// Prover is an interface that lets one query for Proofs, holding the | |||
// data at a specific location in memory | |||
type Prover interface { | |||
DBReader | |||
// Hash is the AppHash (RootHash) for this block | |||
Hash() (hash []byte) | |||
// Prove returns the data along with a merkle Proof | |||
// Model and Proof are nil if not found | |||
Prove(key []byte) (Model, Proof, error) | |||
} | |||
// Transaction is a set of state changes to the DB to be applied atomically. | |||
// There can only be one open transaction at a time, which may only have | |||
// maximum one subtransaction at a time. | |||
// In short, at any time, there is exactly one object that can write to the | |||
// DB, and we can use Subtransactions to group operations and roll them back | |||
// together (kind of like `types.KVCache` from basecoin) | |||
type Transaction interface { | |||
DBReader | |||
// Change the state - will raise error immediately if this Transaction | |||
// is not holding the exclusive write lock | |||
Set(model Model) (err error) | |||
Remove(key []byte) (removed bool, err error) | |||
// Subtransaction starts a new subtransaction, rollback will not affect the | |||
// parent. Only on Commit are the changes applied to this transaction. | |||
// While the subtransaction exists, no write allowed on the parent. | |||
// (You must Commit or Rollback the child to continue) | |||
Subtransaction() Transaction | |||
// Commit this transaction (or subtransaction), the parent reference is | |||
// now updated. | |||
// This only updates persistant store if the top level transaction commits | |||
// (You may have any number of nested sub transactions) | |||
Commit() error | |||
// Rollback ends the transaction and throw away all transaction-local state, | |||
// allowing the tree to prune those elements. | |||
// The parent transaction now recovers the write lock. | |||
Rollback() | |||
} | |||
// Listener registers callbacks on changes to the data store | |||
type Listener interface { | |||
OnSet(key, value, oldValue []byte) | |||
OnRemove(key, oldValue []byte) | |||
} | |||
// Proof represents a merkle proof for a key | |||
type Proof interface { | |||
RootHash() []byte | |||
Verify(key, value, root []byte) bool | |||
} | |||
type Model interface { | |||
Key() []byte | |||
Value() []byte | |||
} | |||
// Closer releases the reference to this state, allowing us to garbage collect | |||
// Make sure to call it before discarding. | |||
type Closer interface { | |||
Close() | |||
} | |||
``` | |||
### Remote Interface | |||
The use-case of allowing out-of-process calls is very powerful. Not just to provide a powerful merkle-ready data store to non-go applications. | |||
It we allow the ABCi app to maintain the only writable connections, we can guarantee that all transactions are only processed through the tendermint consensus engine. We could then allow multiple "web server" machines "read-only" access and scale out the database reads, assuming the consensus engine, ABCi logic, and public key cryptography is more the bottleneck than the database. We could even place the consensus engine, ABCi app, and data store on one machine, connected with unix sockets for security, and expose a tcp/ssl interface for reading the data, to scale out query processing over multiple machines. | |||
But returning our focus directly to the ABCi app (which is the most important use case). An app may well want to maintain 100 or 1000 snapshots of different heights to allow people to easily query many proofs at a given height without race conditions (very important for IBC, ask Jae). Thus, we should not require a separate TCP connection for each height, as this gets quite awkward with so many connections. Also, if we want to use gRPC, we should consider the connections potentially transient (although they are more efficient with keep-alive). | |||
Thus, the wire encoding of a transaction or a snapshot should simply return a unique id. All methods on a `Prover` or `Transaction` over the wire can send this id along with the arguments for the method call. And we just need a hash map on the server to map this id to a state. | |||
The only negative of not requiring a persistent tcp connection for each snapshot is there is no auto-detection if the client crashes without explicitly closing the connections. Thus, I would suggest adding a `Ping` thread in the gRPC interface which keeps the Snapshot alive. If no ping is received within a server-defined time, it may automatically close those transactions. And if we consider a client with 500 snapshots that needs to ping each every 10 seconds, that is a lot of overhead, so we should design the ping to accept a list of IDs for the client and update them all. Or associate all snapshots with a clientID and then just send the clientID in the ping. (Please add other ideas on how to detect client crashes without persistent connections). | |||
To encourage adoption, we should provide a nice client that uses this gRPC interface (like we do with ABCi). For go, the client may have the exact same interface as the in-process version, just that the error call may return network errors, not just illegal operations. We should also add a client with a clean API for Java, since that seems to be popular among app developers in the current tendermint community. Other bindings as we see the need in the server space. | |||
### Persistence | |||
Any data store worth it's name should not lose all data on a crash. Even [redis provides some persistence](https://redis.io/topics/persistence) these days. Ideally, if the system crashes and restarts, it should have the data at the last block N that was committed. If the system crash during the commit of block N+1, then the recovered state should either be block N or completely committed block N+1, but no partial state between the two. Basically, the commit must be an atomic operation (even if updating 100's of records). | |||
To avoid a lot of headaches ourselves, we can use an existing data store, such as leveldb, which provides `WriteBatch` to group all operations. | |||
The other issue is cleaning up old state. We cannot delete any information from our persistent store, as long as any snapshot holds a reference to it (or else we get some panics when the data we query is not there). So, we need to store the outstanding deletions that we can perform when the snapshot is `Close`d. In addition, we must consider the case that the data store crashes with open snapshots. Thus, the info on outstanding deletions must also be persisted somewhere. Something like a "delete-behind log" (the opposite of a "write ahead log"). | |||
This is not a concern of the generic interface, but each implementation should take care to handle this well to avoid accumulation of unused references in the data store and eventual data bloat. | |||
#### Backing stores | |||
It is way outside the scope of this project to build our own database that is capable of efficiently storing the data, provide multiple read-only snapshots at once, and save it atomically. The best approach seems to select an existing database (best a simple one) that provides this functionality and build upon it, much like the current `go-merkle` implementation builds upon `leveldb`. After some research here are winners and losers: | |||
**Winners** | |||
* Leveldb - [provides consistent snapshots](https://ayende.com/blog/161705/reviewing-leveldb-part-xiii-smile-and-here-is-your-snapshot), and [provides tooling for building ACID compliance](http://codeofrob.com/entries/writing-a-transaction-manager-on-top-of-leveldb.html) | |||
* Note there are at least two solid implementations available in go - [goleveldb](https://github.com/syndtr/goleveldb) - a pure go implementation, and [levigo](https://github.com/jmhodges/levigo) - a go wrapper around leveldb. | |||
* Goleveldb is much easier to compile and cross-compile (not requiring cgo), while levigo (or cleveldb) seems to provide a significant performance boosts (but I had trouble even running benchmarks) | |||
* PostgreSQL - fully supports these ACID semantics if you call `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE` at the beginning of a transaction (tested) | |||
* This may be total overkill unless we also want to make use of other features, like storing data in multiple columns with secondary indexes. | |||
* Trillian can show an example of [how to store a merkle tree in sql](https://github.com/google/trillian/blob/master/storage/mysql/tree_storage.go) | |||
**Losers** | |||
* Bolt - open [read-only snapshots can block writing](https://github.com/boltdb/bolt/issues/378) | |||
* Mongo - [barely even supports atomic operations](https://docs.mongodb.com/manual/core/write-operations-atomicity/), much less multiple snapshots | |||
**To investigate** | |||
* [Trillian](https://github.com/google/trillian) - has a [persistent merkle tree interface](https://github.com/google/trillian/blob/master/storage/tree_storage.go) along with [backend storage with mysql](https://github.com/google/trillian/blob/master/storage/mysql/tree_storage.go), good inspiration for our design if not directly using it | |||
* [Moss](https://github.com/couchbase/moss) - another key-value store in go, seems similar to leveldb, maybe compare with performance tests? | |||
### Security | |||
When allowing access out-of-process, we should provide different mechanisms to secure it. The first is the choice of binding to a local unix socket or a tcp port. The second is the optional use of ssl to encrypt the connection (very important over tcp). The third is authentication to control access to the database. | |||
We may also want to consider the case of two server connections with different permissions, eg. a local unix socket that allows write access with no more credentials, and a public TCP connection with ssl and authentication that only provides read-only access. | |||
The use of ssl is quite easy in go, we just need to generate and sign a certificate, so it is nice to be able to disable it for dev machines, but it is very important for production. | |||
For authentication, let me sketch out a minimal solution. The server could just have a simple config file with key/bcrypt(password) pairs along with read/write permission level, and read that upon startup. The client must provide a username and password in the HTTP headers when making the original HTTPS gRPC connection. | |||
This is super minimal to provide some protection. Things like LDAP, OAuth and single-sign on seem overkill and even potential security holes. Maybe there is another solution somewhere in the middle. |
@ -0,0 +1,17 @@ | |||
# Merkle data stores | |||
To allow the efficient creation of an ABCi app, tendermint wishes to provide a reference implemention of a key-value store that provides merkle proofs of the data. These proofs then quickly allow the ABCi app to provide an apphash to the consensus engine, as well as a full proof to any client. | |||
This engine is currently implemented in `go-merkle` with `merkleeyes` providing a language-agnostic binding via ABCi. It uses `go-db` bindings internally to persist data to leveldb. | |||
What are some of the requirements of this store: | |||
* It must support efficient key-value operations (get/set/delete) | |||
* It must support persistance. | |||
* We must only persist complete blocks, so when we come up after a crash we are at the state of block N or N+1, but not in-between these two states. | |||
* It must allow us to read/write from one uncommited state (working state), while serving other queries from the last commited state. And a way to determine which one to serve for each client. | |||
* It must allow us to hold references to old state, to allow providing proofs from 20 blocks ago. We can define some limits as to the maximum time to hold this data. | |||
* We provide in process binding in Go | |||
* We provide language-agnostic bindings when running the data store as it's own process. | |||
@ -1,29 +1,53 @@ | |||
package: github.com/tendermint/tendermint | |||
import: | |||
- package: github.com/gogo/protobuf | |||
subpackages: | |||
- proto | |||
- package: github.com/gorilla/websocket | |||
- package: github.com/spf13/pflag | |||
- package: github.com/tendermint/ed25519 | |||
- package: github.com/tendermint/go-flowrate | |||
- package: github.com/tendermint/go-autofile | |||
version: develop | |||
- package: github.com/tendermint/go-clist | |||
version: develop | |||
- package: github.com/tendermint/go-common | |||
version: develop | |||
- package: github.com/tendermint/go-config | |||
version: develop | |||
- package: github.com/tendermint/go-crypto | |||
version: develop | |||
- package: github.com/tendermint/go-db | |||
version: develop | |||
- package: github.com/tendermint/go-events | |||
version: develop | |||
- package: github.com/tendermint/go-logger | |||
version: develop | |||
- package: github.com/tendermint/go-merkle | |||
version: develop | |||
- package: github.com/tendermint/go-p2p | |||
version: develop | |||
- package: github.com/tendermint/go-rpc | |||
version: develop | |||
- package: github.com/tendermint/go-wire | |||
- package: github.com/tendermint/log15 | |||
version: develop | |||
- package: github.com/tendermint/abci | |||
version: develop | |||
- package: github.com/tendermint/go-flowrate | |||
- package: github.com/tendermint/log15 | |||
- package: github.com/tendermint/ed25519 | |||
- package: github.com/tendermint/merkleeyes | |||
version: develop | |||
subpackages: | |||
- app | |||
- package: github.com/gogo/protobuf | |||
version: ^0.3 | |||
subpackages: | |||
- proto | |||
- package: github.com/gorilla/websocket | |||
version: ^1.1.0 | |||
- package: github.com/spf13/pflag | |||
- package: github.com/pkg/errors | |||
version: ^0.8.0 | |||
- package: golang.org/x/crypto | |||
subpackages: | |||
- ripemd160 | |||
- package: github.com/tendermint/go-flowrate | |||
testImport: | |||
- package: github.com/stretchr/testify | |||
version: ^1.1.4 | |||
subpackages: | |||
- flowrate | |||
- assert | |||
- require |
@ -0,0 +1,65 @@ | |||
package client_test | |||
import ( | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/require" | |||
merktest "github.com/tendermint/merkleeyes/testutil" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func TestHeaderEvents(t *testing.T) { | |||
require := require.New(t) | |||
for i, c := range GetClients() { | |||
// start for this test it if it wasn't already running | |||
if !c.IsRunning() { | |||
// if so, then we start it, listen, and stop it. | |||
st, err := c.Start() | |||
require.Nil(err, "%d: %+v", i, err) | |||
require.True(st, "%d", i) | |||
defer c.Stop() | |||
} | |||
evtTyp := types.EventStringNewBlockHeader() | |||
evt, err := client.WaitForOneEvent(c, evtTyp, 1*time.Second) | |||
require.Nil(err, "%d: %+v", i, err) | |||
_, ok := evt.(types.EventDataNewBlockHeader) | |||
require.True(ok, "%d: %#v", i, evt) | |||
// TODO: more checks... | |||
} | |||
} | |||
func TestTxEvents(t *testing.T) { | |||
require := require.New(t) | |||
for i, c := range GetClients() { | |||
// start for this test it if it wasn't already running | |||
if !c.IsRunning() { | |||
// if so, then we start it, listen, and stop it. | |||
st, err := c.Start() | |||
require.Nil(err, "%d: %+v", i, err) | |||
require.True(st, "%d", i) | |||
defer c.Stop() | |||
} | |||
// make the tx | |||
_, _, tx := merktest.MakeTxKV() | |||
evtTyp := types.EventStringTx(types.Tx(tx)) | |||
// send async | |||
txres, err := c.BroadcastTxAsync(tx) | |||
require.Nil(err, "%+v", err) | |||
require.True(txres.Code.IsOK()) | |||
// and wait for confirmation | |||
evt, err := client.WaitForOneEvent(c, evtTyp, 1*time.Second) | |||
require.Nil(err, "%d: %+v", i, err) | |||
// and make sure it has the proper info | |||
txe, ok := evt.(types.EventDataTx) | |||
require.True(ok, "%d: %#v", i, evt) | |||
// make sure this is the proper tx | |||
require.EqualValues(tx, txe.Tx) | |||
require.True(txe.Code.IsOK()) | |||
} | |||
} |
@ -0,0 +1,88 @@ | |||
package client | |||
import ( | |||
"time" | |||
"github.com/pkg/errors" | |||
cmn "github.com/tendermint/go-common" | |||
events "github.com/tendermint/go-events" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Waiter is informed of current height, decided whether to quit early | |||
type Waiter func(delta int) (abort error) | |||
// DefaultWaitStrategy is the standard backoff algorithm, | |||
// but you can plug in another one | |||
func DefaultWaitStrategy(delta int) (abort error) { | |||
if delta > 10 { | |||
return errors.Errorf("Waiting for %d blocks... aborting", delta) | |||
} else if delta > 0 { | |||
// estimate of wait time.... | |||
// wait half a second for the next block (in progress) | |||
// plus one second for every full block | |||
delay := time.Duration(delta-1)*time.Second + 500*time.Millisecond | |||
time.Sleep(delay) | |||
} | |||
return nil | |||
} | |||
// Wait for height will poll status at reasonable intervals until | |||
// the block at the given height is available. | |||
// | |||
// If waiter is nil, we use DefaultWaitStrategy, but you can also | |||
// provide your own implementation | |||
func WaitForHeight(c StatusClient, h int, waiter Waiter) error { | |||
if waiter == nil { | |||
waiter = DefaultWaitStrategy | |||
} | |||
delta := 1 | |||
for delta > 0 { | |||
s, err := c.Status() | |||
if err != nil { | |||
return err | |||
} | |||
delta = h - s.LatestBlockHeight | |||
// wait for the time, or abort early | |||
if err := waiter(delta); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
// WaitForOneEvent subscribes to a websocket event for the given | |||
// event time and returns upon receiving it one time, or | |||
// when the timeout duration has expired. | |||
// | |||
// This handles subscribing and unsubscribing under the hood | |||
func WaitForOneEvent(evsw types.EventSwitch, | |||
evtTyp string, timeout time.Duration) (types.TMEventData, error) { | |||
listener := cmn.RandStr(12) | |||
evts, quit := make(chan events.EventData, 10), make(chan bool, 1) | |||
// start timeout count-down | |||
go func() { | |||
time.Sleep(timeout) | |||
quit <- true | |||
}() | |||
// register for the next event of this type | |||
evsw.AddListenerForEvent(listener, evtTyp, func(data events.EventData) { | |||
evts <- data | |||
}) | |||
// make sure to unregister after the test is over | |||
defer evsw.RemoveListenerForEvent(evtTyp, listener) | |||
// defer evsw.RemoveListener(listener) // this also works | |||
select { | |||
case <-quit: | |||
return nil, errors.New("timed out waiting for event") | |||
case evt := <-evts: | |||
tmevt, ok := evt.(types.TMEventData) | |||
if ok { | |||
return tmevt, nil | |||
} | |||
return nil, errors.Errorf("Got unexpected event type: %#v", evt) | |||
} | |||
} |
@ -0,0 +1,76 @@ | |||
package client_test | |||
import ( | |||
"errors" | |||
"strings" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
"github.com/tendermint/tendermint/rpc/client/mock" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
func TestWaitForHeight(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
// test with error result - immediate failure | |||
m := &mock.StatusMock{ | |||
Call: mock.Call{ | |||
Error: errors.New("bye"), | |||
}, | |||
} | |||
r := mock.NewStatusRecorder(m) | |||
// connection failure always leads to error | |||
err := client.WaitForHeight(r, 8, nil) | |||
require.NotNil(err) | |||
require.Equal("bye", err.Error()) | |||
// we called status once to check | |||
require.Equal(1, len(r.Calls)) | |||
// now set current block height to 10 | |||
m.Call = mock.Call{ | |||
Response: &ctypes.ResultStatus{LatestBlockHeight: 10}, | |||
} | |||
// we will not wait for more than 10 blocks | |||
err = client.WaitForHeight(r, 40, nil) | |||
require.NotNil(err) | |||
require.True(strings.Contains(err.Error(), "aborting")) | |||
// we called status once more to check | |||
require.Equal(2, len(r.Calls)) | |||
// waiting for the past returns immediately | |||
err = client.WaitForHeight(r, 5, nil) | |||
require.Nil(err) | |||
// we called status once more to check | |||
require.Equal(3, len(r.Calls)) | |||
// since we can't update in a background goroutine (test --race) | |||
// we use the callback to update the status height | |||
myWaiter := func(delta int) error { | |||
// update the height for the next call | |||
m.Call.Response = &ctypes.ResultStatus{LatestBlockHeight: 15} | |||
return client.DefaultWaitStrategy(delta) | |||
} | |||
// we wait for a few blocks | |||
err = client.WaitForHeight(r, 12, myWaiter) | |||
require.Nil(err) | |||
// we called status once to check | |||
require.Equal(5, len(r.Calls)) | |||
pre := r.Calls[3] | |||
require.Nil(pre.Error) | |||
prer, ok := pre.Response.(*ctypes.ResultStatus) | |||
require.True(ok) | |||
assert.Equal(10, prer.LatestBlockHeight) | |||
post := r.Calls[4] | |||
require.Nil(post.Error) | |||
postr, ok := post.Response.(*ctypes.ResultStatus) | |||
require.True(ok) | |||
assert.Equal(15, postr.LatestBlockHeight) | |||
} |
@ -0,0 +1,349 @@ | |||
package client | |||
import ( | |||
"fmt" | |||
"github.com/pkg/errors" | |||
events "github.com/tendermint/go-events" | |||
"github.com/tendermint/go-rpc/client" | |||
wire "github.com/tendermint/go-wire" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
/* | |||
HTTP is a Client implementation that communicates | |||
with a tendermint node over json rpc and websockets. | |||
This is the main implementation you probably want to use in | |||
production code. There are other implementations when calling | |||
the tendermint node in-process (local), or when you want to mock | |||
out the server for test code (mock). | |||
*/ | |||
type HTTP struct { | |||
remote string | |||
rpc *rpcclient.ClientJSONRPC | |||
*WSEvents | |||
} | |||
// New takes a remote endpoint in the form tcp://<host>:<port> | |||
// and the websocket path (which always seems to be "/websocket") | |||
func NewHTTP(remote, wsEndpoint string) *HTTP { | |||
return &HTTP{ | |||
rpc: rpcclient.NewClientJSONRPC(remote), | |||
remote: remote, | |||
WSEvents: newWSEvents(remote, wsEndpoint), | |||
} | |||
} | |||
func (c *HTTP) _assertIsClient() Client { | |||
return c | |||
} | |||
func (c *HTTP) _assertIsNetworkClient() NetworkClient { | |||
return c | |||
} | |||
func (c *HTTP) _assertIsEventSwitch() types.EventSwitch { | |||
return c | |||
} | |||
func (c *HTTP) Status() (*ctypes.ResultStatus, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("status", []interface{}{}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "Status") | |||
} | |||
// note: panics if rpc doesn't match. okay??? | |||
return (*tmResult).(*ctypes.ResultStatus), nil | |||
} | |||
func (c *HTTP) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("abci_info", []interface{}{}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "ABCIInfo") | |||
} | |||
return (*tmResult).(*ctypes.ResultABCIInfo), nil | |||
} | |||
func (c *HTTP) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("abci_query", []interface{}{path, data, prove}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "ABCIQuery") | |||
} | |||
return (*tmResult).(*ctypes.ResultABCIQuery), nil | |||
} | |||
func (c *HTTP) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("broadcast_tx_commit", []interface{}{tx}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "broadcast_tx_commit") | |||
} | |||
return (*tmResult).(*ctypes.ResultBroadcastTxCommit), nil | |||
} | |||
func (c *HTTP) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return c.broadcastTX("broadcast_tx_async", tx) | |||
} | |||
func (c *HTTP) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return c.broadcastTX("broadcast_tx_sync", tx) | |||
} | |||
func (c *HTTP) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call(route, []interface{}{tx}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, route) | |||
} | |||
return (*tmResult).(*ctypes.ResultBroadcastTx), nil | |||
} | |||
func (c *HTTP) NetInfo() (*ctypes.ResultNetInfo, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("net_info", nil, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "NetInfo") | |||
} | |||
return (*tmResult).(*ctypes.ResultNetInfo), nil | |||
} | |||
func (c *HTTP) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("dump_consensus_state", nil, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "DumpConsensusState") | |||
} | |||
return (*tmResult).(*ctypes.ResultDumpConsensusState), nil | |||
} | |||
func (c *HTTP) BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("blockchain", []interface{}{minHeight, maxHeight}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "BlockchainInfo") | |||
} | |||
return (*tmResult).(*ctypes.ResultBlockchainInfo), nil | |||
} | |||
func (c *HTTP) Genesis() (*ctypes.ResultGenesis, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("genesis", nil, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "Genesis") | |||
} | |||
return (*tmResult).(*ctypes.ResultGenesis), nil | |||
} | |||
func (c *HTTP) Block(height int) (*ctypes.ResultBlock, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("block", []interface{}{height}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "Block") | |||
} | |||
return (*tmResult).(*ctypes.ResultBlock), nil | |||
} | |||
func (c *HTTP) Commit(height int) (*ctypes.ResultCommit, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("commit", []interface{}{height}, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "Commit") | |||
} | |||
return (*tmResult).(*ctypes.ResultCommit), nil | |||
} | |||
func (c *HTTP) Validators() (*ctypes.ResultValidators, error) { | |||
tmResult := new(ctypes.TMResult) | |||
_, err := c.rpc.Call("validators", nil, tmResult) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "Validators") | |||
} | |||
return (*tmResult).(*ctypes.ResultValidators), nil | |||
} | |||
/** websocket event stuff here... **/ | |||
type WSEvents struct { | |||
types.EventSwitch | |||
remote string | |||
endpoint string | |||
ws *rpcclient.WSClient | |||
// used for signaling the goroutine that feeds ws -> EventSwitch | |||
quit chan bool | |||
done chan bool | |||
// used to maintain counts of actively listened events | |||
// so we can properly subscribe/unsubscribe | |||
// FIXME: thread-safety??? | |||
// FIXME: reuse code from go-events??? | |||
evtCount map[string]int // count how many time each event is subscribed | |||
listeners map[string][]string // keep track of which events each listener is listening to | |||
} | |||
func newWSEvents(remote, endpoint string) *WSEvents { | |||
return &WSEvents{ | |||
EventSwitch: types.NewEventSwitch(), | |||
endpoint: endpoint, | |||
remote: remote, | |||
quit: make(chan bool, 1), | |||
done: make(chan bool, 1), | |||
evtCount: map[string]int{}, | |||
listeners: map[string][]string{}, | |||
} | |||
} | |||
func (w *WSEvents) _assertIsEventSwitch() types.EventSwitch { | |||
return w | |||
} | |||
// Start is the only way I could think the extend OnStart from | |||
// events.eventSwitch. If only it wasn't private... | |||
// BaseService.Start -> eventSwitch.OnStart -> WSEvents.Start | |||
func (w *WSEvents) Start() (bool, error) { | |||
st, err := w.EventSwitch.Start() | |||
// if we did start, then OnStart here... | |||
if st && err == nil { | |||
ws := rpcclient.NewWSClient(w.remote, w.endpoint) | |||
_, err = ws.Start() | |||
if err == nil { | |||
w.ws = ws | |||
go w.eventListener() | |||
} | |||
} | |||
return st, errors.Wrap(err, "StartWSEvent") | |||
} | |||
// Stop wraps the BaseService/eventSwitch actions as Start does | |||
func (w *WSEvents) Stop() bool { | |||
stop := w.EventSwitch.Stop() | |||
if stop { | |||
// send a message to quit to stop the eventListener | |||
w.quit <- true | |||
<-w.done | |||
w.ws.Stop() | |||
w.ws = nil | |||
} | |||
return stop | |||
} | |||
/** TODO: more intelligent subscriptions! **/ | |||
func (w *WSEvents) AddListenerForEvent(listenerID, event string, cb events.EventCallback) { | |||
// no one listening -> subscribe | |||
if w.evtCount[event] == 0 { | |||
w.subscribe(event) | |||
} | |||
// if this listener was already listening to this event, return early | |||
for _, s := range w.listeners[listenerID] { | |||
if event == s { | |||
return | |||
} | |||
} | |||
// otherwise, add this event to this listener | |||
w.evtCount[event] += 1 | |||
w.listeners[listenerID] = append(w.listeners[listenerID], event) | |||
w.EventSwitch.AddListenerForEvent(listenerID, event, cb) | |||
} | |||
func (w *WSEvents) RemoveListenerForEvent(event string, listenerID string) { | |||
// if this listener is listening already, splice it out | |||
found := false | |||
l := w.listeners[listenerID] | |||
for i, s := range l { | |||
if event == s { | |||
found = true | |||
w.listeners[listenerID] = append(l[:i], l[i+1:]...) | |||
break | |||
} | |||
} | |||
// if the listener wasn't already listening to the event, exit early | |||
if !found { | |||
return | |||
} | |||
// now we can update the subscriptions | |||
w.evtCount[event] -= 1 | |||
if w.evtCount[event] == 0 { | |||
w.unsubscribe(event) | |||
} | |||
w.EventSwitch.RemoveListenerForEvent(event, listenerID) | |||
} | |||
func (w *WSEvents) RemoveListener(listenerID string) { | |||
// remove all counts for this listener | |||
for _, s := range w.listeners[listenerID] { | |||
w.evtCount[s] -= 1 | |||
if w.evtCount[s] == 0 { | |||
w.unsubscribe(s) | |||
} | |||
} | |||
w.listeners[listenerID] = nil | |||
// then let the switch do it's magic | |||
w.EventSwitch.RemoveListener(listenerID) | |||
} | |||
// eventListener is an infinite loop pulling all websocket events | |||
// and pushing them to the EventSwitch. | |||
// | |||
// the goroutine only stops by closing quit | |||
func (w *WSEvents) eventListener() { | |||
for { | |||
select { | |||
case res := <-w.ws.ResultsCh: | |||
// res is json.RawMessage | |||
err := w.parseEvent(res) | |||
if err != nil { | |||
// FIXME: better logging/handling of errors?? | |||
fmt.Printf("ws result: %+v\n", err) | |||
} | |||
case err := <-w.ws.ErrorsCh: | |||
// FIXME: better logging/handling of errors?? | |||
fmt.Printf("ws err: %+v\n", err) | |||
case <-w.quit: | |||
// send a message so we can wait for the routine to exit | |||
// before cleaning up the w.ws stuff | |||
w.done <- true | |||
return | |||
} | |||
} | |||
} | |||
// parseEvent unmarshals the json message and converts it into | |||
// some implementation of types.TMEventData, and sends it off | |||
// on the merry way to the EventSwitch | |||
func (w *WSEvents) parseEvent(data []byte) (err error) { | |||
result := new(ctypes.TMResult) | |||
wire.ReadJSONPtr(result, data, &err) | |||
if err != nil { | |||
return err | |||
} | |||
event, ok := (*result).(*ctypes.ResultEvent) | |||
if !ok { | |||
// ignore silently (eg. subscribe, unsubscribe and maybe other events) | |||
return nil | |||
} | |||
// looks good! let's fire this baby! | |||
w.EventSwitch.FireEvent(event.Name, event.Data) | |||
return nil | |||
} | |||
// no way of exposing these failures, so we panic. | |||
// is this right? or silently ignore??? | |||
func (w *WSEvents) subscribe(event string) { | |||
err := w.ws.Subscribe(event) | |||
if err != nil { | |||
panic(err) | |||
} | |||
} | |||
func (w *WSEvents) unsubscribe(event string) { | |||
err := w.ws.Unsubscribe(event) | |||
if err != nil { | |||
panic(err) | |||
} | |||
} |
@ -0,0 +1,82 @@ | |||
/* | |||
package client provides a general purpose interface (Client) for connecting | |||
to a tendermint node, as well as higher-level functionality. | |||
The main implementation for production code is client.HTTP, which | |||
connects via http to the jsonrpc interface of the tendermint node. | |||
For connecting to a node running in the same process (eg. when | |||
compiling the abci app in the same process), you can use the client.Local | |||
implementation. | |||
For mocking out server responses during testing to see behavior for | |||
arbitrary return values, use the mock package. | |||
In addition to the Client interface, which should be used externally | |||
for maximum flexibility and testability, and two implementations, | |||
this package also provides helper functions that work on any Client | |||
implementation. | |||
*/ | |||
package client | |||
import ( | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// ABCIClient groups together the functionality that principally | |||
// affects the ABCI app. In many cases this will be all we want, | |||
// so we can accept an interface which is easier to mock | |||
type ABCIClient interface { | |||
// reading from abci app | |||
ABCIInfo() (*ctypes.ResultABCIInfo, error) | |||
ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) | |||
// writing to abci app | |||
BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) | |||
BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) | |||
BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) | |||
} | |||
// SignClient groups together the interfaces need to get valid | |||
// signatures and prove anything about the chain | |||
type SignClient interface { | |||
Block(height int) (*ctypes.ResultBlock, error) | |||
Commit(height int) (*ctypes.ResultCommit, error) | |||
Validators() (*ctypes.ResultValidators, error) | |||
} | |||
// HistoryClient shows us data from genesis to now in large chunks. | |||
type HistoryClient interface { | |||
Genesis() (*ctypes.ResultGenesis, error) | |||
BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) | |||
} | |||
type StatusClient interface { | |||
// general chain info | |||
Status() (*ctypes.ResultStatus, error) | |||
} | |||
// Client wraps most important rpc calls a client would make | |||
// if you want to listen for events, test if it also | |||
// implements events.EventSwitch | |||
type Client interface { | |||
ABCIClient | |||
SignClient | |||
HistoryClient | |||
StatusClient | |||
// this Client is reactive, you can subscribe to any TMEventData | |||
// type, given the proper string. see tendermint/types/events.go | |||
types.EventSwitch | |||
} | |||
// NetworkClient is general info about the network state. May not | |||
// be needed usually. | |||
// | |||
// Not included in the Client interface, but generally implemented | |||
// by concrete implementations. | |||
type NetworkClient interface { | |||
NetInfo() (*ctypes.ResultNetInfo, error) | |||
DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) | |||
} |
@ -0,0 +1,105 @@ | |||
package client | |||
import ( | |||
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" | |||
) | |||
/* | |||
Local is a Client implementation that directly executes the rpc | |||
functions on a given node, without going through HTTP or GRPC | |||
This implementation is useful for: | |||
* Running tests against a node in-process without the overhead | |||
of going through an http server | |||
* Communication between an ABCI app and tendermin core when they | |||
are compiled in process. | |||
For real clients, you probably want to use client.HTTP. For more | |||
powerful control during testing, you probably want the "client/mock" package. | |||
*/ | |||
type Local struct { | |||
node *nm.Node | |||
types.EventSwitch | |||
} | |||
// NewLocal configures a client that calls the Node directly. | |||
// | |||
// Note that given how rpc/core works with package singletons, that | |||
// you can only have one node per process. So make sure test cases | |||
// don't run in parallel, or try to simulate an entire network in | |||
// one process... | |||
func NewLocal(node *nm.Node) Local { | |||
node.ConfigureRPC() | |||
return Local{ | |||
node: node, | |||
EventSwitch: node.EventSwitch(), | |||
} | |||
} | |||
func (c Local) _assertIsClient() Client { | |||
return c | |||
} | |||
func (c Local) _assertIsNetworkClient() NetworkClient { | |||
return c | |||
} | |||
func (c Local) Status() (*ctypes.ResultStatus, error) { | |||
return core.Status() | |||
} | |||
func (c Local) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
return core.ABCIInfo() | |||
} | |||
func (c Local) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
return core.ABCIQuery(path, data, prove) | |||
} | |||
func (c Local) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
return core.BroadcastTxCommit(tx) | |||
} | |||
func (c Local) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return core.BroadcastTxAsync(tx) | |||
} | |||
func (c Local) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return core.BroadcastTxSync(tx) | |||
} | |||
func (c Local) NetInfo() (*ctypes.ResultNetInfo, error) { | |||
return core.NetInfo() | |||
} | |||
func (c Local) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { | |||
return core.DumpConsensusState() | |||
} | |||
func (c Local) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { | |||
return core.UnsafeDialSeeds(seeds) | |||
} | |||
func (c Local) BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) { | |||
return core.BlockchainInfo(minHeight, maxHeight) | |||
} | |||
func (c Local) Genesis() (*ctypes.ResultGenesis, error) { | |||
return core.Genesis() | |||
} | |||
func (c Local) Block(height int) (*ctypes.ResultBlock, error) { | |||
return core.Block(height) | |||
} | |||
func (c Local) Commit(height int) (*ctypes.ResultCommit, error) { | |||
return core.Commit(height) | |||
} | |||
func (c Local) Validators() (*ctypes.ResultValidators, error) { | |||
return core.Validators() | |||
} |
@ -0,0 +1,24 @@ | |||
package client_test | |||
import ( | |||
"os" | |||
"testing" | |||
meapp "github.com/tendermint/merkleeyes/app" | |||
nm "github.com/tendermint/tendermint/node" | |||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||
) | |||
var node *nm.Node | |||
func TestMain(m *testing.M) { | |||
// start a tendermint node (and merkleeyes) in the background to test against | |||
app := meapp.NewMerkleEyesApp("", 100) | |||
node = rpctest.StartTendermint(app) | |||
code := m.Run() | |||
// and shut down proper at the end | |||
node.Stop() | |||
node.Wait() | |||
os.Exit(code) | |||
} |
@ -0,0 +1,194 @@ | |||
package mock | |||
import ( | |||
abci "github.com/tendermint/abci/types" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// ABCIApp will send all abci related request to the named app, | |||
// so you can test app behavior from a client without needing | |||
// an entire tendermint node | |||
type ABCIApp struct { | |||
App abci.Application | |||
} | |||
func (a ABCIApp) _assertABCIClient() client.ABCIClient { | |||
return a | |||
} | |||
func (a ABCIApp) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
return &ctypes.ResultABCIInfo{a.App.Info()}, nil | |||
} | |||
func (a ABCIApp) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
q := a.App.Query(abci.RequestQuery{data, path, 0, prove}) | |||
return &ctypes.ResultABCIQuery{q}, nil | |||
} | |||
func (a ABCIApp) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
res := ctypes.ResultBroadcastTxCommit{} | |||
c := a.App.CheckTx(tx) | |||
res.CheckTx = &abci.ResponseCheckTx{c.Code, c.Data, c.Log} | |||
if !c.IsOK() { | |||
return &res, nil | |||
} | |||
d := a.App.DeliverTx(tx) | |||
res.DeliverTx = &abci.ResponseDeliverTx{d.Code, d.Data, d.Log} | |||
return &res, nil | |||
} | |||
func (a ABCIApp) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
c := a.App.CheckTx(tx) | |||
// and this gets writen in a background thread... | |||
if c.IsOK() { | |||
go func() { a.App.DeliverTx(tx) }() | |||
} | |||
return &ctypes.ResultBroadcastTx{c.Code, c.Data, c.Log}, nil | |||
} | |||
func (a ABCIApp) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
c := a.App.CheckTx(tx) | |||
// and this gets writen in a background thread... | |||
if c.IsOK() { | |||
go func() { a.App.DeliverTx(tx) }() | |||
} | |||
return &ctypes.ResultBroadcastTx{c.Code, c.Data, c.Log}, nil | |||
} | |||
// ABCIMock will send all abci related request to the named app, | |||
// so you can test app behavior from a client without needing | |||
// an entire tendermint node | |||
type ABCIMock struct { | |||
Info Call | |||
Query Call | |||
BroadcastCommit Call | |||
Broadcast Call | |||
} | |||
func (m ABCIMock) _assertABCIClient() client.ABCIClient { | |||
return m | |||
} | |||
func (m ABCIMock) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
res, err := m.Info.GetResponse(nil) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &ctypes.ResultABCIInfo{res.(abci.ResponseInfo)}, nil | |||
} | |||
func (m ABCIMock) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
res, err := m.Query.GetResponse(QueryArgs{path, data, prove}) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &ctypes.ResultABCIQuery{res.(abci.ResponseQuery)}, nil | |||
} | |||
func (m ABCIMock) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
res, err := m.BroadcastCommit.GetResponse(tx) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return res.(*ctypes.ResultBroadcastTxCommit), nil | |||
} | |||
func (m ABCIMock) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
res, err := m.Broadcast.GetResponse(tx) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return res.(*ctypes.ResultBroadcastTx), nil | |||
} | |||
func (m ABCIMock) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
res, err := m.Broadcast.GetResponse(tx) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return res.(*ctypes.ResultBroadcastTx), nil | |||
} | |||
// ABCIRecorder can wrap another type (ABCIApp, ABCIMock, or Client) | |||
// and record all ABCI related calls. | |||
type ABCIRecorder struct { | |||
Client client.ABCIClient | |||
Calls []Call | |||
} | |||
func NewABCIRecorder(client client.ABCIClient) *ABCIRecorder { | |||
return &ABCIRecorder{ | |||
Client: client, | |||
Calls: []Call{}, | |||
} | |||
} | |||
func (r *ABCIRecorder) _assertABCIClient() client.ABCIClient { | |||
return r | |||
} | |||
type QueryArgs struct { | |||
Path string | |||
Data []byte | |||
Prove bool | |||
} | |||
func (r *ABCIRecorder) addCall(call Call) { | |||
r.Calls = append(r.Calls, call) | |||
} | |||
func (r *ABCIRecorder) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
res, err := r.Client.ABCIInfo() | |||
r.addCall(Call{ | |||
Name: "abci_info", | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} | |||
func (r *ABCIRecorder) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
res, err := r.Client.ABCIQuery(path, data, prove) | |||
r.addCall(Call{ | |||
Name: "abci_query", | |||
Args: QueryArgs{path, data, prove}, | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} | |||
func (r *ABCIRecorder) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
res, err := r.Client.BroadcastTxCommit(tx) | |||
r.addCall(Call{ | |||
Name: "broadcast_tx_commit", | |||
Args: tx, | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} | |||
func (r *ABCIRecorder) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
res, err := r.Client.BroadcastTxAsync(tx) | |||
r.addCall(Call{ | |||
Name: "broadcast_tx_async", | |||
Args: tx, | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} | |||
func (r *ABCIRecorder) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
res, err := r.Client.BroadcastTxSync(tx) | |||
r.addCall(Call{ | |||
Name: "broadcast_tx_sync", | |||
Args: tx, | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} |
@ -0,0 +1,169 @@ | |||
package mock_test | |||
import ( | |||
"fmt" | |||
"testing" | |||
"github.com/pkg/errors" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/abci/example/dummy" | |||
abci "github.com/tendermint/abci/types" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
"github.com/tendermint/tendermint/rpc/client/mock" | |||
) | |||
func TestABCIMock(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
key, value := []byte("foo"), []byte("bar") | |||
height := uint64(10) | |||
goodTx := types.Tx{0x01, 0xff} | |||
badTx := types.Tx{0x12, 0x21} | |||
m := mock.ABCIMock{ | |||
Info: mock.Call{Error: errors.New("foobar")}, | |||
Query: mock.Call{Response: abci.ResponseQuery{ | |||
Key: key, | |||
Value: value, | |||
Height: height, | |||
}}, | |||
// Broadcast commit depends on call | |||
BroadcastCommit: mock.Call{ | |||
Args: goodTx, | |||
Response: &ctypes.ResultBroadcastTxCommit{ | |||
CheckTx: &abci.ResponseCheckTx{Data: []byte("stand")}, | |||
DeliverTx: &abci.ResponseDeliverTx{Data: []byte("deliver")}, | |||
}, | |||
Error: errors.New("bad tx"), | |||
}, | |||
Broadcast: mock.Call{Error: errors.New("must commit")}, | |||
} | |||
// now, let's try to make some calls | |||
_, err := m.ABCIInfo() | |||
require.NotNil(err) | |||
assert.Equal("foobar", err.Error()) | |||
// query always returns the response | |||
query, err := m.ABCIQuery("/", nil, false) | |||
require.Nil(err) | |||
require.NotNil(query) | |||
assert.Equal(key, query.Response.GetKey()) | |||
assert.Equal(value, query.Response.GetValue()) | |||
assert.Equal(height, query.Response.GetHeight()) | |||
// non-commit calls always return errors | |||
_, err = m.BroadcastTxSync(goodTx) | |||
require.NotNil(err) | |||
assert.Equal("must commit", err.Error()) | |||
_, err = m.BroadcastTxAsync(goodTx) | |||
require.NotNil(err) | |||
assert.Equal("must commit", err.Error()) | |||
// commit depends on the input | |||
_, err = m.BroadcastTxCommit(badTx) | |||
require.NotNil(err) | |||
assert.Equal("bad tx", err.Error()) | |||
bres, err := m.BroadcastTxCommit(goodTx) | |||
require.Nil(err, "%+v", err) | |||
assert.EqualValues(0, bres.CheckTx.Code) | |||
assert.EqualValues("stand", bres.CheckTx.Data) | |||
assert.EqualValues("deliver", bres.DeliverTx.Data) | |||
} | |||
func TestABCIRecorder(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
m := mock.ABCIMock{ | |||
Info: mock.Call{Response: abci.ResponseInfo{ | |||
Data: "data", | |||
Version: "v0.9.9", | |||
}}, | |||
Query: mock.Call{Error: errors.New("query")}, | |||
Broadcast: mock.Call{Error: errors.New("broadcast")}, | |||
BroadcastCommit: mock.Call{Error: errors.New("broadcast_commit")}, | |||
} | |||
r := mock.NewABCIRecorder(m) | |||
require.Equal(0, len(r.Calls)) | |||
r.ABCIInfo() | |||
r.ABCIQuery("path", []byte("data"), true) | |||
require.Equal(2, len(r.Calls)) | |||
info := r.Calls[0] | |||
assert.Equal("abci_info", info.Name) | |||
assert.Nil(info.Error) | |||
assert.Nil(info.Args) | |||
require.NotNil(info.Response) | |||
ir, ok := info.Response.(*ctypes.ResultABCIInfo) | |||
require.True(ok) | |||
assert.Equal("data", ir.Response.Data) | |||
assert.Equal("v0.9.9", ir.Response.Version) | |||
query := r.Calls[1] | |||
assert.Equal("abci_query", query.Name) | |||
assert.Nil(query.Response) | |||
require.NotNil(query.Error) | |||
assert.Equal("query", query.Error.Error()) | |||
require.NotNil(query.Args) | |||
qa, ok := query.Args.(mock.QueryArgs) | |||
require.True(ok) | |||
assert.Equal("path", qa.Path) | |||
assert.EqualValues("data", qa.Data) | |||
assert.True(qa.Prove) | |||
// now add some broadcasts | |||
txs := []types.Tx{{1}, {2}, {3}} | |||
r.BroadcastTxCommit(txs[0]) | |||
r.BroadcastTxSync(txs[1]) | |||
r.BroadcastTxAsync(txs[2]) | |||
require.Equal(5, len(r.Calls)) | |||
bc := r.Calls[2] | |||
assert.Equal("broadcast_tx_commit", bc.Name) | |||
assert.Nil(bc.Response) | |||
require.NotNil(bc.Error) | |||
assert.EqualValues(bc.Args, txs[0]) | |||
bs := r.Calls[3] | |||
assert.Equal("broadcast_tx_sync", bs.Name) | |||
assert.Nil(bs.Response) | |||
require.NotNil(bs.Error) | |||
assert.EqualValues(bs.Args, txs[1]) | |||
ba := r.Calls[4] | |||
assert.Equal("broadcast_tx_async", ba.Name) | |||
assert.Nil(ba.Response) | |||
require.NotNil(ba.Error) | |||
assert.EqualValues(ba.Args, txs[2]) | |||
} | |||
func TestABCIApp(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
app := dummy.NewDummyApplication() | |||
m := mock.ABCIApp{app} | |||
// get some info | |||
info, err := m.ABCIInfo() | |||
require.Nil(err) | |||
assert.Equal(`{"size":0}`, info.Response.GetData()) | |||
// add a key | |||
key, value := "foo", "bar" | |||
tx := fmt.Sprintf("%s=%s", key, value) | |||
res, err := m.BroadcastTxCommit(types.Tx(tx)) | |||
require.Nil(err) | |||
assert.True(res.CheckTx.Code.IsOK()) | |||
require.NotNil(res.DeliverTx) | |||
assert.True(res.DeliverTx.Code.IsOK()) | |||
// check the key | |||
qres, err := m.ABCIQuery("/key", []byte(key), false) | |||
require.Nil(err) | |||
assert.EqualValues(value, qres.Response.Value) | |||
} |
@ -0,0 +1,128 @@ | |||
/* | |||
package mock returns a Client implementation that | |||
accepts various (mock) implementations of the various methods. | |||
This implementation is useful for using in tests, when you don't | |||
need a real server, but want a high-level of control about | |||
the server response you want to mock (eg. error handling), | |||
or if you just want to record the calls to verify in your tests. | |||
For real clients, you probably want the "http" package. If you | |||
want to directly call a tendermint node in process, you can use the | |||
"local" package. | |||
*/ | |||
package mock | |||
import ( | |||
"reflect" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
"github.com/tendermint/tendermint/rpc/core" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Client wraps arbitrary implementations of the various interfaces. | |||
// | |||
// We provide a few choices to mock out each one in this package. | |||
// Nothing hidden here, so no New function, just construct it from | |||
// some parts, and swap them out them during the tests. | |||
type Client struct { | |||
client.ABCIClient | |||
client.SignClient | |||
client.HistoryClient | |||
client.StatusClient | |||
// create a mock with types.NewEventSwitch() | |||
types.EventSwitch | |||
} | |||
func (c Client) _assertIsClient() client.Client { | |||
return c | |||
} | |||
// Call is used by recorders to save a call and response. | |||
// It can also be used to configure mock responses. | |||
// | |||
type Call struct { | |||
Name string | |||
Args interface{} | |||
Response interface{} | |||
Error error | |||
} | |||
// GetResponse will generate the apporiate response for us, when | |||
// using the Call struct to configure a Mock handler. | |||
// | |||
// When configuring a response, if only one of Response or Error is | |||
// set then that will always be returned. If both are set, then | |||
// we return Response if the Args match the set args, Error otherwise. | |||
func (c Call) GetResponse(args interface{}) (interface{}, error) { | |||
// handle the case with no response | |||
if c.Response == nil { | |||
if c.Error == nil { | |||
panic("Misconfigured call, you must set either Response or Error") | |||
} | |||
return nil, c.Error | |||
} | |||
// response without error | |||
if c.Error == nil { | |||
return c.Response, nil | |||
} | |||
// have both, we must check args.... | |||
if reflect.DeepEqual(args, c.Args) { | |||
return c.Response, nil | |||
} | |||
return nil, c.Error | |||
} | |||
func (c Client) Status() (*ctypes.ResultStatus, error) { | |||
return core.Status() | |||
} | |||
func (c Client) ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
return core.ABCIInfo() | |||
} | |||
func (c Client) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
return core.ABCIQuery(path, data, prove) | |||
} | |||
func (c Client) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { | |||
return core.BroadcastTxCommit(tx) | |||
} | |||
func (c Client) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return core.BroadcastTxAsync(tx) | |||
} | |||
func (c Client) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { | |||
return core.BroadcastTxSync(tx) | |||
} | |||
func (c Client) NetInfo() (*ctypes.ResultNetInfo, error) { | |||
return core.NetInfo() | |||
} | |||
func (c Client) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { | |||
return core.UnsafeDialSeeds(seeds) | |||
} | |||
func (c Client) BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) { | |||
return core.BlockchainInfo(minHeight, maxHeight) | |||
} | |||
func (c Client) Genesis() (*ctypes.ResultGenesis, error) { | |||
return core.Genesis() | |||
} | |||
func (c Client) Block(height int) (*ctypes.ResultBlock, error) { | |||
return core.Block(height) | |||
} | |||
func (c Client) Commit(height int) (*ctypes.ResultCommit, error) { | |||
return core.Commit(height) | |||
} | |||
func (c Client) Validators() (*ctypes.ResultValidators, error) { | |||
return core.Validators() | |||
} |
@ -0,0 +1,55 @@ | |||
package mock | |||
import ( | |||
"github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
// StatusMock returns the result specified by the Call | |||
type StatusMock struct { | |||
Call | |||
} | |||
func (m *StatusMock) _assertStatusClient() client.StatusClient { | |||
return m | |||
} | |||
func (m *StatusMock) Status() (*ctypes.ResultStatus, error) { | |||
res, err := m.GetResponse(nil) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return res.(*ctypes.ResultStatus), nil | |||
} | |||
// StatusRecorder can wrap another type (StatusMock, full client) | |||
// and record the status calls | |||
type StatusRecorder struct { | |||
Client client.StatusClient | |||
Calls []Call | |||
} | |||
func NewStatusRecorder(client client.StatusClient) *StatusRecorder { | |||
return &StatusRecorder{ | |||
Client: client, | |||
Calls: []Call{}, | |||
} | |||
} | |||
func (r *StatusRecorder) _assertStatusClient() client.StatusClient { | |||
return r | |||
} | |||
func (r *StatusRecorder) addCall(call Call) { | |||
r.Calls = append(r.Calls, call) | |||
} | |||
func (r *StatusRecorder) Status() (*ctypes.ResultStatus, error) { | |||
res, err := r.Client.Status() | |||
r.addCall(Call{ | |||
Name: "status", | |||
Response: res, | |||
Error: err, | |||
}) | |||
return res, err | |||
} |
@ -0,0 +1,45 @@ | |||
package mock_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
"github.com/tendermint/tendermint/rpc/client/mock" | |||
) | |||
func TestStatus(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
m := &mock.StatusMock{ | |||
Call: mock.Call{ | |||
Response: &ctypes.ResultStatus{ | |||
LatestBlockHash: []byte("block"), | |||
LatestAppHash: []byte("app"), | |||
LatestBlockHeight: 10, | |||
}}, | |||
} | |||
r := mock.NewStatusRecorder(m) | |||
require.Equal(0, len(r.Calls)) | |||
// make sure response works proper | |||
status, err := r.Status() | |||
require.Nil(err, "%+v", err) | |||
assert.EqualValues("block", status.LatestBlockHash) | |||
assert.EqualValues(10, status.LatestBlockHeight) | |||
// make sure recorder works properly | |||
require.Equal(1, len(r.Calls)) | |||
rs := r.Calls[0] | |||
assert.Equal("status", rs.Name) | |||
assert.Nil(rs.Args) | |||
assert.Nil(rs.Error) | |||
require.NotNil(rs.Response) | |||
st, ok := rs.Response.(*ctypes.ResultStatus) | |||
require.True(ok) | |||
assert.EqualValues("block", st.LatestBlockHash) | |||
assert.EqualValues(10, st.LatestBlockHeight) | |||
} |
@ -0,0 +1,179 @@ | |||
package client_test | |||
import ( | |||
"strings" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
merkle "github.com/tendermint/go-merkle" | |||
merktest "github.com/tendermint/merkleeyes/testutil" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||
) | |||
func getHTTPClient() *client.HTTP { | |||
rpcAddr := rpctest.GetConfig().GetString("rpc_laddr") | |||
return client.NewHTTP(rpcAddr, "/websocket") | |||
} | |||
func getLocalClient() client.Local { | |||
return client.NewLocal(node) | |||
} | |||
// GetClients returns a slice of clients for table-driven tests | |||
func GetClients() []client.Client { | |||
return []client.Client{ | |||
getHTTPClient(), | |||
getLocalClient(), | |||
} | |||
} | |||
// Make sure status is correct (we connect properly) | |||
func TestStatus(t *testing.T) { | |||
for i, c := range GetClients() { | |||
chainID := rpctest.GetConfig().GetString("chain_id") | |||
status, err := c.Status() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
assert.Equal(t, chainID, status.NodeInfo.Network) | |||
} | |||
} | |||
// Make sure info is correct (we connect properly) | |||
func TestInfo(t *testing.T) { | |||
for i, c := range GetClients() { | |||
// status, err := c.Status() | |||
// require.Nil(t, err, "%+v", err) | |||
info, err := c.ABCIInfo() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
// TODO: this is not correct - fix merkleeyes! | |||
// assert.EqualValues(t, status.LatestBlockHeight, info.Response.LastBlockHeight) | |||
assert.True(t, strings.HasPrefix(info.Response.Data, "size")) | |||
} | |||
} | |||
func TestNetInfo(t *testing.T) { | |||
for i, c := range GetClients() { | |||
nc, ok := c.(client.NetworkClient) | |||
require.True(t, ok, "%d", i) | |||
netinfo, err := nc.NetInfo() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
assert.True(t, netinfo.Listening) | |||
assert.Equal(t, 0, len(netinfo.Peers)) | |||
} | |||
} | |||
func TestDumpConsensusState(t *testing.T) { | |||
for i, c := range GetClients() { | |||
// FIXME: fix server so it doesn't panic on invalid input | |||
nc, ok := c.(client.NetworkClient) | |||
require.True(t, ok, "%d", i) | |||
cons, err := nc.DumpConsensusState() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
assert.NotEmpty(t, cons.RoundState) | |||
assert.Empty(t, cons.PeerRoundStates) | |||
} | |||
} | |||
func TestGenesisAndValidators(t *testing.T) { | |||
for i, c := range GetClients() { | |||
chainID := rpctest.GetConfig().GetString("chain_id") | |||
// make sure this is the right genesis file | |||
gen, err := c.Genesis() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
assert.Equal(t, chainID, gen.Genesis.ChainID) | |||
// get the genesis validator | |||
require.Equal(t, 1, len(gen.Genesis.Validators)) | |||
gval := gen.Genesis.Validators[0] | |||
// get the current validators | |||
vals, err := c.Validators() | |||
require.Nil(t, err, "%d: %+v", i, err) | |||
require.Equal(t, 1, len(vals.Validators)) | |||
val := vals.Validators[0] | |||
// make sure the current set is also the genesis set | |||
assert.Equal(t, gval.Amount, val.VotingPower) | |||
assert.Equal(t, gval.PubKey, val.PubKey) | |||
} | |||
} | |||
// Make some app checks | |||
func TestAppCalls(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
for i, c := range GetClients() { | |||
// get an offset of height to avoid racing and guessing | |||
s, err := c.Status() | |||
require.Nil(err, "%d: %+v", i, err) | |||
// sh is start height or status height | |||
sh := s.LatestBlockHeight | |||
// look for the future | |||
_, err = c.Block(sh + 2) | |||
assert.NotNil(err) // no block yet | |||
// write something | |||
k, v, tx := merktest.MakeTxKV() | |||
_, err = c.BroadcastTxCommit(tx) | |||
require.Nil(err, "%d: %+v", i, err) | |||
// wait before querying | |||
time.Sleep(time.Second * 1) | |||
qres, err := c.ABCIQuery("/key", k, false) | |||
if assert.Nil(err) && assert.True(qres.Response.Code.IsOK()) { | |||
data := qres.Response | |||
// assert.Equal(k, data.GetKey()) // only returned for proofs | |||
assert.Equal(v, data.GetValue()) | |||
} | |||
// +/- 1 making my head hurt | |||
h := int(qres.Response.Height) - 1 | |||
// and we can even check the block is added | |||
block, err := c.Block(h) | |||
require.Nil(err, "%d: %+v", i, err) | |||
appHash := block.BlockMeta.Header.AppHash | |||
assert.True(len(appHash) > 0) | |||
assert.EqualValues(h, block.BlockMeta.Header.Height) | |||
// check blockchain info, now that we know there is info | |||
// TODO: is this commented somewhere that they are returned | |||
// in order of descending height??? | |||
info, err := c.BlockchainInfo(h-2, h) | |||
require.Nil(err, "%d: %+v", i, err) | |||
assert.True(info.LastHeight > 2) | |||
if assert.Equal(3, len(info.BlockMetas)) { | |||
lastMeta := info.BlockMetas[0] | |||
assert.EqualValues(h, lastMeta.Header.Height) | |||
bMeta := block.BlockMeta | |||
assert.Equal(bMeta.Header.AppHash, lastMeta.Header.AppHash) | |||
assert.Equal(bMeta.BlockID, lastMeta.BlockID) | |||
} | |||
// and get the corresponding commit with the same apphash | |||
commit, err := c.Commit(h) | |||
require.Nil(err, "%d: %+v", i, err) | |||
cappHash := commit.Header.AppHash | |||
assert.Equal(appHash, cappHash) | |||
assert.NotNil(commit.Commit) | |||
// compare the commits (note Commit(2) has commit from Block(3)) | |||
commit2, err := c.Commit(h - 1) | |||
require.Nil(err, "%d: %+v", i, err) | |||
assert.Equal(block.Block.LastCommit, commit2.Commit) | |||
// and we got a proof that works! | |||
pres, err := c.ABCIQuery("/key", k, true) | |||
if assert.Nil(err) && assert.True(pres.Response.Code.IsOK()) { | |||
proof, err := merkle.ReadProof(pres.Response.GetProof()) | |||
if assert.Nil(err) { | |||
key := pres.Response.GetKey() | |||
value := pres.Response.GetValue() | |||
assert.Equal(appHash, proof.RootHash) | |||
valid := proof.Verify(key, value, appHash) | |||
assert.True(valid) | |||
} | |||
} | |||
} | |||
} |
@ -1,25 +1,29 @@ | |||
package core | |||
import ( | |||
abci "github.com/tendermint/abci/types" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
//----------------------------------------------------------------------------- | |||
func ABCIQuery(query []byte) (*ctypes.ResultABCIQuery, error) { | |||
res := proxyAppQuery.QuerySync(query) | |||
return &ctypes.ResultABCIQuery{res}, nil | |||
func ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { | |||
resQuery, err := proxyAppQuery.QuerySync(abci.RequestQuery{ | |||
Path: path, | |||
Data: data, | |||
Prove: prove, | |||
}) | |||
if err != nil { | |||
return nil, err | |||
} | |||
log.Info("ABCIQuery", "path", path, "data", data, "result", resQuery) | |||
return &ctypes.ResultABCIQuery{resQuery}, nil | |||
} | |||
func ABCIInfo() (*ctypes.ResultABCIInfo, error) { | |||
res, err := proxyAppQuery.InfoSync() | |||
resInfo, err := proxyAppQuery.InfoSync() | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &ctypes.ResultABCIInfo{ | |||
Data: res.Data, | |||
Version: res.Version, | |||
LastBlockHeight: res.LastBlockHeight, | |||
LastBlockAppHash: res.LastBlockAppHash, | |||
}, nil | |||
return &ctypes.ResultABCIInfo{resInfo}, nil | |||
} |
@ -0,0 +1,36 @@ | |||
/* | |||
package tests contain integration tests and helper functions for testing | |||
the RPC interface | |||
In particular, it allows us to spin up a tendermint node in process, with | |||
a live RPC server, which we can use to verify our rpc calls. It provides | |||
all data structures, enabling us to do more complex tests (like node_test.go) | |||
that introspect the blocks themselves to validate signatures and the like. | |||
It currently only spins up one node, it would be interesting to expand it | |||
to multiple nodes to see the real effects of validating partially signed | |||
blocks. | |||
*/ | |||
package rpctest | |||
import ( | |||
"os" | |||
"testing" | |||
"github.com/tendermint/abci/example/dummy" | |||
nm "github.com/tendermint/tendermint/node" | |||
) | |||
var node *nm.Node | |||
func TestMain(m *testing.M) { | |||
// start a tendermint node (and merkleeyes) in the background to test against | |||
app := dummy.NewDummyApplication() | |||
node = StartTendermint(app) | |||
code := m.Run() | |||
// and shut down proper at the end | |||
node.Stop() | |||
node.Wait() | |||
os.Exit(code) | |||
} |
@ -0,0 +1,51 @@ | |||
#!/usr/bin/env bash | |||
set -e | |||
# 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) | |||
fi | |||
if [ -z "$VERSION" ]; then | |||
echo "Please specify a version." | |||
exit 1 | |||
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" | |||
# Generate the tag. | |||
if [ -z "$NOTAG" ]; then | |||
echo "==> Tagging..." | |||
git commit --allow-empty -a -m "Release v$VERSION" | |||
git tag -a -m "Version $VERSION" "v${VERSION}" master | |||
fi | |||
# 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 | |||
# Add "tendermint" and $VERSION prefix to package name. | |||
rm -rf ./build/dist | |||
mkdir -p ./build/dist | |||
for FILENAME in $(find ./build/pkg -mindepth 1 -maxdepth 1 -type f); do | |||
FILENAME=$(basename "$FILENAME") | |||
cp "./build/pkg/${FILENAME}" "./build/dist/tendermint_${VERSION}_${FILENAME}" | |||
done | |||
# Make the checksums. | |||
pushd ./build/dist | |||
shasum -a256 ./* > "./tendermint_${VERSION}_SHA256SUMS" | |||
popd | |||
# Done | |||
echo | |||
echo "==> Results:" | |||
ls -hl ./build/dist | |||
exit 0 |
@ -0,0 +1,54 @@ | |||
#!/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_DESCRIBE="$(git describe --tags --always)" | |||
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"} | |||
# Delete the old dir | |||
echo "==> Removing old directory..." | |||
rm -rf build/pkg | |||
mkdir -p build/pkg | |||
# Make sure build tools are available. | |||
make tools | |||
# Get VENDORED dependencies | |||
make get_vendor_deps | |||
# Build! | |||
echo "==> Building..." | |||
"$(which gox)" \ | |||
-os="${XC_OS}" \ | |||
-arch="${XC_ARCH}" \ | |||
-osarch="!darwin/arm !solaris/amd64 !freebsd/amd64" \ | |||
-ldflags "-X ${GIT_IMPORT}.GitCommit='${GIT_COMMIT}' -X ${GIT_IMPORT}.GitDescribe='${GIT_DESCRIBE}'" \ | |||
-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 |
@ -0,0 +1,12 @@ | |||
FROM golang:1.7.4 | |||
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 |
@ -1,210 +0,0 @@ | |||
package state | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"path" | |||
"testing" | |||
"github.com/tendermint/tendermint/config/tendermint_test" | |||
// . "github.com/tendermint/go-common" | |||
cfg "github.com/tendermint/go-config" | |||
"github.com/tendermint/go-crypto" | |||
dbm "github.com/tendermint/go-db" | |||
"github.com/tendermint/tendermint/proxy" | |||
"github.com/tendermint/tendermint/types" | |||
"github.com/tendermint/abci/example/dummy" | |||
) | |||
var ( | |||
privKey = crypto.GenPrivKeyEd25519FromSecret([]byte("handshake_test")) | |||
chainID = "handshake_chain" | |||
nBlocks = 5 | |||
mempool = MockMempool{} | |||
testPartSize = 65536 | |||
) | |||
//--------------------------------------- | |||
// Test block execution | |||
func TestExecBlock(t *testing.T) { | |||
// TODO | |||
} | |||
//--------------------------------------- | |||
// Test handshake/replay | |||
// Sync from scratch | |||
func TestHandshakeReplayAll(t *testing.T) { | |||
testHandshakeReplay(t, 0) | |||
} | |||
// Sync many, not from scratch | |||
func TestHandshakeReplaySome(t *testing.T) { | |||
testHandshakeReplay(t, 1) | |||
} | |||
// Sync from lagging by one | |||
func TestHandshakeReplayOne(t *testing.T) { | |||
testHandshakeReplay(t, nBlocks-1) | |||
} | |||
// Sync from caught up | |||
func TestHandshakeReplayNone(t *testing.T) { | |||
testHandshakeReplay(t, nBlocks) | |||
} | |||
// Make some blocks. Start a fresh app and apply n blocks. Then restart the app and sync it up with the remaining blocks | |||
func testHandshakeReplay(t *testing.T, n int) { | |||
config := tendermint_test.ResetConfig("proxy_test_") | |||
state, store := stateAndStore(config) | |||
clientCreator := proxy.NewLocalClientCreator(dummy.NewPersistentDummyApplication(path.Join(config.GetString("db_dir"), "1"))) | |||
clientCreator2 := proxy.NewLocalClientCreator(dummy.NewPersistentDummyApplication(path.Join(config.GetString("db_dir"), "2"))) | |||
proxyApp := proxy.NewAppConns(config, clientCreator, NewHandshaker(config, state, store)) | |||
if _, err := proxyApp.Start(); err != nil { | |||
t.Fatalf("Error starting proxy app connections: %v", err) | |||
} | |||
chain := makeBlockchain(t, proxyApp, state) | |||
store.chain = chain // | |||
latestAppHash := state.AppHash | |||
proxyApp.Stop() | |||
if n > 0 { | |||
// start a new app without handshake, play n blocks | |||
proxyApp = proxy.NewAppConns(config, clientCreator2, nil) | |||
if _, err := proxyApp.Start(); err != nil { | |||
t.Fatalf("Error starting proxy app connections: %v", err) | |||
} | |||
state2, _ := stateAndStore(config) | |||
for i := 0; i < n; i++ { | |||
block := chain[i] | |||
err := state2.ApplyBlock(nil, proxyApp.Consensus(), block, block.MakePartSet(testPartSize).Header(), mempool) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
} | |||
proxyApp.Stop() | |||
} | |||
// now start it with the handshake | |||
handshaker := NewHandshaker(config, state, store) | |||
proxyApp = proxy.NewAppConns(config, clientCreator2, handshaker) | |||
if _, err := proxyApp.Start(); err != nil { | |||
t.Fatalf("Error starting proxy app connections: %v", err) | |||
} | |||
// get the latest app hash from the app | |||
res, err := proxyApp.Query().InfoSync() | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
// the app hash should be synced up | |||
if !bytes.Equal(latestAppHash, res.LastBlockAppHash) { | |||
t.Fatalf("Expected app hashes to match after handshake/replay. got %X, expected %X", res.LastBlockAppHash, latestAppHash) | |||
} | |||
if handshaker.nBlocks != nBlocks-n { | |||
t.Fatalf("Expected handshake to sync %d blocks, got %d", nBlocks-n, handshaker.nBlocks) | |||
} | |||
} | |||
//-------------------------- | |||
// utils for making blocks | |||
// make some bogus txs | |||
func txsFunc(blockNum int) (txs []types.Tx) { | |||
for i := 0; i < 10; i++ { | |||
txs = append(txs, types.Tx([]byte{byte(blockNum), byte(i)})) | |||
} | |||
return txs | |||
} | |||
// sign a commit vote | |||
func signCommit(height, round int, hash []byte, header types.PartSetHeader) *types.Vote { | |||
vote := &types.Vote{ | |||
ValidatorIndex: 0, | |||
ValidatorAddress: privKey.PubKey().Address(), | |||
Height: height, | |||
Round: round, | |||
Type: types.VoteTypePrecommit, | |||
BlockID: types.BlockID{hash, header}, | |||
} | |||
sig := privKey.Sign(types.SignBytes(chainID, vote)) | |||
vote.Signature = sig | |||
return vote | |||
} | |||
// make a blockchain with one validator | |||
func makeBlockchain(t *testing.T, proxyApp proxy.AppConns, state *State) (blockchain []*types.Block) { | |||
prevHash := state.LastBlockID.Hash | |||
lastCommit := new(types.Commit) | |||
prevParts := types.PartSetHeader{} | |||
valHash := state.Validators.Hash() | |||
prevBlockID := types.BlockID{prevHash, prevParts} | |||
for i := 1; i < nBlocks+1; i++ { | |||
block, parts := types.MakeBlock(i, chainID, txsFunc(i), lastCommit, | |||
prevBlockID, valHash, state.AppHash, testPartSize) | |||
fmt.Println(i) | |||
fmt.Println(prevBlockID) | |||
fmt.Println(block.LastBlockID) | |||
err := state.ApplyBlock(nil, proxyApp.Consensus(), block, block.MakePartSet(testPartSize).Header(), mempool) | |||
if err != nil { | |||
t.Fatal(i, err) | |||
} | |||
voteSet := types.NewVoteSet(chainID, i, 0, types.VoteTypePrecommit, state.Validators) | |||
vote := signCommit(i, 0, block.Hash(), parts.Header()) | |||
_, err = voteSet.AddVote(vote) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
blockchain = append(blockchain, block) | |||
prevHash = block.Hash() | |||
prevParts = parts.Header() | |||
lastCommit = voteSet.MakeCommit() | |||
prevBlockID = types.BlockID{prevHash, prevParts} | |||
} | |||
return blockchain | |||
} | |||
// fresh state and mock store | |||
func stateAndStore(config cfg.Config) (*State, *mockBlockStore) { | |||
stateDB := dbm.NewMemDB() | |||
return MakeGenesisState(stateDB, &types.GenesisDoc{ | |||
ChainID: chainID, | |||
Validators: []types.GenesisValidator{ | |||
types.GenesisValidator{privKey.PubKey(), 10000, "test"}, | |||
}, | |||
AppHash: nil, | |||
}), NewMockBlockStore(config, nil) | |||
} | |||
//---------------------------------- | |||
// mock block store | |||
type mockBlockStore struct { | |||
config cfg.Config | |||
chain []*types.Block | |||
} | |||
func NewMockBlockStore(config cfg.Config, chain []*types.Block) *mockBlockStore { | |||
return &mockBlockStore{config, chain} | |||
} | |||
func (bs *mockBlockStore) Height() int { return len(bs.chain) } | |||
func (bs *mockBlockStore) LoadBlock(height int) *types.Block { return bs.chain[height-1] } | |||
func (bs *mockBlockStore) LoadBlockMeta(height int) *types.BlockMeta { | |||
block := bs.chain[height-1] | |||
return &types.BlockMeta{ | |||
Hash: block.Hash(), | |||
Header: block.Header, | |||
PartsHeader: block.MakePartSet(bs.config.GetInt("block_part_size")).Header(), | |||
} | |||
} |
@ -0,0 +1,54 @@ | |||
# Tendermint P2P Tests | |||
These scripts facilitate setting up and testing a local testnet using docker containers. | |||
Setup your own local testnet as follows. | |||
For consistency, we assume all commands are run from the Tendermint repository root (ie. $GOPATH/src/github.com/tendermint/tendermint). | |||
First, build the docker image: | |||
``` | |||
docker build -t tendermint_tester -f ./test/docker/Dockerfile . | |||
``` | |||
Now create the docker network: | |||
``` | |||
docker network create --driver bridge --subnet 172.57.0.0/16 my_testnet | |||
``` | |||
This gives us a new network with IP addresses in the rage `172.57.0.0 - 172.57.255.255`. | |||
Peers on the network can have any IP address in this range. | |||
For our four node network, let's pick `172.57.0.101 - 172.57.0.104`. | |||
Since we use Tendermint's default listening port of 46656, our list of seed nodes will look like: | |||
``` | |||
172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 | |||
``` | |||
Now we can start up the peers. We already have config files setup in `test/p2p/data/`. | |||
Let's use a for-loop to start our peers: | |||
``` | |||
for i in $(seq 1 4); do | |||
docker run -d \ | |||
--net=my_testnet\ | |||
--ip="172.57.0.$((100 + $i))" \ | |||
--name local_testnet_$i \ | |||
--entrypoint tendermint \ | |||
-e TMROOT=/go/src/github.com/tendermint/tendermint/test/p2p/data/mach$i/core \ | |||
tendermint_tester node --seeds 172.57.0.101:46656,172.57.0.102:46656,172.57.0.103:46656,172.57.0.104:46656 --proxy_app=dummy | |||
done | |||
``` | |||
If you now run `docker ps`, you'll see your containers! | |||
We can confirm they are making blocks by checking the `/status` message using `curl` and `jq` to pretty print the output json: | |||
``` | |||
curl 172.57.0.101:46657/status | jq . | |||
``` | |||
@ -0,0 +1,17 @@ | |||
#! /bin/bash | |||
set -u | |||
ID=$1 | |||
N=$2 | |||
addr=$(test/p2p/ip.sh "$ID"):46657 | |||
echo "2. wait until peer $ID connects to other nodes using pex reactor" | |||
peers_count="0" | |||
while [[ "$peers_count" -lt "$((N-1))" ]]; do | |||
sleep 1 | |||
peers_count=$(curl -s "$addr/net_info" | jq ".result[1].peers | length") | |||
echo "... peers count = $peers_count, expected = $((N-1))" | |||
done | |||
echo "... successful" |
@ -0,0 +1,31 @@ | |||
#! /bin/bash | |||
set -u | |||
N=$1 | |||
cd $GOPATH/src/github.com/tendermint/tendermint | |||
echo "Waiting for nodes to come online" | |||
for i in `seq 1 $N`; do | |||
addr=$(test/p2p/ip.sh $i):46657 | |||
curl -s $addr/status > /dev/null | |||
ERR=$? | |||
while [ "$ERR" != 0 ]; do | |||
sleep 1 | |||
curl -s $addr/status > /dev/null | |||
ERR=$? | |||
done | |||
echo "... node $i is up" | |||
done | |||
set -e | |||
# seeds need quotes | |||
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 | |||
echo $seeds | |||
IP=$(test/p2p/ip.sh 1) | |||
curl --data-urlencode "seeds=[$seeds]" "$IP:46657/dial_seeds" |
@ -0,0 +1,15 @@ | |||
#! /bin/bash | |||
set -eu | |||
DOCKER_IMAGE=$1 | |||
NETWORK_NAME=$2 | |||
N=$3 | |||
PROXY_APP=$4 | |||
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 | |||
echo "Test connecting via /dial_seeds" | |||
bash test/p2p/pex/test_dial_seeds.sh $DOCKER_IMAGE $NETWORK_NAME $N $PROXY_APP |
@ -0,0 +1,57 @@ | |||
#! /bin/bash | |||
set -eu | |||
DOCKER_IMAGE=$1 | |||
NETWORK_NAME=$2 | |||
N=$3 | |||
PROXY_APP=$4 | |||
ID=1 | |||
echo "----------------------------------------------------------------------" | |||
echo "Testing pex creates the addrbook and uses it if seeds are not provided" | |||
echo "(assuming peers are started with pex enabled)" | |||
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" | |||
set +e #CIRCLE | |||
docker rm -vf "local_testnet_$ID" | |||
set -e | |||
# NOTE that we do not provide seeds | |||
bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$ID" "$PROXY_APP" "--pex" | |||
docker cp "/tmp/addrbook.json" "local_testnet_$ID:/go/src/github.com/tendermint/tendermint/test/p2p/data/mach1/core/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" | |||
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 "(assuming peers are started with pex enabled)" | |||
CLIENT_NAME="pex_no_addrbook_$ID" | |||
echo "1. restart peer $ID" | |||
docker stop "local_testnet_$ID" | |||
set +e #CIRCLE | |||
docker rm -vf "local_testnet_$ID" | |||
set -e | |||
# NOTE that we do not provide seeds | |||
bash test/p2p/peer.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$ID" "$PROXY_APP" "--pex" | |||
# if the client runs forever, it means other peers have removed us from their books (which should not happen) | |||
bash test/p2p/client.sh "$DOCKER_IMAGE" "$NETWORK_NAME" "$CLIENT_NAME" "test/p2p/pex/check_peer.sh $ID $N" | |||
echo "" | |||
echo "PASS" | |||
echo "" |
@ -0,0 +1,36 @@ | |||
#! /bin/bash | |||
set -eu | |||
DOCKER_IMAGE=$1 | |||
NETWORK_NAME=$2 | |||
N=$3 | |||
PROXY_APP=$4 | |||
ID=1 | |||
cd $GOPATH/src/github.com/tendermint/tendermint | |||
echo "----------------------------------------------------------------------" | |||
echo "Testing full network connection using one /dial_seeds call" | |||
echo "(assuming peers are started with pex enabled)" | |||
# stop the existing testnet and remove local network | |||
set +e | |||
bash test/p2p/local_testnet_stop.sh $NETWORK_NAME $N | |||
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 | |||
# 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" | |||
# test basic connectivity and consensus | |||
# start client container and check the num peers and height for all nodes | |||
CLIENT_NAME="dial_seeds_basic" | |||
bash test/p2p/client.sh $DOCKER_IMAGE $NETWORK_NAME $CLIENT_NAME "test/p2p/basic/test.sh $N" |
@ -0,0 +1,12 @@ | |||
#! /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" |
@ -1,5 +1,5 @@ | |||
#! /bin/bash | |||
cd $GOPATH/src/github.com/tendermint/tendermint | |||
cd "$GOPATH/src/github.com/tendermint/tendermint" | |||
bash ./test/persist/test_failure_indices.sh |