Merge abcipull/1813/head
@ -0,0 +1,23 @@ | |||
FROM golang:latest | |||
RUN mkdir -p /go/src/github.com/tendermint/abci | |||
WORKDIR /go/src/github.com/tendermint/abci | |||
COPY Makefile /go/src/github.com/tendermint/abci/ | |||
# see make protoc for details on ldconfig | |||
RUN make get_protoc && ldconfig | |||
# killall is used in tests | |||
RUN apt-get update && apt-get install -y \ | |||
psmisc \ | |||
&& rm -rf /var/lib/apt/lists/* | |||
COPY Gopkg.toml /go/src/github.com/tendermint/abci/ | |||
COPY Gopkg.lock /go/src/github.com/tendermint/abci/ | |||
RUN make get_tools | |||
# see https://github.com/golang/dep/issues/1312 | |||
RUN dep ensure -vendor-only | |||
COPY . /go/src/github.com/tendermint/abci |
@ -0,0 +1,174 @@ | |||
GOTOOLS = \ | |||
github.com/mitchellh/gox \ | |||
github.com/golang/dep/cmd/dep \ | |||
gopkg.in/alecthomas/gometalinter.v2 \ | |||
github.com/gogo/protobuf/protoc-gen-gogo \ | |||
github.com/gogo/protobuf/gogoproto | |||
GOTOOLS_CHECK = gox dep gometalinter.v2 protoc protoc-gen-gogo | |||
PACKAGES=$(shell go list ./... | grep -v '/vendor/') | |||
INCLUDE = -I=. -I=${GOPATH}/src -I=${GOPATH}/src/github.com/gogo/protobuf/protobuf | |||
all: check get_vendor_deps protoc build test install metalinter | |||
check: check_tools | |||
######################################## | |||
### Build | |||
protoc: | |||
## If you get the following error, | |||
## "error while loading shared libraries: libprotobuf.so.14: cannot open shared object file: No such file or directory" | |||
## See https://stackoverflow.com/a/25518702 | |||
protoc $(INCLUDE) --gogo_out=plugins=grpc:. types/*.proto | |||
@echo "--> adding nolint declarations to protobuf generated files" | |||
@awk '/package types/ { print "//nolint: gas"; print; next }1' types/types.pb.go > types/types.pb.go.new | |||
@mv types/types.pb.go.new types/types.pb.go | |||
build: | |||
@go build -i ./cmd/... | |||
dist: | |||
@bash scripts/dist.sh | |||
@bash scripts/publish.sh | |||
install: | |||
@go install ./cmd/... | |||
######################################## | |||
### Tools & dependencies | |||
check_tools: | |||
@# https://stackoverflow.com/a/25668869 | |||
@echo "Found tools: $(foreach tool,$(GOTOOLS_CHECK),\ | |||
$(if $(shell which $(tool)),$(tool),$(error "No $(tool) in PATH")))" | |||
get_tools: | |||
@echo "--> Installing tools" | |||
go get -u -v $(GOTOOLS) | |||
@gometalinter.v2 --install | |||
get_protoc: | |||
@# https://github.com/google/protobuf/releases | |||
curl -L https://github.com/google/protobuf/releases/download/v3.4.1/protobuf-cpp-3.4.1.tar.gz | tar xvz && \ | |||
cd protobuf-3.4.1 && \ | |||
DIST_LANG=cpp ./configure && \ | |||
make && \ | |||
make install && \ | |||
cd .. && \ | |||
rm -rf protobuf-3.4.1 | |||
update_tools: | |||
@echo "--> Updating tools" | |||
@go get -u $(GOTOOLS) | |||
get_vendor_deps: | |||
@rm -rf vendor/ | |||
@echo "--> Running dep ensure" | |||
@dep ensure | |||
######################################## | |||
### Testing | |||
test: | |||
@find . -path ./vendor -prune -o -name "*.sock" -exec rm {} \; | |||
@echo "==> Running go test" | |||
@go test $(PACKAGES) | |||
test_race: | |||
@find . -path ./vendor -prune -o -name "*.sock" -exec rm {} \; | |||
@echo "==> Running go test --race" | |||
@go test -v -race $(PACKAGES) | |||
### three tests tested by Jenkins | |||
test_cover: | |||
@ bash tests/test_cover.sh | |||
test_apps: | |||
# test the counter using a go test script | |||
@ bash tests/test_app/test.sh | |||
test_cli: | |||
# test the cli against the examples in the tutorial at: | |||
# http://tendermint.readthedocs.io/projects/tools/en/master/abci-cli.html | |||
# | |||
# XXX: if this test fails, fix it and update the docs at: | |||
# https://github.com/tendermint/tendermint/blob/develop/docs/abci-cli.rst | |||
@ bash tests/test_cli/test.sh | |||
######################################## | |||
### Formatting, linting, and vetting | |||
fmt: | |||
@go fmt ./... | |||
metalinter: | |||
@echo "==> Running linter" | |||
gometalinter.v2 --vendor --deadline=600s --disable-all \ | |||
--enable=maligned \ | |||
--enable=deadcode \ | |||
--enable=goconst \ | |||
--enable=goimports \ | |||
--enable=gosimple \ | |||
--enable=ineffassign \ | |||
--enable=megacheck \ | |||
--enable=misspell \ | |||
--enable=staticcheck \ | |||
--enable=safesql \ | |||
--enable=structcheck \ | |||
--enable=unconvert \ | |||
--enable=unused \ | |||
--enable=varcheck \ | |||
--enable=vetshadow \ | |||
./... | |||
#--enable=gas \ | |||
#--enable=dupl \ | |||
#--enable=errcheck \ | |||
#--enable=gocyclo \ | |||
#--enable=golint \ <== comments on anything exported | |||
#--enable=gotype \ | |||
#--enable=interfacer \ | |||
#--enable=unparam \ | |||
#--enable=vet \ | |||
metalinter_all: | |||
protoc $(INCLUDE) --lint_out=. types/*.proto | |||
gometalinter.v2 --vendor --deadline=600s --enable-all --disable=lll ./... | |||
######################################## | |||
### Docker | |||
DEVDOC_SAVE = docker commit `docker ps -a -n 1 -q` devdoc:local | |||
docker_build: | |||
docker build -t "tendermint/abci-dev" -f Dockerfile.develop . | |||
docker_run: | |||
docker run -it -v "$(CURDIR):/go/src/github.com/tendermint/abci" -w "/go/src/github.com/tendermint/abci" "tendermint/abci-dev" /bin/bash | |||
docker_run_rm: | |||
docker run -it --rm -v "$(CURDIR):/go/src/github.com/tendermint/abci" -w "/go/src/github.com/tendermint/abci" "tendermint/abci-dev" /bin/bash | |||
devdoc_init: | |||
docker run -it -v "$(CURDIR):/go/src/github.com/tendermint/abci" -w "/go/src/github.com/tendermint/abci" tendermint/devdoc echo | |||
# TODO make this safer | |||
$(call DEVDOC_SAVE) | |||
devdoc: | |||
docker run -it -v "$(CURDIR):/go/src/github.com/tendermint/abci" -w "/go/src/github.com/tendermint/abci" devdoc:local bash | |||
devdoc_save: | |||
# TODO make this safer | |||
$(call DEVDOC_SAVE) | |||
devdoc_clean: | |||
docker rmi $$(docker images -f "dangling=true" -q) | |||
# To avoid unintended conflicts with file names, always add to .PHONY | |||
# unless there is a reason not to. | |||
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html | |||
.PHONY: check protoc build dist install check_tools get_tools get_protoc update_tools get_vendor_deps test test_race fmt metalinter metalinter_all docker_build docker_run docker_run_rm devdoc_init devdoc devdoc_save devdoc_clean |
@ -0,0 +1,168 @@ | |||
# Application BlockChain Interface (ABCI) | |||
[![CircleCI](https://circleci.com/gh/tendermint/abci.svg?style=svg)](https://circleci.com/gh/tendermint/abci) | |||
Blockchains are systems for multi-master state machine replication. | |||
**ABCI** is an interface that defines the boundary between the replication engine (the blockchain), | |||
and the state machine (the application). | |||
Using a socket protocol, a consensus engine running in one process | |||
can manage an application state running in another. | |||
Previously, the ABCI was referred to as TMSP. | |||
The community has provided a number of addtional implementations, see the [Tendermint Ecosystem](https://tendermint.com/ecosystem) | |||
## Specification | |||
A detailed description of the ABCI methods and message types is contained in: | |||
- [A prose specification](specification.md) | |||
- [A protobuf file](https://github.com/tendermint/abci/blob/master/types/types.proto) | |||
- [A Go interface](https://github.com/tendermint/abci/blob/master/types/application.go). | |||
For more background information on ABCI, motivations, and tendermint, please visit [the documentation](http://tendermint.readthedocs.io/en/master/). | |||
The two guides to focus on are the `Application Development Guide` and `Using ABCI-CLI`. | |||
## Protocl Buffers | |||
To compile the protobuf file, run: | |||
``` | |||
make protoc | |||
``` | |||
See `protoc --help` and [the Protocol Buffers site](https://developers.google.com/protocol-buffers) | |||
for details on compiling for other languages. Note we also include a [GRPC](http://www.grpc.io/docs) | |||
service definition. | |||
## Install ABCI-CLI | |||
The `abci-cli` is a simple tool for debugging ABCI servers and running some | |||
example apps. To install it: | |||
``` | |||
go get github.com/tendermint/abci | |||
cd $GOPATH/src/github.com/tendermint/abci | |||
make get_vendor_deps | |||
make install | |||
``` | |||
## Implementation | |||
We provide three implementations of the ABCI in Go: | |||
- Golang in-process | |||
- ABCI-socket | |||
- GRPC | |||
Note the GRPC version is maintained primarily to simplify onboarding and prototyping and is not receiving the same | |||
attention to security and performance as the others | |||
### In Process | |||
The simplest implementation just uses function calls within Go. | |||
This means ABCI applications written in Golang can be compiled with TendermintCore and run as a single binary. | |||
See the [examples](#examples) below for more information. | |||
### Socket (TSP) | |||
ABCI is best implemented as a streaming protocol. | |||
The socket implementation provides for asynchronous, ordered message passing over unix or tcp. | |||
Messages are serialized using Protobuf3 and length-prefixed with a [signed Varint](https://developers.google.com/protocol-buffers/docs/encoding?csw=1#signed-integers) | |||
For example, if the Protobuf3 encoded ABCI message is `0xDEADBEEF` (4 bytes), the length-prefixed message is `0x08DEADBEEF`, since `0x08` is the signed varint | |||
encoding of `4`. If the Protobuf3 encoded ABCI message is 65535 bytes long, the length-prefixed message would be like `0xFEFF07...`. | |||
Note the benefit of using this `varint` encoding over the old version (where integers were encoded as `<len of len><big endian len>` is that | |||
it is the standard way to encode integers in Protobuf. It is also generally shorter. | |||
### GRPC | |||
GRPC is an rpc framework native to Protocol Buffers with support in many languages. | |||
Implementing the ABCI using GRPC can allow for faster prototyping, but is expected to be much slower than | |||
the ordered, asynchronous socket protocol. The implementation has also not received as much testing or review. | |||
Note the length-prefixing used in the socket implementation does not apply for GRPC. | |||
## Usage | |||
The `abci-cli` tool wraps an ABCI client and can be used for probing/testing an ABCI server. | |||
For instance, `abci-cli test` will run a test sequence against a listening server running the Counter application (see below). | |||
It can also be used to run some example applications. | |||
See [the documentation](http://tendermint.readthedocs.io/en/master/) for more details. | |||
### Examples | |||
Check out the variety of example applications in the [example directory](example/). | |||
It also contains the code refered to by the `counter` and `kvstore` apps; these apps come | |||
built into the `abci-cli` binary. | |||
#### Counter | |||
The `abci-cli counter` application illustrates nonce checking in transactions. It's code looks like: | |||
```golang | |||
func cmdCounter(cmd *cobra.Command, args []string) error { | |||
app := counter.NewCounterApplication(flagSerial) | |||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | |||
// Start the listener | |||
srv, err := server.NewServer(flagAddrC, flagAbci, app) | |||
if err != nil { | |||
return err | |||
} | |||
srv.SetLogger(logger.With("module", "abci-server")) | |||
if err := srv.Start(); err != nil { | |||
return err | |||
} | |||
// Wait forever | |||
cmn.TrapSignal(func() { | |||
// Cleanup | |||
srv.Stop() | |||
}) | |||
return nil | |||
} | |||
``` | |||
and can be found in [this file](cmd/abci-cli/abci-cli.go). | |||
#### kvstore | |||
The `abci-cli kvstore` application, which illustrates a simple key-value Merkle tree | |||
```golang | |||
func cmdKVStore(cmd *cobra.Command, args []string) error { | |||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | |||
// Create the application - in memory or persisted to disk | |||
var app types.Application | |||
if flagPersist == "" { | |||
app = kvstore.NewKVStoreApplication() | |||
} else { | |||
app = kvstore.NewPersistentKVStoreApplication(flagPersist) | |||
app.(*kvstore.PersistentKVStoreApplication).SetLogger(logger.With("module", "kvstore")) | |||
} | |||
// Start the listener | |||
srv, err := server.NewServer(flagAddrD, flagAbci, app) | |||
if err != nil { | |||
return err | |||
} | |||
srv.SetLogger(logger.With("module", "abci-server")) | |||
if err := srv.Start(); err != nil { | |||
return err | |||
} | |||
// Wait forever | |||
cmn.TrapSignal(func() { | |||
// Cleanup | |||
srv.Stop() | |||
}) | |||
return nil | |||
} | |||
``` |
@ -0,0 +1,129 @@ | |||
package abcicli | |||
import ( | |||
"fmt" | |||
"sync" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
const ( | |||
dialRetryIntervalSeconds = 3 | |||
echoRetryIntervalSeconds = 1 | |||
) | |||
// Client defines an interface for an ABCI client. | |||
// All `Async` methods return a `ReqRes` object. | |||
// All `Sync` methods return the appropriate protobuf ResponseXxx struct and an error. | |||
// Note these are client errors, eg. ABCI socket connectivity issues. | |||
// Application-related errors are reflected in response via ABCI error codes and logs. | |||
type Client interface { | |||
cmn.Service | |||
SetResponseCallback(Callback) | |||
Error() error | |||
FlushAsync() *ReqRes | |||
EchoAsync(msg string) *ReqRes | |||
InfoAsync(types.RequestInfo) *ReqRes | |||
SetOptionAsync(types.RequestSetOption) *ReqRes | |||
DeliverTxAsync(tx []byte) *ReqRes | |||
CheckTxAsync(tx []byte) *ReqRes | |||
QueryAsync(types.RequestQuery) *ReqRes | |||
CommitAsync() *ReqRes | |||
InitChainAsync(types.RequestInitChain) *ReqRes | |||
BeginBlockAsync(types.RequestBeginBlock) *ReqRes | |||
EndBlockAsync(types.RequestEndBlock) *ReqRes | |||
FlushSync() error | |||
EchoSync(msg string) (*types.ResponseEcho, error) | |||
InfoSync(types.RequestInfo) (*types.ResponseInfo, error) | |||
SetOptionSync(types.RequestSetOption) (*types.ResponseSetOption, error) | |||
DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) | |||
CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) | |||
QuerySync(types.RequestQuery) (*types.ResponseQuery, error) | |||
CommitSync() (*types.ResponseCommit, error) | |||
InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error) | |||
BeginBlockSync(types.RequestBeginBlock) (*types.ResponseBeginBlock, error) | |||
EndBlockSync(types.RequestEndBlock) (*types.ResponseEndBlock, error) | |||
} | |||
//---------------------------------------- | |||
// NewClient returns a new ABCI client of the specified transport type. | |||
// It returns an error if the transport is not "socket" or "grpc" | |||
func NewClient(addr, transport string, mustConnect bool) (client Client, err error) { | |||
switch transport { | |||
case "socket": | |||
client = NewSocketClient(addr, mustConnect) | |||
case "grpc": | |||
client = NewGRPCClient(addr, mustConnect) | |||
default: | |||
err = fmt.Errorf("Unknown abci transport %s", transport) | |||
} | |||
return | |||
} | |||
//---------------------------------------- | |||
type Callback func(*types.Request, *types.Response) | |||
//---------------------------------------- | |||
type ReqRes struct { | |||
*types.Request | |||
*sync.WaitGroup | |||
*types.Response // Not set atomically, so be sure to use WaitGroup. | |||
mtx sync.Mutex | |||
done bool // Gets set to true once *after* WaitGroup.Done(). | |||
cb func(*types.Response) // A single callback that may be set. | |||
} | |||
func NewReqRes(req *types.Request) *ReqRes { | |||
return &ReqRes{ | |||
Request: req, | |||
WaitGroup: waitGroup1(), | |||
Response: nil, | |||
done: false, | |||
cb: nil, | |||
} | |||
} | |||
// Sets the callback for this ReqRes atomically. | |||
// If reqRes is already done, calls cb immediately. | |||
// NOTE: reqRes.cb should not change if reqRes.done. | |||
// NOTE: only one callback is supported. | |||
func (reqRes *ReqRes) SetCallback(cb func(res *types.Response)) { | |||
reqRes.mtx.Lock() | |||
if reqRes.done { | |||
reqRes.mtx.Unlock() | |||
cb(reqRes.Response) | |||
return | |||
} | |||
defer reqRes.mtx.Unlock() | |||
reqRes.cb = cb | |||
} | |||
func (reqRes *ReqRes) GetCallback() func(*types.Response) { | |||
reqRes.mtx.Lock() | |||
defer reqRes.mtx.Unlock() | |||
return reqRes.cb | |||
} | |||
// NOTE: it should be safe to read reqRes.cb without locks after this. | |||
func (reqRes *ReqRes) SetDone() { | |||
reqRes.mtx.Lock() | |||
reqRes.done = true | |||
reqRes.mtx.Unlock() | |||
} | |||
func waitGroup1() (wg *sync.WaitGroup) { | |||
wg = &sync.WaitGroup{} | |||
wg.Add(1) | |||
return | |||
} |
@ -0,0 +1,301 @@ | |||
package abcicli | |||
import ( | |||
"fmt" | |||
"net" | |||
"sync" | |||
"time" | |||
context "golang.org/x/net/context" | |||
grpc "google.golang.org/grpc" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
var _ Client = (*grpcClient)(nil) | |||
// A stripped copy of the remoteClient that makes | |||
// synchronous calls using grpc | |||
type grpcClient struct { | |||
cmn.BaseService | |||
mustConnect bool | |||
client types.ABCIApplicationClient | |||
mtx sync.Mutex | |||
addr string | |||
err error | |||
resCb func(*types.Request, *types.Response) // listens to all callbacks | |||
} | |||
func NewGRPCClient(addr string, mustConnect bool) *grpcClient { | |||
cli := &grpcClient{ | |||
addr: addr, | |||
mustConnect: mustConnect, | |||
} | |||
cli.BaseService = *cmn.NewBaseService(nil, "grpcClient", cli) | |||
return cli | |||
} | |||
func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { | |||
return cmn.Connect(addr) | |||
} | |||
func (cli *grpcClient) OnStart() error { | |||
if err := cli.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
RETRY_LOOP: | |||
for { | |||
conn, err := grpc.Dial(cli.addr, grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) | |||
if err != nil { | |||
if cli.mustConnect { | |||
return err | |||
} | |||
cli.Logger.Error(fmt.Sprintf("abci.grpcClient failed to connect to %v. Retrying...\n", cli.addr)) | |||
time.Sleep(time.Second * dialRetryIntervalSeconds) | |||
continue RETRY_LOOP | |||
} | |||
cli.Logger.Info("Dialed server. Waiting for echo.", "addr", cli.addr) | |||
client := types.NewABCIApplicationClient(conn) | |||
ENSURE_CONNECTED: | |||
for { | |||
_, err := client.Echo(context.Background(), &types.RequestEcho{"hello"}, grpc.FailFast(true)) | |||
if err == nil { | |||
break ENSURE_CONNECTED | |||
} | |||
cli.Logger.Error("Echo failed", "err", err) | |||
time.Sleep(time.Second * echoRetryIntervalSeconds) | |||
} | |||
cli.client = client | |||
return nil | |||
} | |||
} | |||
func (cli *grpcClient) OnStop() { | |||
cli.BaseService.OnStop() | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
// TODO: how to close conn? its not a net.Conn and grpc doesn't expose a Close() | |||
/*if cli.client.conn != nil { | |||
cli.client.conn.Close() | |||
}*/ | |||
} | |||
func (cli *grpcClient) StopForError(err error) { | |||
cli.mtx.Lock() | |||
if !cli.IsRunning() { | |||
return | |||
} | |||
if cli.err == nil { | |||
cli.err = err | |||
} | |||
cli.mtx.Unlock() | |||
cli.Logger.Error(fmt.Sprintf("Stopping abci.grpcClient for error: %v", err.Error())) | |||
cli.Stop() | |||
} | |||
func (cli *grpcClient) Error() error { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
return cli.err | |||
} | |||
// Set listener for all responses | |||
// NOTE: callback may get internally generated flush responses. | |||
func (cli *grpcClient) SetResponseCallback(resCb Callback) { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
cli.resCb = resCb | |||
} | |||
//---------------------------------------- | |||
// GRPC calls are synchronous, but some callbacks expect to be called asynchronously | |||
// (eg. the mempool expects to be able to lock to remove bad txs from cache). | |||
// To accommodate, we finish each call in its own go-routine, | |||
// which is expensive, but easy - if you want something better, use the socket protocol! | |||
// maybe one day, if people really want it, we use grpc streams, | |||
// but hopefully not :D | |||
func (cli *grpcClient) EchoAsync(msg string) *ReqRes { | |||
req := types.ToRequestEcho(msg) | |||
res, err := cli.client.Echo(context.Background(), req.GetEcho(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_Echo{res}}) | |||
} | |||
func (cli *grpcClient) FlushAsync() *ReqRes { | |||
req := types.ToRequestFlush() | |||
res, err := cli.client.Flush(context.Background(), req.GetFlush(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_Flush{res}}) | |||
} | |||
func (cli *grpcClient) InfoAsync(params types.RequestInfo) *ReqRes { | |||
req := types.ToRequestInfo(params) | |||
res, err := cli.client.Info(context.Background(), req.GetInfo(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_Info{res}}) | |||
} | |||
func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { | |||
req := types.ToRequestSetOption(params) | |||
res, err := cli.client.SetOption(context.Background(), req.GetSetOption(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_SetOption{res}}) | |||
} | |||
func (cli *grpcClient) DeliverTxAsync(tx []byte) *ReqRes { | |||
req := types.ToRequestDeliverTx(tx) | |||
res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_DeliverTx{res}}) | |||
} | |||
func (cli *grpcClient) CheckTxAsync(tx []byte) *ReqRes { | |||
req := types.ToRequestCheckTx(tx) | |||
res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_CheckTx{res}}) | |||
} | |||
func (cli *grpcClient) QueryAsync(params types.RequestQuery) *ReqRes { | |||
req := types.ToRequestQuery(params) | |||
res, err := cli.client.Query(context.Background(), req.GetQuery(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_Query{res}}) | |||
} | |||
func (cli *grpcClient) CommitAsync() *ReqRes { | |||
req := types.ToRequestCommit() | |||
res, err := cli.client.Commit(context.Background(), req.GetCommit(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_Commit{res}}) | |||
} | |||
func (cli *grpcClient) InitChainAsync(params types.RequestInitChain) *ReqRes { | |||
req := types.ToRequestInitChain(params) | |||
res, err := cli.client.InitChain(context.Background(), req.GetInitChain(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_InitChain{res}}) | |||
} | |||
func (cli *grpcClient) BeginBlockAsync(params types.RequestBeginBlock) *ReqRes { | |||
req := types.ToRequestBeginBlock(params) | |||
res, err := cli.client.BeginBlock(context.Background(), req.GetBeginBlock(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_BeginBlock{res}}) | |||
} | |||
func (cli *grpcClient) EndBlockAsync(params types.RequestEndBlock) *ReqRes { | |||
req := types.ToRequestEndBlock(params) | |||
res, err := cli.client.EndBlock(context.Background(), req.GetEndBlock(), grpc.FailFast(true)) | |||
if err != nil { | |||
cli.StopForError(err) | |||
} | |||
return cli.finishAsyncCall(req, &types.Response{&types.Response_EndBlock{res}}) | |||
} | |||
func (cli *grpcClient) finishAsyncCall(req *types.Request, res *types.Response) *ReqRes { | |||
reqres := NewReqRes(req) | |||
reqres.Response = res // Set response | |||
reqres.Done() // Release waiters | |||
reqres.SetDone() // so reqRes.SetCallback will run the callback | |||
// go routine for callbacks | |||
go func() { | |||
// Notify reqRes listener if set | |||
if cb := reqres.GetCallback(); cb != nil { | |||
cb(res) | |||
} | |||
// Notify client listener if set | |||
if cli.resCb != nil { | |||
cli.resCb(reqres.Request, res) | |||
} | |||
}() | |||
return reqres | |||
} | |||
//---------------------------------------- | |||
func (cli *grpcClient) FlushSync() error { | |||
return nil | |||
} | |||
func (cli *grpcClient) EchoSync(msg string) (*types.ResponseEcho, error) { | |||
reqres := cli.EchoAsync(msg) | |||
// StopForError should already have been called if error is set | |||
return reqres.Response.GetEcho(), cli.Error() | |||
} | |||
func (cli *grpcClient) InfoSync(req types.RequestInfo) (*types.ResponseInfo, error) { | |||
reqres := cli.InfoAsync(req) | |||
return reqres.Response.GetInfo(), cli.Error() | |||
} | |||
func (cli *grpcClient) SetOptionSync(req types.RequestSetOption) (*types.ResponseSetOption, error) { | |||
reqres := cli.SetOptionAsync(req) | |||
return reqres.Response.GetSetOption(), cli.Error() | |||
} | |||
func (cli *grpcClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { | |||
reqres := cli.DeliverTxAsync(tx) | |||
return reqres.Response.GetDeliverTx(), cli.Error() | |||
} | |||
func (cli *grpcClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { | |||
reqres := cli.CheckTxAsync(tx) | |||
return reqres.Response.GetCheckTx(), cli.Error() | |||
} | |||
func (cli *grpcClient) QuerySync(req types.RequestQuery) (*types.ResponseQuery, error) { | |||
reqres := cli.QueryAsync(req) | |||
return reqres.Response.GetQuery(), cli.Error() | |||
} | |||
func (cli *grpcClient) CommitSync() (*types.ResponseCommit, error) { | |||
reqres := cli.CommitAsync() | |||
return reqres.Response.GetCommit(), cli.Error() | |||
} | |||
func (cli *grpcClient) InitChainSync(params types.RequestInitChain) (*types.ResponseInitChain, error) { | |||
reqres := cli.InitChainAsync(params) | |||
return reqres.Response.GetInitChain(), cli.Error() | |||
} | |||
func (cli *grpcClient) BeginBlockSync(params types.RequestBeginBlock) (*types.ResponseBeginBlock, error) { | |||
reqres := cli.BeginBlockAsync(params) | |||
return reqres.Response.GetBeginBlock(), cli.Error() | |||
} | |||
func (cli *grpcClient) EndBlockSync(params types.RequestEndBlock) (*types.ResponseEndBlock, error) { | |||
reqres := cli.EndBlockAsync(params) | |||
return reqres.Response.GetEndBlock(), cli.Error() | |||
} |
@ -0,0 +1,230 @@ | |||
package abcicli | |||
import ( | |||
"sync" | |||
types "github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
var _ Client = (*localClient)(nil) | |||
type localClient struct { | |||
cmn.BaseService | |||
mtx *sync.Mutex | |||
types.Application | |||
Callback | |||
} | |||
func NewLocalClient(mtx *sync.Mutex, app types.Application) *localClient { | |||
if mtx == nil { | |||
mtx = new(sync.Mutex) | |||
} | |||
cli := &localClient{ | |||
mtx: mtx, | |||
Application: app, | |||
} | |||
cli.BaseService = *cmn.NewBaseService(nil, "localClient", cli) | |||
return cli | |||
} | |||
func (app *localClient) SetResponseCallback(cb Callback) { | |||
app.mtx.Lock() | |||
defer app.mtx.Unlock() | |||
app.Callback = cb | |||
} | |||
// TODO: change types.Application to include Error()? | |||
func (app *localClient) Error() error { | |||
return nil | |||
} | |||
func (app *localClient) FlushAsync() *ReqRes { | |||
// Do nothing | |||
return newLocalReqRes(types.ToRequestFlush(), nil) | |||
} | |||
func (app *localClient) EchoAsync(msg string) *ReqRes { | |||
return app.callback( | |||
types.ToRequestEcho(msg), | |||
types.ToResponseEcho(msg), | |||
) | |||
} | |||
func (app *localClient) InfoAsync(req types.RequestInfo) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.Info(req) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestInfo(req), | |||
types.ToResponseInfo(res), | |||
) | |||
} | |||
func (app *localClient) SetOptionAsync(req types.RequestSetOption) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.SetOption(req) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestSetOption(req), | |||
types.ToResponseSetOption(res), | |||
) | |||
} | |||
func (app *localClient) DeliverTxAsync(tx []byte) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.DeliverTx(tx) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestDeliverTx(tx), | |||
types.ToResponseDeliverTx(res), | |||
) | |||
} | |||
func (app *localClient) CheckTxAsync(tx []byte) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.CheckTx(tx) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestCheckTx(tx), | |||
types.ToResponseCheckTx(res), | |||
) | |||
} | |||
func (app *localClient) QueryAsync(req types.RequestQuery) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.Query(req) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestQuery(req), | |||
types.ToResponseQuery(res), | |||
) | |||
} | |||
func (app *localClient) CommitAsync() *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.Commit() | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestCommit(), | |||
types.ToResponseCommit(res), | |||
) | |||
} | |||
func (app *localClient) InitChainAsync(req types.RequestInitChain) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.InitChain(req) | |||
reqRes := app.callback( | |||
types.ToRequestInitChain(req), | |||
types.ToResponseInitChain(res), | |||
) | |||
app.mtx.Unlock() | |||
return reqRes | |||
} | |||
func (app *localClient) BeginBlockAsync(req types.RequestBeginBlock) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.BeginBlock(req) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestBeginBlock(req), | |||
types.ToResponseBeginBlock(res), | |||
) | |||
} | |||
func (app *localClient) EndBlockAsync(req types.RequestEndBlock) *ReqRes { | |||
app.mtx.Lock() | |||
res := app.Application.EndBlock(req) | |||
app.mtx.Unlock() | |||
return app.callback( | |||
types.ToRequestEndBlock(req), | |||
types.ToResponseEndBlock(res), | |||
) | |||
} | |||
//------------------------------------------------------- | |||
func (app *localClient) FlushSync() error { | |||
return nil | |||
} | |||
func (app *localClient) EchoSync(msg string) (*types.ResponseEcho, error) { | |||
return &types.ResponseEcho{msg}, nil | |||
} | |||
func (app *localClient) InfoSync(req types.RequestInfo) (*types.ResponseInfo, error) { | |||
app.mtx.Lock() | |||
res := app.Application.Info(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) SetOptionSync(req types.RequestSetOption) (*types.ResponseSetOption, error) { | |||
app.mtx.Lock() | |||
res := app.Application.SetOption(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { | |||
app.mtx.Lock() | |||
res := app.Application.DeliverTx(tx) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { | |||
app.mtx.Lock() | |||
res := app.Application.CheckTx(tx) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) QuerySync(req types.RequestQuery) (*types.ResponseQuery, error) { | |||
app.mtx.Lock() | |||
res := app.Application.Query(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) CommitSync() (*types.ResponseCommit, error) { | |||
app.mtx.Lock() | |||
res := app.Application.Commit() | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) InitChainSync(req types.RequestInitChain) (*types.ResponseInitChain, error) { | |||
app.mtx.Lock() | |||
res := app.Application.InitChain(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) BeginBlockSync(req types.RequestBeginBlock) (*types.ResponseBeginBlock, error) { | |||
app.mtx.Lock() | |||
res := app.Application.BeginBlock(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
func (app *localClient) EndBlockSync(req types.RequestEndBlock) (*types.ResponseEndBlock, error) { | |||
app.mtx.Lock() | |||
res := app.Application.EndBlock(req) | |||
app.mtx.Unlock() | |||
return &res, nil | |||
} | |||
//------------------------------------------------------- | |||
func (app *localClient) callback(req *types.Request, res *types.Response) *ReqRes { | |||
app.Callback(req, res) | |||
return newLocalReqRes(req, res) | |||
} | |||
func newLocalReqRes(req *types.Request, res *types.Response) *ReqRes { | |||
reqRes := NewReqRes(req) | |||
reqRes.Response = res | |||
reqRes.SetDone() | |||
return reqRes | |||
} |
@ -0,0 +1,399 @@ | |||
package abcicli | |||
import ( | |||
"bufio" | |||
"container/list" | |||
"errors" | |||
"fmt" | |||
"net" | |||
"reflect" | |||
"sync" | |||
"time" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
const reqQueueSize = 256 // TODO make configurable | |||
// const maxResponseSize = 1048576 // 1MB TODO make configurable | |||
const flushThrottleMS = 20 // Don't wait longer than... | |||
var _ Client = (*socketClient)(nil) | |||
// This is goroutine-safe, but users should beware that | |||
// the application in general is not meant to be interfaced | |||
// with concurrent callers. | |||
type socketClient struct { | |||
cmn.BaseService | |||
reqQueue chan *ReqRes | |||
flushTimer *cmn.ThrottleTimer | |||
mustConnect bool | |||
mtx sync.Mutex | |||
addr string | |||
conn net.Conn | |||
err error | |||
reqSent *list.List | |||
resCb func(*types.Request, *types.Response) // listens to all callbacks | |||
} | |||
func NewSocketClient(addr string, mustConnect bool) *socketClient { | |||
cli := &socketClient{ | |||
reqQueue: make(chan *ReqRes, reqQueueSize), | |||
flushTimer: cmn.NewThrottleTimer("socketClient", flushThrottleMS), | |||
mustConnect: mustConnect, | |||
addr: addr, | |||
reqSent: list.New(), | |||
resCb: nil, | |||
} | |||
cli.BaseService = *cmn.NewBaseService(nil, "socketClient", cli) | |||
return cli | |||
} | |||
func (cli *socketClient) OnStart() error { | |||
if err := cli.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
var err error | |||
var conn net.Conn | |||
RETRY_LOOP: | |||
for { | |||
conn, err = cmn.Connect(cli.addr) | |||
if err != nil { | |||
if cli.mustConnect { | |||
return err | |||
} | |||
cli.Logger.Error(fmt.Sprintf("abci.socketClient failed to connect to %v. Retrying...", cli.addr)) | |||
time.Sleep(time.Second * dialRetryIntervalSeconds) | |||
continue RETRY_LOOP | |||
} | |||
cli.conn = conn | |||
go cli.sendRequestsRoutine(conn) | |||
go cli.recvResponseRoutine(conn) | |||
return nil | |||
} | |||
} | |||
func (cli *socketClient) OnStop() { | |||
cli.BaseService.OnStop() | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
if cli.conn != nil { | |||
cli.conn.Close() | |||
} | |||
cli.flushQueue() | |||
} | |||
// Stop the client and set the error | |||
func (cli *socketClient) StopForError(err error) { | |||
if !cli.IsRunning() { | |||
return | |||
} | |||
cli.mtx.Lock() | |||
if cli.err == nil { | |||
cli.err = err | |||
} | |||
cli.mtx.Unlock() | |||
cli.Logger.Error(fmt.Sprintf("Stopping abci.socketClient for error: %v", err.Error())) | |||
cli.Stop() | |||
} | |||
func (cli *socketClient) Error() error { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
return cli.err | |||
} | |||
// Set listener for all responses | |||
// NOTE: callback may get internally generated flush responses. | |||
func (cli *socketClient) SetResponseCallback(resCb Callback) { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
cli.resCb = resCb | |||
} | |||
//---------------------------------------- | |||
func (cli *socketClient) sendRequestsRoutine(conn net.Conn) { | |||
w := bufio.NewWriter(conn) | |||
for { | |||
select { | |||
case <-cli.flushTimer.Ch: | |||
select { | |||
case cli.reqQueue <- NewReqRes(types.ToRequestFlush()): | |||
default: | |||
// Probably will fill the buffer, or retry later. | |||
} | |||
case <-cli.Quit(): | |||
return | |||
case reqres := <-cli.reqQueue: | |||
cli.willSendReq(reqres) | |||
err := types.WriteMessage(reqres.Request, w) | |||
if err != nil { | |||
cli.StopForError(fmt.Errorf("Error writing msg: %v", err)) | |||
return | |||
} | |||
// cli.Logger.Debug("Sent request", "requestType", reflect.TypeOf(reqres.Request), "request", reqres.Request) | |||
if _, ok := reqres.Request.Value.(*types.Request_Flush); ok { | |||
err = w.Flush() | |||
if err != nil { | |||
cli.StopForError(fmt.Errorf("Error flushing writer: %v", err)) | |||
return | |||
} | |||
} | |||
} | |||
} | |||
} | |||
func (cli *socketClient) recvResponseRoutine(conn net.Conn) { | |||
r := bufio.NewReader(conn) // Buffer reads | |||
for { | |||
var res = &types.Response{} | |||
err := types.ReadMessage(r, res) | |||
if err != nil { | |||
cli.StopForError(err) | |||
return | |||
} | |||
switch r := res.Value.(type) { | |||
case *types.Response_Exception: | |||
// XXX After setting cli.err, release waiters (e.g. reqres.Done()) | |||
cli.StopForError(errors.New(r.Exception.Error)) | |||
return | |||
default: | |||
// cli.Logger.Debug("Received response", "responseType", reflect.TypeOf(res), "response", res) | |||
err := cli.didRecvResponse(res) | |||
if err != nil { | |||
cli.StopForError(err) | |||
return | |||
} | |||
} | |||
} | |||
} | |||
func (cli *socketClient) willSendReq(reqres *ReqRes) { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
cli.reqSent.PushBack(reqres) | |||
} | |||
func (cli *socketClient) didRecvResponse(res *types.Response) error { | |||
cli.mtx.Lock() | |||
defer cli.mtx.Unlock() | |||
// Get the first ReqRes | |||
next := cli.reqSent.Front() | |||
if next == nil { | |||
return fmt.Errorf("Unexpected result type %v when nothing expected", reflect.TypeOf(res.Value)) | |||
} | |||
reqres := next.Value.(*ReqRes) | |||
if !resMatchesReq(reqres.Request, res) { | |||
return fmt.Errorf("Unexpected result type %v when response to %v expected", | |||
reflect.TypeOf(res.Value), reflect.TypeOf(reqres.Request.Value)) | |||
} | |||
reqres.Response = res // Set response | |||
reqres.Done() // Release waiters | |||
cli.reqSent.Remove(next) // Pop first item from linked list | |||
// Notify reqRes listener if set | |||
if cb := reqres.GetCallback(); cb != nil { | |||
cb(res) | |||
} | |||
// Notify client listener if set | |||
if cli.resCb != nil { | |||
cli.resCb(reqres.Request, res) | |||
} | |||
return nil | |||
} | |||
//---------------------------------------- | |||
func (cli *socketClient) EchoAsync(msg string) *ReqRes { | |||
return cli.queueRequest(types.ToRequestEcho(msg)) | |||
} | |||
func (cli *socketClient) FlushAsync() *ReqRes { | |||
return cli.queueRequest(types.ToRequestFlush()) | |||
} | |||
func (cli *socketClient) InfoAsync(req types.RequestInfo) *ReqRes { | |||
return cli.queueRequest(types.ToRequestInfo(req)) | |||
} | |||
func (cli *socketClient) SetOptionAsync(req types.RequestSetOption) *ReqRes { | |||
return cli.queueRequest(types.ToRequestSetOption(req)) | |||
} | |||
func (cli *socketClient) DeliverTxAsync(tx []byte) *ReqRes { | |||
return cli.queueRequest(types.ToRequestDeliverTx(tx)) | |||
} | |||
func (cli *socketClient) CheckTxAsync(tx []byte) *ReqRes { | |||
return cli.queueRequest(types.ToRequestCheckTx(tx)) | |||
} | |||
func (cli *socketClient) QueryAsync(req types.RequestQuery) *ReqRes { | |||
return cli.queueRequest(types.ToRequestQuery(req)) | |||
} | |||
func (cli *socketClient) CommitAsync() *ReqRes { | |||
return cli.queueRequest(types.ToRequestCommit()) | |||
} | |||
func (cli *socketClient) InitChainAsync(req types.RequestInitChain) *ReqRes { | |||
return cli.queueRequest(types.ToRequestInitChain(req)) | |||
} | |||
func (cli *socketClient) BeginBlockAsync(req types.RequestBeginBlock) *ReqRes { | |||
return cli.queueRequest(types.ToRequestBeginBlock(req)) | |||
} | |||
func (cli *socketClient) EndBlockAsync(req types.RequestEndBlock) *ReqRes { | |||
return cli.queueRequest(types.ToRequestEndBlock(req)) | |||
} | |||
//---------------------------------------- | |||
func (cli *socketClient) FlushSync() error { | |||
reqRes := cli.queueRequest(types.ToRequestFlush()) | |||
if err := cli.Error(); err != nil { | |||
return err | |||
} | |||
reqRes.Wait() // NOTE: if we don't flush the queue, its possible to get stuck here | |||
return cli.Error() | |||
} | |||
func (cli *socketClient) EchoSync(msg string) (*types.ResponseEcho, error) { | |||
reqres := cli.queueRequest(types.ToRequestEcho(msg)) | |||
cli.FlushSync() | |||
return reqres.Response.GetEcho(), cli.Error() | |||
} | |||
func (cli *socketClient) InfoSync(req types.RequestInfo) (*types.ResponseInfo, error) { | |||
reqres := cli.queueRequest(types.ToRequestInfo(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetInfo(), cli.Error() | |||
} | |||
func (cli *socketClient) SetOptionSync(req types.RequestSetOption) (*types.ResponseSetOption, error) { | |||
reqres := cli.queueRequest(types.ToRequestSetOption(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetSetOption(), cli.Error() | |||
} | |||
func (cli *socketClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { | |||
reqres := cli.queueRequest(types.ToRequestDeliverTx(tx)) | |||
cli.FlushSync() | |||
return reqres.Response.GetDeliverTx(), cli.Error() | |||
} | |||
func (cli *socketClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { | |||
reqres := cli.queueRequest(types.ToRequestCheckTx(tx)) | |||
cli.FlushSync() | |||
return reqres.Response.GetCheckTx(), cli.Error() | |||
} | |||
func (cli *socketClient) QuerySync(req types.RequestQuery) (*types.ResponseQuery, error) { | |||
reqres := cli.queueRequest(types.ToRequestQuery(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetQuery(), cli.Error() | |||
} | |||
func (cli *socketClient) CommitSync() (*types.ResponseCommit, error) { | |||
reqres := cli.queueRequest(types.ToRequestCommit()) | |||
cli.FlushSync() | |||
return reqres.Response.GetCommit(), cli.Error() | |||
} | |||
func (cli *socketClient) InitChainSync(req types.RequestInitChain) (*types.ResponseInitChain, error) { | |||
reqres := cli.queueRequest(types.ToRequestInitChain(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetInitChain(), cli.Error() | |||
} | |||
func (cli *socketClient) BeginBlockSync(req types.RequestBeginBlock) (*types.ResponseBeginBlock, error) { | |||
reqres := cli.queueRequest(types.ToRequestBeginBlock(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetBeginBlock(), cli.Error() | |||
} | |||
func (cli *socketClient) EndBlockSync(req types.RequestEndBlock) (*types.ResponseEndBlock, error) { | |||
reqres := cli.queueRequest(types.ToRequestEndBlock(req)) | |||
cli.FlushSync() | |||
return reqres.Response.GetEndBlock(), cli.Error() | |||
} | |||
//---------------------------------------- | |||
func (cli *socketClient) queueRequest(req *types.Request) *ReqRes { | |||
reqres := NewReqRes(req) | |||
// TODO: set cli.err if reqQueue times out | |||
cli.reqQueue <- reqres | |||
// Maybe auto-flush, or unset auto-flush | |||
switch req.Value.(type) { | |||
case *types.Request_Flush: | |||
cli.flushTimer.Unset() | |||
default: | |||
cli.flushTimer.Set() | |||
} | |||
return reqres | |||
} | |||
func (cli *socketClient) flushQueue() { | |||
LOOP: | |||
for { | |||
select { | |||
case reqres := <-cli.reqQueue: | |||
reqres.Done() | |||
default: | |||
break LOOP | |||
} | |||
} | |||
} | |||
//---------------------------------------- | |||
func resMatchesReq(req *types.Request, res *types.Response) (ok bool) { | |||
switch req.Value.(type) { | |||
case *types.Request_Echo: | |||
_, ok = res.Value.(*types.Response_Echo) | |||
case *types.Request_Flush: | |||
_, ok = res.Value.(*types.Response_Flush) | |||
case *types.Request_Info: | |||
_, ok = res.Value.(*types.Response_Info) | |||
case *types.Request_SetOption: | |||
_, ok = res.Value.(*types.Response_SetOption) | |||
case *types.Request_DeliverTx: | |||
_, ok = res.Value.(*types.Response_DeliverTx) | |||
case *types.Request_CheckTx: | |||
_, ok = res.Value.(*types.Response_CheckTx) | |||
case *types.Request_Commit: | |||
_, ok = res.Value.(*types.Response_Commit) | |||
case *types.Request_Query: | |||
_, ok = res.Value.(*types.Response_Query) | |||
case *types.Request_InitChain: | |||
_, ok = res.Value.(*types.Response_InitChain) | |||
case *types.Request_BeginBlock: | |||
_, ok = res.Value.(*types.Response_BeginBlock) | |||
case *types.Request_EndBlock: | |||
_, ok = res.Value.(*types.Response_EndBlock) | |||
} | |||
return ok | |||
} |
@ -0,0 +1,28 @@ | |||
package abcicli_test | |||
import ( | |||
"errors" | |||
"testing" | |||
"time" | |||
"github.com/tendermint/tendermint/abci/client" | |||
) | |||
func TestSocketClientStopForErrorDeadlock(t *testing.T) { | |||
c := abcicli.NewSocketClient(":80", false) | |||
err := errors.New("foo-tendermint") | |||
// See Issue https://github.com/tendermint/abci/issues/114 | |||
doneChan := make(chan bool) | |||
go func() { | |||
defer close(doneChan) | |||
c.StopForError(err) | |||
c.StopForError(err) | |||
}() | |||
select { | |||
case <-doneChan: | |||
case <-time.After(time.Second * 4): | |||
t.Fatalf("Test took too long, potential deadlock still exists") | |||
} | |||
} |
@ -0,0 +1,765 @@ | |||
package main | |||
import ( | |||
"bufio" | |||
"encoding/hex" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"os" | |||
"strings" | |||
"github.com/spf13/cobra" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/tmlibs/log" | |||
abcicli "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/example/counter" | |||
"github.com/tendermint/tendermint/abci/example/kvstore" | |||
"github.com/tendermint/tendermint/abci/server" | |||
servertest "github.com/tendermint/tendermint/abci/tests/server" | |||
"github.com/tendermint/tendermint/abci/types" | |||
"github.com/tendermint/tendermint/abci/version" | |||
) | |||
// client is a global variable so it can be reused by the console | |||
var ( | |||
client abcicli.Client | |||
logger log.Logger | |||
) | |||
// flags | |||
var ( | |||
// global | |||
flagAddress string | |||
flagAbci string | |||
flagVerbose bool // for the println output | |||
flagLogLevel string // for the logger | |||
// query | |||
flagPath string | |||
flagHeight int | |||
flagProve bool | |||
// counter | |||
flagSerial bool | |||
// kvstore | |||
flagPersist string | |||
) | |||
var RootCmd = &cobra.Command{ | |||
Use: "abci-cli", | |||
Short: "the ABCI CLI tool wraps an ABCI client", | |||
Long: "the ABCI CLI tool wraps an ABCI client and is used for testing ABCI servers", | |||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { | |||
switch cmd.Use { | |||
case "counter", "kvstore", "dummy": // for the examples apps, don't pre-run | |||
return nil | |||
case "version": // skip running for version command | |||
return nil | |||
} | |||
if logger == nil { | |||
allowLevel, err := log.AllowLevel(flagLogLevel) | |||
if err != nil { | |||
return err | |||
} | |||
logger = log.NewFilter(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), allowLevel) | |||
} | |||
if client == nil { | |||
var err error | |||
client, err = abcicli.NewClient(flagAddress, flagAbci, false) | |||
if err != nil { | |||
return err | |||
} | |||
client.SetLogger(logger.With("module", "abci-client")) | |||
if err := client.Start(); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
}, | |||
} | |||
// Structure for data passed to print response. | |||
type response struct { | |||
// generic abci response | |||
Data []byte | |||
Code uint32 | |||
Info string | |||
Log string | |||
Query *queryResponse | |||
} | |||
type queryResponse struct { | |||
Key []byte | |||
Value []byte | |||
Height int64 | |||
Proof []byte | |||
} | |||
func Execute() error { | |||
addGlobalFlags() | |||
addCommands() | |||
return RootCmd.Execute() | |||
} | |||
func addGlobalFlags() { | |||
RootCmd.PersistentFlags().StringVarP(&flagAddress, "address", "", "tcp://0.0.0.0:26658", "address of application socket") | |||
RootCmd.PersistentFlags().StringVarP(&flagAbci, "abci", "", "socket", "either socket or grpc") | |||
RootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "print the command and results as if it were a console session") | |||
RootCmd.PersistentFlags().StringVarP(&flagLogLevel, "log_level", "", "debug", "set the logger level") | |||
} | |||
func addQueryFlags() { | |||
queryCmd.PersistentFlags().StringVarP(&flagPath, "path", "", "/store", "path to prefix query with") | |||
queryCmd.PersistentFlags().IntVarP(&flagHeight, "height", "", 0, "height to query the blockchain at") | |||
queryCmd.PersistentFlags().BoolVarP(&flagProve, "prove", "", false, "whether or not to return a merkle proof of the query result") | |||
} | |||
func addCounterFlags() { | |||
counterCmd.PersistentFlags().BoolVarP(&flagSerial, "serial", "", false, "enforce incrementing (serial) transactions") | |||
} | |||
func addDummyFlags() { | |||
dummyCmd.PersistentFlags().StringVarP(&flagPersist, "persist", "", "", "directory to use for a database") | |||
} | |||
func addKVStoreFlags() { | |||
kvstoreCmd.PersistentFlags().StringVarP(&flagPersist, "persist", "", "", "directory to use for a database") | |||
} | |||
func addCommands() { | |||
RootCmd.AddCommand(batchCmd) | |||
RootCmd.AddCommand(consoleCmd) | |||
RootCmd.AddCommand(echoCmd) | |||
RootCmd.AddCommand(infoCmd) | |||
RootCmd.AddCommand(setOptionCmd) | |||
RootCmd.AddCommand(deliverTxCmd) | |||
RootCmd.AddCommand(checkTxCmd) | |||
RootCmd.AddCommand(commitCmd) | |||
RootCmd.AddCommand(versionCmd) | |||
RootCmd.AddCommand(testCmd) | |||
addQueryFlags() | |||
RootCmd.AddCommand(queryCmd) | |||
// examples | |||
addCounterFlags() | |||
RootCmd.AddCommand(counterCmd) | |||
// deprecated, left for backwards compatibility | |||
addDummyFlags() | |||
RootCmd.AddCommand(dummyCmd) | |||
// replaces dummy, see issue #196 | |||
addKVStoreFlags() | |||
RootCmd.AddCommand(kvstoreCmd) | |||
} | |||
var batchCmd = &cobra.Command{ | |||
Use: "batch", | |||
Short: "run a batch of abci commands against an application", | |||
Long: `run a batch of abci commands against an application | |||
This command is run by piping in a file containing a series of commands | |||
you'd like to run: | |||
abci-cli batch < example.file | |||
where example.file looks something like: | |||
set_option serial on | |||
check_tx 0x00 | |||
check_tx 0xff | |||
deliver_tx 0x00 | |||
check_tx 0x00 | |||
deliver_tx 0x01 | |||
deliver_tx 0x04 | |||
info | |||
`, | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdBatch(cmd, args) | |||
}, | |||
} | |||
var consoleCmd = &cobra.Command{ | |||
Use: "console", | |||
Short: "start an interactive ABCI console for multiple commands", | |||
Long: `start an interactive ABCI console for multiple commands | |||
This command opens an interactive console for running any of the other commands | |||
without opening a new connection each time | |||
`, | |||
Args: cobra.ExactArgs(0), | |||
ValidArgs: []string{"echo", "info", "set_option", "deliver_tx", "check_tx", "commit", "query"}, | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdConsole(cmd, args) | |||
}, | |||
} | |||
var echoCmd = &cobra.Command{ | |||
Use: "echo", | |||
Short: "have the application echo a message", | |||
Long: "have the application echo a message", | |||
Args: cobra.ExactArgs(1), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdEcho(cmd, args) | |||
}, | |||
} | |||
var infoCmd = &cobra.Command{ | |||
Use: "info", | |||
Short: "get some info about the application", | |||
Long: "get some info about the application", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdInfo(cmd, args) | |||
}, | |||
} | |||
var setOptionCmd = &cobra.Command{ | |||
Use: "set_option", | |||
Short: "set an option on the application", | |||
Long: "set an option on the application", | |||
Args: cobra.ExactArgs(2), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdSetOption(cmd, args) | |||
}, | |||
} | |||
var deliverTxCmd = &cobra.Command{ | |||
Use: "deliver_tx", | |||
Short: "deliver a new transaction to the application", | |||
Long: "deliver a new transaction to the application", | |||
Args: cobra.ExactArgs(1), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdDeliverTx(cmd, args) | |||
}, | |||
} | |||
var checkTxCmd = &cobra.Command{ | |||
Use: "check_tx", | |||
Short: "validate a transaction", | |||
Long: "validate a transaction", | |||
Args: cobra.ExactArgs(1), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdCheckTx(cmd, args) | |||
}, | |||
} | |||
var commitCmd = &cobra.Command{ | |||
Use: "commit", | |||
Short: "commit the application state and return the Merkle root hash", | |||
Long: "commit the application state and return the Merkle root hash", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdCommit(cmd, args) | |||
}, | |||
} | |||
var versionCmd = &cobra.Command{ | |||
Use: "version", | |||
Short: "print ABCI console version", | |||
Long: "print ABCI console version", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
fmt.Println(version.Version) | |||
return nil | |||
}, | |||
} | |||
var queryCmd = &cobra.Command{ | |||
Use: "query", | |||
Short: "query the application state", | |||
Long: "query the application state", | |||
Args: cobra.ExactArgs(1), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdQuery(cmd, args) | |||
}, | |||
} | |||
var counterCmd = &cobra.Command{ | |||
Use: "counter", | |||
Short: "ABCI demo example", | |||
Long: "ABCI demo example", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdCounter(cmd, args) | |||
}, | |||
} | |||
// deprecated, left for backwards compatibility | |||
var dummyCmd = &cobra.Command{ | |||
Use: "dummy", | |||
Deprecated: "use: [abci-cli kvstore] instead", | |||
Short: "ABCI demo example", | |||
Long: "ABCI demo example", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdKVStore(cmd, args) | |||
}, | |||
} | |||
var kvstoreCmd = &cobra.Command{ | |||
Use: "kvstore", | |||
Short: "ABCI demo example", | |||
Long: "ABCI demo example", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdKVStore(cmd, args) | |||
}, | |||
} | |||
var testCmd = &cobra.Command{ | |||
Use: "test", | |||
Short: "run integration tests", | |||
Long: "run integration tests", | |||
Args: cobra.ExactArgs(0), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return cmdTest(cmd, args) | |||
}, | |||
} | |||
// Generates new Args array based off of previous call args to maintain flag persistence | |||
func persistentArgs(line []byte) []string { | |||
// generate the arguments to run from original os.Args | |||
// to maintain flag arguments | |||
args := os.Args | |||
args = args[:len(args)-1] // remove the previous command argument | |||
if len(line) > 0 { // prevents introduction of extra space leading to argument parse errors | |||
args = append(args, strings.Split(string(line), " ")...) | |||
} | |||
return args | |||
} | |||
//-------------------------------------------------------------------------------- | |||
func compose(fs []func() error) error { | |||
if len(fs) == 0 { | |||
return nil | |||
} else { | |||
err := fs[0]() | |||
if err == nil { | |||
return compose(fs[1:]) | |||
} else { | |||
return err | |||
} | |||
} | |||
} | |||
func cmdTest(cmd *cobra.Command, args []string) error { | |||
return compose( | |||
[]func() error{ | |||
func() error { return servertest.InitChain(client) }, | |||
func() error { return servertest.SetOption(client, "serial", "on") }, | |||
func() error { return servertest.Commit(client, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte("abc"), code.CodeTypeBadNonce, nil) }, | |||
func() error { return servertest.Commit(client, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x00}, code.CodeTypeOK, nil) }, | |||
func() error { return servertest.Commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 1}) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x00}, code.CodeTypeBadNonce, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x01}, code.CodeTypeOK, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x00, 0x02}, code.CodeTypeOK, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x00, 0x03}, code.CodeTypeOK, nil) }, | |||
func() error { return servertest.DeliverTx(client, []byte{0x00, 0x00, 0x04}, code.CodeTypeOK, nil) }, | |||
func() error { | |||
return servertest.DeliverTx(client, []byte{0x00, 0x00, 0x06}, code.CodeTypeBadNonce, nil) | |||
}, | |||
func() error { return servertest.Commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 5}) }, | |||
}) | |||
} | |||
func cmdBatch(cmd *cobra.Command, args []string) error { | |||
bufReader := bufio.NewReader(os.Stdin) | |||
for { | |||
line, more, err := bufReader.ReadLine() | |||
if more { | |||
return errors.New("Input line is too long") | |||
} else if err == io.EOF { | |||
break | |||
} else if len(line) == 0 { | |||
continue | |||
} else if err != nil { | |||
return err | |||
} | |||
cmdArgs := persistentArgs(line) | |||
if err := muxOnCommands(cmd, cmdArgs); err != nil { | |||
return err | |||
} | |||
fmt.Println() | |||
} | |||
return nil | |||
} | |||
func cmdConsole(cmd *cobra.Command, args []string) error { | |||
for { | |||
fmt.Printf("> ") | |||
bufReader := bufio.NewReader(os.Stdin) | |||
line, more, err := bufReader.ReadLine() | |||
if more { | |||
return errors.New("Input is too long") | |||
} else if err != nil { | |||
return err | |||
} | |||
pArgs := persistentArgs(line) | |||
if err := muxOnCommands(cmd, pArgs); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
func muxOnCommands(cmd *cobra.Command, pArgs []string) error { | |||
if len(pArgs) < 2 { | |||
return errors.New("expecting persistent args of the form: abci-cli [command] <...>") | |||
} | |||
// TODO: this parsing is fragile | |||
args := []string{} | |||
for i := 0; i < len(pArgs); i++ { | |||
arg := pArgs[i] | |||
// check for flags | |||
if strings.HasPrefix(arg, "-") { | |||
// if it has an equal, we can just skip | |||
if strings.Contains(arg, "=") { | |||
continue | |||
} | |||
// if its a boolean, we can just skip | |||
_, err := cmd.Flags().GetBool(strings.TrimLeft(arg, "-")) | |||
if err == nil { | |||
continue | |||
} | |||
// otherwise, we need to skip the next one too | |||
i += 1 | |||
continue | |||
} | |||
// append the actual arg | |||
args = append(args, arg) | |||
} | |||
var subCommand string | |||
var actualArgs []string | |||
if len(args) > 1 { | |||
subCommand = args[1] | |||
} | |||
if len(args) > 2 { | |||
actualArgs = args[2:] | |||
} | |||
cmd.Use = subCommand // for later print statements ... | |||
switch strings.ToLower(subCommand) { | |||
case "check_tx": | |||
return cmdCheckTx(cmd, actualArgs) | |||
case "commit": | |||
return cmdCommit(cmd, actualArgs) | |||
case "deliver_tx": | |||
return cmdDeliverTx(cmd, actualArgs) | |||
case "echo": | |||
return cmdEcho(cmd, actualArgs) | |||
case "info": | |||
return cmdInfo(cmd, actualArgs) | |||
case "query": | |||
return cmdQuery(cmd, actualArgs) | |||
case "set_option": | |||
return cmdSetOption(cmd, actualArgs) | |||
default: | |||
return cmdUnimplemented(cmd, pArgs) | |||
} | |||
} | |||
func cmdUnimplemented(cmd *cobra.Command, args []string) error { | |||
// TODO: Print out all the sub-commands available | |||
msg := "unimplemented command" | |||
if err := cmd.Help(); err != nil { | |||
msg = err.Error() | |||
} | |||
if len(args) > 0 { | |||
msg += fmt.Sprintf(" args: [%s]", strings.Join(args, " ")) | |||
} | |||
printResponse(cmd, args, response{ | |||
Code: codeBad, | |||
Log: msg, | |||
}) | |||
return nil | |||
} | |||
// Have the application echo a message | |||
func cmdEcho(cmd *cobra.Command, args []string) error { | |||
msg := "" | |||
if len(args) > 0 { | |||
msg = args[0] | |||
} | |||
res, err := client.EchoSync(msg) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Data: []byte(res.Message), | |||
}) | |||
return nil | |||
} | |||
// Get some info from the application | |||
func cmdInfo(cmd *cobra.Command, args []string) error { | |||
var version string | |||
if len(args) == 1 { | |||
version = args[0] | |||
} | |||
res, err := client.InfoSync(types.RequestInfo{version}) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Data: []byte(res.Data), | |||
}) | |||
return nil | |||
} | |||
const codeBad uint32 = 10 | |||
// Set an option on the application | |||
func cmdSetOption(cmd *cobra.Command, args []string) error { | |||
if len(args) < 2 { | |||
printResponse(cmd, args, response{ | |||
Code: codeBad, | |||
Log: "want at least arguments of the form: <key> <value>", | |||
}) | |||
return nil | |||
} | |||
key, val := args[0], args[1] | |||
_, err := client.SetOptionSync(types.RequestSetOption{key, val}) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{Log: "OK (SetOption doesn't return anything.)"}) // NOTE: Nothing to show... | |||
return nil | |||
} | |||
// Append a new tx to application | |||
func cmdDeliverTx(cmd *cobra.Command, args []string) error { | |||
if len(args) == 0 { | |||
printResponse(cmd, args, response{ | |||
Code: codeBad, | |||
Log: "want the tx", | |||
}) | |||
return nil | |||
} | |||
txBytes, err := stringOrHexToBytes(args[0]) | |||
if err != nil { | |||
return err | |||
} | |||
res, err := client.DeliverTxSync(txBytes) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Code: res.Code, | |||
Data: res.Data, | |||
Info: res.Info, | |||
Log: res.Log, | |||
}) | |||
return nil | |||
} | |||
// Validate a tx | |||
func cmdCheckTx(cmd *cobra.Command, args []string) error { | |||
if len(args) == 0 { | |||
printResponse(cmd, args, response{ | |||
Code: codeBad, | |||
Info: "want the tx", | |||
}) | |||
return nil | |||
} | |||
txBytes, err := stringOrHexToBytes(args[0]) | |||
if err != nil { | |||
return err | |||
} | |||
res, err := client.CheckTxSync(txBytes) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Code: res.Code, | |||
Data: res.Data, | |||
Info: res.Info, | |||
Log: res.Log, | |||
}) | |||
return nil | |||
} | |||
// Get application Merkle root hash | |||
func cmdCommit(cmd *cobra.Command, args []string) error { | |||
res, err := client.CommitSync() | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Data: res.Data, | |||
}) | |||
return nil | |||
} | |||
// Query application state | |||
func cmdQuery(cmd *cobra.Command, args []string) error { | |||
if len(args) == 0 { | |||
printResponse(cmd, args, response{ | |||
Code: codeBad, | |||
Info: "want the query", | |||
Log: "", | |||
}) | |||
return nil | |||
} | |||
queryBytes, err := stringOrHexToBytes(args[0]) | |||
if err != nil { | |||
return err | |||
} | |||
resQuery, err := client.QuerySync(types.RequestQuery{ | |||
Data: queryBytes, | |||
Path: flagPath, | |||
Height: int64(flagHeight), | |||
Prove: flagProve, | |||
}) | |||
if err != nil { | |||
return err | |||
} | |||
printResponse(cmd, args, response{ | |||
Code: resQuery.Code, | |||
Info: resQuery.Info, | |||
Log: resQuery.Log, | |||
Query: &queryResponse{ | |||
Key: resQuery.Key, | |||
Value: resQuery.Value, | |||
Height: resQuery.Height, | |||
Proof: resQuery.Proof, | |||
}, | |||
}) | |||
return nil | |||
} | |||
func cmdCounter(cmd *cobra.Command, args []string) error { | |||
app := counter.NewCounterApplication(flagSerial) | |||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | |||
// Start the listener | |||
srv, err := server.NewServer(flagAddress, flagAbci, app) | |||
if err != nil { | |||
return err | |||
} | |||
srv.SetLogger(logger.With("module", "abci-server")) | |||
if err := srv.Start(); err != nil { | |||
return err | |||
} | |||
// Wait forever | |||
cmn.TrapSignal(func() { | |||
// Cleanup | |||
srv.Stop() | |||
}) | |||
return nil | |||
} | |||
func cmdKVStore(cmd *cobra.Command, args []string) error { | |||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | |||
// Create the application - in memory or persisted to disk | |||
var app types.Application | |||
if flagPersist == "" { | |||
app = kvstore.NewKVStoreApplication() | |||
} else { | |||
app = kvstore.NewPersistentKVStoreApplication(flagPersist) | |||
app.(*kvstore.PersistentKVStoreApplication).SetLogger(logger.With("module", "kvstore")) | |||
} | |||
// Start the listener | |||
srv, err := server.NewServer(flagAddress, flagAbci, app) | |||
if err != nil { | |||
return err | |||
} | |||
srv.SetLogger(logger.With("module", "abci-server")) | |||
if err := srv.Start(); err != nil { | |||
return err | |||
} | |||
// Wait forever | |||
cmn.TrapSignal(func() { | |||
// Cleanup | |||
srv.Stop() | |||
}) | |||
return nil | |||
} | |||
//-------------------------------------------------------------------------------- | |||
func printResponse(cmd *cobra.Command, args []string, rsp response) { | |||
if flagVerbose { | |||
fmt.Println(">", cmd.Use, strings.Join(args, " ")) | |||
} | |||
// Always print the status code. | |||
if rsp.Code == types.CodeTypeOK { | |||
fmt.Printf("-> code: OK\n") | |||
} else { | |||
fmt.Printf("-> code: %d\n", rsp.Code) | |||
} | |||
if len(rsp.Data) != 0 { | |||
// Do no print this line when using the commit command | |||
// because the string comes out as gibberish | |||
if cmd.Use != "commit" { | |||
fmt.Printf("-> data: %s\n", rsp.Data) | |||
} | |||
fmt.Printf("-> data.hex: 0x%X\n", rsp.Data) | |||
} | |||
if rsp.Log != "" { | |||
fmt.Printf("-> log: %s\n", rsp.Log) | |||
} | |||
if rsp.Query != nil { | |||
fmt.Printf("-> height: %d\n", rsp.Query.Height) | |||
if rsp.Query.Key != nil { | |||
fmt.Printf("-> key: %s\n", rsp.Query.Key) | |||
fmt.Printf("-> key.hex: %X\n", rsp.Query.Key) | |||
} | |||
if rsp.Query.Value != nil { | |||
fmt.Printf("-> value: %s\n", rsp.Query.Value) | |||
fmt.Printf("-> value.hex: %X\n", rsp.Query.Value) | |||
} | |||
if rsp.Query.Proof != nil { | |||
fmt.Printf("-> proof: %X\n", rsp.Query.Proof) | |||
} | |||
} | |||
} | |||
// NOTE: s is interpreted as a string unless prefixed with 0x | |||
func stringOrHexToBytes(s string) ([]byte, error) { | |||
if len(s) > 2 && strings.ToLower(s[:2]) == "0x" { | |||
b, err := hex.DecodeString(s[2:]) | |||
if err != nil { | |||
err = fmt.Errorf("Error decoding hex argument: %s", err.Error()) | |||
return nil, err | |||
} | |||
return b, nil | |||
} | |||
if !strings.HasPrefix(s, "\"") || !strings.HasSuffix(s, "\"") { | |||
err := fmt.Errorf("Invalid string arg: \"%s\". Must be quoted or a \"0x\"-prefixed hex string", s) | |||
return nil, err | |||
} | |||
return []byte(s[1 : len(s)-1]), nil | |||
} |
@ -0,0 +1,14 @@ | |||
package main | |||
import ( | |||
"fmt" | |||
"os" | |||
) | |||
func main() { | |||
err := Execute() | |||
if err != nil { | |||
fmt.Print(err) | |||
os.Exit(1) | |||
} | |||
} |
@ -0,0 +1,9 @@ | |||
package code | |||
// Return codes for the examples | |||
const ( | |||
CodeTypeOK uint32 = 0 | |||
CodeTypeEncodingError uint32 = 1 | |||
CodeTypeBadNonce uint32 = 2 | |||
CodeTypeUnauthorized uint32 = 3 | |||
) |
@ -0,0 +1,104 @@ | |||
package counter | |||
import ( | |||
"encoding/binary" | |||
"fmt" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
type CounterApplication struct { | |||
types.BaseApplication | |||
hashCount int | |||
txCount int | |||
serial bool | |||
} | |||
func NewCounterApplication(serial bool) *CounterApplication { | |||
return &CounterApplication{serial: serial} | |||
} | |||
func (app *CounterApplication) Info(req types.RequestInfo) types.ResponseInfo { | |||
return types.ResponseInfo{Data: cmn.Fmt("{\"hashes\":%v,\"txs\":%v}", app.hashCount, app.txCount)} | |||
} | |||
func (app *CounterApplication) SetOption(req types.RequestSetOption) types.ResponseSetOption { | |||
key, value := req.Key, req.Value | |||
if key == "serial" && value == "on" { | |||
app.serial = true | |||
} else { | |||
/* | |||
TODO Panic and have the ABCI server pass an exception. | |||
The client can call SetOptionSync() and get an `error`. | |||
return types.ResponseSetOption{ | |||
Error: cmn.Fmt("Unknown key (%s) or value (%s)", key, value), | |||
} | |||
*/ | |||
return types.ResponseSetOption{} | |||
} | |||
return types.ResponseSetOption{} | |||
} | |||
func (app *CounterApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { | |||
if app.serial { | |||
if len(tx) > 8 { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} | |||
} | |||
tx8 := make([]byte, 8) | |||
copy(tx8[len(tx8)-len(tx):], tx) | |||
txValue := binary.BigEndian.Uint64(tx8) | |||
if txValue != uint64(app.txCount) { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeBadNonce, | |||
Log: fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.txCount, txValue)} | |||
} | |||
} | |||
app.txCount++ | |||
return types.ResponseDeliverTx{Code: code.CodeTypeOK} | |||
} | |||
func (app *CounterApplication) CheckTx(tx []byte) types.ResponseCheckTx { | |||
if app.serial { | |||
if len(tx) > 8 { | |||
return types.ResponseCheckTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} | |||
} | |||
tx8 := make([]byte, 8) | |||
copy(tx8[len(tx8)-len(tx):], tx) | |||
txValue := binary.BigEndian.Uint64(tx8) | |||
if txValue < uint64(app.txCount) { | |||
return types.ResponseCheckTx{ | |||
Code: code.CodeTypeBadNonce, | |||
Log: fmt.Sprintf("Invalid nonce. Expected >= %v, got %v", app.txCount, txValue)} | |||
} | |||
} | |||
return types.ResponseCheckTx{Code: code.CodeTypeOK} | |||
} | |||
func (app *CounterApplication) Commit() (resp types.ResponseCommit) { | |||
app.hashCount++ | |||
if app.txCount == 0 { | |||
return types.ResponseCommit{} | |||
} | |||
hash := make([]byte, 8) | |||
binary.BigEndian.PutUint64(hash, uint64(app.txCount)) | |||
return types.ResponseCommit{Data: hash} | |||
} | |||
func (app *CounterApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { | |||
switch reqQuery.Path { | |||
case "hash": | |||
return types.ResponseQuery{Value: []byte(cmn.Fmt("%v", app.hashCount))} | |||
case "tx": | |||
return types.ResponseQuery{Value: []byte(cmn.Fmt("%v", app.txCount))} | |||
default: | |||
return types.ResponseQuery{Log: cmn.Fmt("Invalid query path. Expected hash or tx, got %v", reqQuery.Path)} | |||
} | |||
} |
@ -0,0 +1,3 @@ | |||
package example | |||
// so the go tool doesn't return errors about no buildable go files ... |
@ -0,0 +1,154 @@ | |||
package example | |||
import ( | |||
"fmt" | |||
"net" | |||
"reflect" | |||
"testing" | |||
"time" | |||
"google.golang.org/grpc" | |||
"golang.org/x/net/context" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/tmlibs/log" | |||
abcicli "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/example/kvstore" | |||
abciserver "github.com/tendermint/tendermint/abci/server" | |||
"github.com/tendermint/tendermint/abci/types" | |||
) | |||
func TestKVStore(t *testing.T) { | |||
fmt.Println("### Testing KVStore") | |||
testStream(t, kvstore.NewKVStoreApplication()) | |||
} | |||
func TestBaseApp(t *testing.T) { | |||
fmt.Println("### Testing BaseApp") | |||
testStream(t, types.NewBaseApplication()) | |||
} | |||
func TestGRPC(t *testing.T) { | |||
fmt.Println("### Testing GRPC") | |||
testGRPCSync(t, types.NewGRPCApplication(types.NewBaseApplication())) | |||
} | |||
func testStream(t *testing.T, app types.Application) { | |||
numDeliverTxs := 200000 | |||
// Start the listener | |||
server := abciserver.NewSocketServer("unix://test.sock", app) | |||
server.SetLogger(log.TestingLogger().With("module", "abci-server")) | |||
if err := server.Start(); err != nil { | |||
t.Fatalf("Error starting socket server: %v", err.Error()) | |||
} | |||
defer server.Stop() | |||
// Connect to the socket | |||
client := abcicli.NewSocketClient("unix://test.sock", false) | |||
client.SetLogger(log.TestingLogger().With("module", "abci-client")) | |||
if err := client.Start(); err != nil { | |||
t.Fatalf("Error starting socket client: %v", err.Error()) | |||
} | |||
defer client.Stop() | |||
done := make(chan struct{}) | |||
counter := 0 | |||
client.SetResponseCallback(func(req *types.Request, res *types.Response) { | |||
// Process response | |||
switch r := res.Value.(type) { | |||
case *types.Response_DeliverTx: | |||
counter++ | |||
if r.DeliverTx.Code != code.CodeTypeOK { | |||
t.Error("DeliverTx failed with ret_code", r.DeliverTx.Code) | |||
} | |||
if counter > numDeliverTxs { | |||
t.Fatalf("Too many DeliverTx responses. Got %d, expected %d", counter, numDeliverTxs) | |||
} | |||
if counter == numDeliverTxs { | |||
go func() { | |||
time.Sleep(time.Second * 2) // Wait for a bit to allow counter overflow | |||
close(done) | |||
}() | |||
return | |||
} | |||
case *types.Response_Flush: | |||
// ignore | |||
default: | |||
t.Error("Unexpected response type", reflect.TypeOf(res.Value)) | |||
} | |||
}) | |||
// Write requests | |||
for counter := 0; counter < numDeliverTxs; counter++ { | |||
// Send request | |||
reqRes := client.DeliverTxAsync([]byte("test")) | |||
_ = reqRes | |||
// check err ? | |||
// Sometimes send flush messages | |||
if counter%123 == 0 { | |||
client.FlushAsync() | |||
// check err ? | |||
} | |||
} | |||
// Send final flush message | |||
client.FlushAsync() | |||
<-done | |||
} | |||
//------------------------- | |||
// test grpc | |||
func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { | |||
return cmn.Connect(addr) | |||
} | |||
func testGRPCSync(t *testing.T, app *types.GRPCApplication) { | |||
numDeliverTxs := 2000 | |||
// Start the listener | |||
server := abciserver.NewGRPCServer("unix://test.sock", app) | |||
server.SetLogger(log.TestingLogger().With("module", "abci-server")) | |||
if err := server.Start(); err != nil { | |||
t.Fatalf("Error starting GRPC server: %v", err.Error()) | |||
} | |||
defer server.Stop() | |||
// Connect to the socket | |||
conn, err := grpc.Dial("unix://test.sock", grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) | |||
if err != nil { | |||
t.Fatalf("Error dialing GRPC server: %v", err.Error()) | |||
} | |||
defer conn.Close() | |||
client := types.NewABCIApplicationClient(conn) | |||
// Write requests | |||
for counter := 0; counter < numDeliverTxs; counter++ { | |||
// Send request | |||
response, err := client.DeliverTx(context.Background(), &types.RequestDeliverTx{[]byte("test")}) | |||
if err != nil { | |||
t.Fatalf("Error in GRPC DeliverTx: %v", err.Error()) | |||
} | |||
counter++ | |||
if response.Code != code.CodeTypeOK { | |||
t.Error("DeliverTx failed with ret_code", response.Code) | |||
} | |||
if counter > numDeliverTxs { | |||
t.Fatal("Too many DeliverTx responses") | |||
} | |||
t.Log("response", counter) | |||
if counter == numDeliverTxs { | |||
go func() { | |||
time.Sleep(time.Second * 2) // Wait for a bit to allow counter overflow | |||
}() | |||
} | |||
} | |||
} |
@ -0,0 +1 @@ | |||
node_modules |
@ -0,0 +1 @@ | |||
This example has been moved here: https://github.com/tendermint/js-abci/tree/master/example |
@ -0,0 +1,31 @@ | |||
# KVStore | |||
There are two app's here: the KVStoreApplication and the PersistentKVStoreApplication. | |||
## KVStoreApplication | |||
The KVStoreApplication is a simple merkle key-value store. | |||
Transactions of the form `key=value` are stored as key-value pairs in the tree. | |||
Transactions without an `=` sign set the value to the key. | |||
The app has no replay protection (other than what the mempool provides). | |||
## PersistentKVStoreApplication | |||
The PersistentKVStoreApplication wraps the KVStoreApplication | |||
and provides two additional features: | |||
1) persistence of state across app restarts (using Tendermint's ABCI-Handshake mechanism) | |||
2) validator set changes | |||
The state is persisted in leveldb along with the last block committed, | |||
and the Handshake allows any necessary blocks to be replayed. | |||
Validator set changes are effected using the following transaction format: | |||
``` | |||
val:pubkey1/power1,addr2/power2,addr3/power3" | |||
``` | |||
where `power1` is the new voting power for the validator with `pubkey1` (possibly a new one). | |||
There is no sybil protection against new validators joining. | |||
Validators can be removed by setting their power to `0`. | |||
@ -0,0 +1,38 @@ | |||
package kvstore | |||
import ( | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
// RandVal creates one random validator, with a key derived | |||
// from the input value | |||
func RandVal(i int) types.Validator { | |||
addr := cmn.RandBytes(20) | |||
pubkey := cmn.RandBytes(32) | |||
power := cmn.RandUint16() + 1 | |||
v := types.Ed25519Validator(pubkey, int64(power)) | |||
v.Address = addr | |||
return v | |||
} | |||
// RandVals returns a list of cnt validators for initializing | |||
// the application. Note that the keys are deterministically | |||
// derived from the index in the array, while the power is | |||
// random (Change this if not desired) | |||
func RandVals(cnt int) []types.Validator { | |||
res := make([]types.Validator, cnt) | |||
for i := 0; i < cnt; i++ { | |||
res[i] = RandVal(i) | |||
} | |||
return res | |||
} | |||
// InitKVStore initializes the kvstore app with some data, | |||
// which allows tests to pass and is fine as long as you | |||
// don't make any tx that modify the validator state | |||
func InitKVStore(app *PersistentKVStoreApplication) { | |||
app.InitChain(types.RequestInitChain{ | |||
Validators: RandVals(1), | |||
}) | |||
} |
@ -0,0 +1,126 @@ | |||
package kvstore | |||
import ( | |||
"bytes" | |||
"encoding/binary" | |||
"encoding/json" | |||
"fmt" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
dbm "github.com/tendermint/tmlibs/db" | |||
) | |||
var ( | |||
stateKey = []byte("stateKey") | |||
kvPairPrefixKey = []byte("kvPairKey:") | |||
) | |||
type State struct { | |||
db dbm.DB | |||
Size int64 `json:"size"` | |||
Height int64 `json:"height"` | |||
AppHash []byte `json:"app_hash"` | |||
} | |||
func loadState(db dbm.DB) State { | |||
stateBytes := db.Get(stateKey) | |||
var state State | |||
if len(stateBytes) != 0 { | |||
err := json.Unmarshal(stateBytes, &state) | |||
if err != nil { | |||
panic(err) | |||
} | |||
} | |||
state.db = db | |||
return state | |||
} | |||
func saveState(state State) { | |||
stateBytes, err := json.Marshal(state) | |||
if err != nil { | |||
panic(err) | |||
} | |||
state.db.Set(stateKey, stateBytes) | |||
} | |||
func prefixKey(key []byte) []byte { | |||
return append(kvPairPrefixKey, key...) | |||
} | |||
//--------------------------------------------------- | |||
var _ types.Application = (*KVStoreApplication)(nil) | |||
type KVStoreApplication struct { | |||
types.BaseApplication | |||
state State | |||
} | |||
func NewKVStoreApplication() *KVStoreApplication { | |||
state := loadState(dbm.NewMemDB()) | |||
return &KVStoreApplication{state: state} | |||
} | |||
func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) { | |||
return types.ResponseInfo{Data: fmt.Sprintf("{\"size\":%v}", app.state.Size)} | |||
} | |||
// tx is either "key=value" or just arbitrary bytes | |||
func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { | |||
var key, value []byte | |||
parts := bytes.Split(tx, []byte("=")) | |||
if len(parts) == 2 { | |||
key, value = parts[0], parts[1] | |||
} else { | |||
key, value = tx, tx | |||
} | |||
app.state.db.Set(prefixKey(key), value) | |||
app.state.Size += 1 | |||
tags := []cmn.KVPair{ | |||
{[]byte("app.creator"), []byte("jae")}, | |||
{[]byte("app.key"), key}, | |||
} | |||
return types.ResponseDeliverTx{Code: code.CodeTypeOK, Tags: tags} | |||
} | |||
func (app *KVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { | |||
return types.ResponseCheckTx{Code: code.CodeTypeOK} | |||
} | |||
func (app *KVStoreApplication) Commit() types.ResponseCommit { | |||
// Using a memdb - just return the big endian size of the db | |||
appHash := make([]byte, 8) | |||
binary.PutVarint(appHash, app.state.Size) | |||
app.state.AppHash = appHash | |||
app.state.Height += 1 | |||
saveState(app.state) | |||
return types.ResponseCommit{Data: appHash} | |||
} | |||
func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { | |||
if reqQuery.Prove { | |||
value := app.state.db.Get(prefixKey(reqQuery.Data)) | |||
resQuery.Index = -1 // TODO make Proof return index | |||
resQuery.Key = reqQuery.Data | |||
resQuery.Value = value | |||
if value != nil { | |||
resQuery.Log = "exists" | |||
} else { | |||
resQuery.Log = "does not exist" | |||
} | |||
return | |||
} else { | |||
value := app.state.db.Get(prefixKey(reqQuery.Data)) | |||
resQuery.Value = value | |||
if value != nil { | |||
resQuery.Log = "exists" | |||
} else { | |||
resQuery.Log = "does not exist" | |||
} | |||
return | |||
} | |||
} |
@ -0,0 +1,310 @@ | |||
package kvstore | |||
import ( | |||
"bytes" | |||
"io/ioutil" | |||
"sort" | |||
"testing" | |||
"github.com/stretchr/testify/require" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/tmlibs/log" | |||
abcicli "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
abciserver "github.com/tendermint/tendermint/abci/server" | |||
"github.com/tendermint/tendermint/abci/types" | |||
) | |||
func testKVStore(t *testing.T, app types.Application, tx []byte, key, value string) { | |||
ar := app.DeliverTx(tx) | |||
require.False(t, ar.IsErr(), ar) | |||
// repeating tx doesn't raise error | |||
ar = app.DeliverTx(tx) | |||
require.False(t, ar.IsErr(), ar) | |||
// make sure query is fine | |||
resQuery := app.Query(types.RequestQuery{ | |||
Path: "/store", | |||
Data: []byte(key), | |||
}) | |||
require.Equal(t, code.CodeTypeOK, resQuery.Code) | |||
require.Equal(t, value, string(resQuery.Value)) | |||
// make sure proof is fine | |||
resQuery = app.Query(types.RequestQuery{ | |||
Path: "/store", | |||
Data: []byte(key), | |||
Prove: true, | |||
}) | |||
require.EqualValues(t, code.CodeTypeOK, resQuery.Code) | |||
require.Equal(t, value, string(resQuery.Value)) | |||
} | |||
func TestKVStoreKV(t *testing.T) { | |||
kvstore := NewKVStoreApplication() | |||
key := "abc" | |||
value := key | |||
tx := []byte(key) | |||
testKVStore(t, kvstore, tx, key, value) | |||
value = "def" | |||
tx = []byte(key + "=" + value) | |||
testKVStore(t, kvstore, tx, key, value) | |||
} | |||
func TestPersistentKVStoreKV(t *testing.T) { | |||
dir, err := ioutil.TempDir("/tmp", "abci-kvstore-test") // TODO | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
kvstore := NewPersistentKVStoreApplication(dir) | |||
key := "abc" | |||
value := key | |||
tx := []byte(key) | |||
testKVStore(t, kvstore, tx, key, value) | |||
value = "def" | |||
tx = []byte(key + "=" + value) | |||
testKVStore(t, kvstore, tx, key, value) | |||
} | |||
func TestPersistentKVStoreInfo(t *testing.T) { | |||
dir, err := ioutil.TempDir("/tmp", "abci-kvstore-test") // TODO | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
kvstore := NewPersistentKVStoreApplication(dir) | |||
InitKVStore(kvstore) | |||
height := int64(0) | |||
resInfo := kvstore.Info(types.RequestInfo{}) | |||
if resInfo.LastBlockHeight != height { | |||
t.Fatalf("expected height of %d, got %d", height, resInfo.LastBlockHeight) | |||
} | |||
// make and apply block | |||
height = int64(1) | |||
hash := []byte("foo") | |||
header := types.Header{ | |||
Height: int64(height), | |||
} | |||
kvstore.BeginBlock(types.RequestBeginBlock{hash, header, nil, nil}) | |||
kvstore.EndBlock(types.RequestEndBlock{header.Height}) | |||
kvstore.Commit() | |||
resInfo = kvstore.Info(types.RequestInfo{}) | |||
if resInfo.LastBlockHeight != height { | |||
t.Fatalf("expected height of %d, got %d", height, resInfo.LastBlockHeight) | |||
} | |||
} | |||
// add a validator, remove a validator, update a validator | |||
func TestValUpdates(t *testing.T) { | |||
dir, err := ioutil.TempDir("/tmp", "abci-kvstore-test") // TODO | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
kvstore := NewPersistentKVStoreApplication(dir) | |||
// init with some validators | |||
total := 10 | |||
nInit := 5 | |||
vals := RandVals(total) | |||
// iniitalize with the first nInit | |||
kvstore.InitChain(types.RequestInitChain{ | |||
Validators: vals[:nInit], | |||
}) | |||
vals1, vals2 := vals[:nInit], kvstore.Validators() | |||
valsEqual(t, vals1, vals2) | |||
var v1, v2, v3 types.Validator | |||
// add some validators | |||
v1, v2 = vals[nInit], vals[nInit+1] | |||
diff := []types.Validator{v1, v2} | |||
tx1 := MakeValSetChangeTx(v1.PubKey, v1.Power) | |||
tx2 := MakeValSetChangeTx(v2.PubKey, v2.Power) | |||
makeApplyBlock(t, kvstore, 1, diff, tx1, tx2) | |||
vals1, vals2 = vals[:nInit+2], kvstore.Validators() | |||
valsEqual(t, vals1, vals2) | |||
// remove some validators | |||
v1, v2, v3 = vals[nInit-2], vals[nInit-1], vals[nInit] | |||
v1.Power = 0 | |||
v2.Power = 0 | |||
v3.Power = 0 | |||
diff = []types.Validator{v1, v2, v3} | |||
tx1 = MakeValSetChangeTx(v1.PubKey, v1.Power) | |||
tx2 = MakeValSetChangeTx(v2.PubKey, v2.Power) | |||
tx3 := MakeValSetChangeTx(v3.PubKey, v3.Power) | |||
makeApplyBlock(t, kvstore, 2, diff, tx1, tx2, tx3) | |||
vals1 = append(vals[:nInit-2], vals[nInit+1]) | |||
vals2 = kvstore.Validators() | |||
valsEqual(t, vals1, vals2) | |||
// update some validators | |||
v1 = vals[0] | |||
if v1.Power == 5 { | |||
v1.Power = 6 | |||
} else { | |||
v1.Power = 5 | |||
} | |||
diff = []types.Validator{v1} | |||
tx1 = MakeValSetChangeTx(v1.PubKey, v1.Power) | |||
makeApplyBlock(t, kvstore, 3, diff, tx1) | |||
vals1 = append([]types.Validator{v1}, vals1[1:]...) | |||
vals2 = kvstore.Validators() | |||
valsEqual(t, vals1, vals2) | |||
} | |||
func makeApplyBlock(t *testing.T, kvstore types.Application, heightInt int, diff []types.Validator, txs ...[]byte) { | |||
// make and apply block | |||
height := int64(heightInt) | |||
hash := []byte("foo") | |||
header := types.Header{ | |||
Height: height, | |||
} | |||
kvstore.BeginBlock(types.RequestBeginBlock{hash, header, nil, nil}) | |||
for _, tx := range txs { | |||
if r := kvstore.DeliverTx(tx); r.IsErr() { | |||
t.Fatal(r) | |||
} | |||
} | |||
resEndBlock := kvstore.EndBlock(types.RequestEndBlock{header.Height}) | |||
kvstore.Commit() | |||
valsEqual(t, diff, resEndBlock.ValidatorUpdates) | |||
} | |||
// order doesn't matter | |||
func valsEqual(t *testing.T, vals1, vals2 []types.Validator) { | |||
if len(vals1) != len(vals2) { | |||
t.Fatalf("vals dont match in len. got %d, expected %d", len(vals2), len(vals1)) | |||
} | |||
sort.Sort(types.Validators(vals1)) | |||
sort.Sort(types.Validators(vals2)) | |||
for i, v1 := range vals1 { | |||
v2 := vals2[i] | |||
if !bytes.Equal(v1.PubKey.Data, v2.PubKey.Data) || | |||
v1.Power != v2.Power { | |||
t.Fatalf("vals dont match at index %d. got %X/%d , expected %X/%d", i, v2.PubKey, v2.Power, v1.PubKey, v1.Power) | |||
} | |||
} | |||
} | |||
func makeSocketClientServer(app types.Application, name string) (abcicli.Client, cmn.Service, error) { | |||
// Start the listener | |||
socket := cmn.Fmt("unix://%s.sock", name) | |||
logger := log.TestingLogger() | |||
server := abciserver.NewSocketServer(socket, app) | |||
server.SetLogger(logger.With("module", "abci-server")) | |||
if err := server.Start(); err != nil { | |||
return nil, nil, err | |||
} | |||
// Connect to the socket | |||
client := abcicli.NewSocketClient(socket, false) | |||
client.SetLogger(logger.With("module", "abci-client")) | |||
if err := client.Start(); err != nil { | |||
server.Stop() | |||
return nil, nil, err | |||
} | |||
return client, server, nil | |||
} | |||
func makeGRPCClientServer(app types.Application, name string) (abcicli.Client, cmn.Service, error) { | |||
// Start the listener | |||
socket := cmn.Fmt("unix://%s.sock", name) | |||
logger := log.TestingLogger() | |||
gapp := types.NewGRPCApplication(app) | |||
server := abciserver.NewGRPCServer(socket, gapp) | |||
server.SetLogger(logger.With("module", "abci-server")) | |||
if err := server.Start(); err != nil { | |||
return nil, nil, err | |||
} | |||
client := abcicli.NewGRPCClient(socket, true) | |||
client.SetLogger(logger.With("module", "abci-client")) | |||
if err := client.Start(); err != nil { | |||
server.Stop() | |||
return nil, nil, err | |||
} | |||
return client, server, nil | |||
} | |||
func TestClientServer(t *testing.T) { | |||
// set up socket app | |||
kvstore := NewKVStoreApplication() | |||
client, server, err := makeSocketClientServer(kvstore, "kvstore-socket") | |||
require.Nil(t, err) | |||
defer server.Stop() | |||
defer client.Stop() | |||
runClientTests(t, client) | |||
// set up grpc app | |||
kvstore = NewKVStoreApplication() | |||
gclient, gserver, err := makeGRPCClientServer(kvstore, "kvstore-grpc") | |||
require.Nil(t, err) | |||
defer gserver.Stop() | |||
defer gclient.Stop() | |||
runClientTests(t, gclient) | |||
} | |||
func runClientTests(t *testing.T, client abcicli.Client) { | |||
// run some tests.... | |||
key := "abc" | |||
value := key | |||
tx := []byte(key) | |||
testClient(t, client, tx, key, value) | |||
value = "def" | |||
tx = []byte(key + "=" + value) | |||
testClient(t, client, tx, key, value) | |||
} | |||
func testClient(t *testing.T, app abcicli.Client, tx []byte, key, value string) { | |||
ar, err := app.DeliverTxSync(tx) | |||
require.NoError(t, err) | |||
require.False(t, ar.IsErr(), ar) | |||
// repeating tx doesn't raise error | |||
ar, err = app.DeliverTxSync(tx) | |||
require.NoError(t, err) | |||
require.False(t, ar.IsErr(), ar) | |||
// make sure query is fine | |||
resQuery, err := app.QuerySync(types.RequestQuery{ | |||
Path: "/store", | |||
Data: []byte(key), | |||
}) | |||
require.Nil(t, err) | |||
require.Equal(t, code.CodeTypeOK, resQuery.Code) | |||
require.Equal(t, value, string(resQuery.Value)) | |||
// make sure proof is fine | |||
resQuery, err = app.QuerySync(types.RequestQuery{ | |||
Path: "/store", | |||
Data: []byte(key), | |||
Prove: true, | |||
}) | |||
require.Nil(t, err) | |||
require.Equal(t, code.CodeTypeOK, resQuery.Code) | |||
require.Equal(t, value, string(resQuery.Value)) | |||
} |
@ -0,0 +1,200 @@ | |||
package kvstore | |||
import ( | |||
"bytes" | |||
"encoding/hex" | |||
"fmt" | |||
"strconv" | |||
"strings" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
dbm "github.com/tendermint/tmlibs/db" | |||
"github.com/tendermint/tmlibs/log" | |||
) | |||
const ( | |||
ValidatorSetChangePrefix string = "val:" | |||
) | |||
//----------------------------------------- | |||
var _ types.Application = (*PersistentKVStoreApplication)(nil) | |||
type PersistentKVStoreApplication struct { | |||
app *KVStoreApplication | |||
// validator set | |||
ValUpdates []types.Validator | |||
logger log.Logger | |||
} | |||
func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication { | |||
name := "kvstore" | |||
db, err := dbm.NewGoLevelDB(name, dbDir) | |||
if err != nil { | |||
panic(err) | |||
} | |||
state := loadState(db) | |||
return &PersistentKVStoreApplication{ | |||
app: &KVStoreApplication{state: state}, | |||
logger: log.NewNopLogger(), | |||
} | |||
} | |||
func (app *PersistentKVStoreApplication) SetLogger(l log.Logger) { | |||
app.logger = l | |||
} | |||
func (app *PersistentKVStoreApplication) Info(req types.RequestInfo) types.ResponseInfo { | |||
res := app.app.Info(req) | |||
res.LastBlockHeight = app.app.state.Height | |||
res.LastBlockAppHash = app.app.state.AppHash | |||
return res | |||
} | |||
func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) types.ResponseSetOption { | |||
return app.app.SetOption(req) | |||
} | |||
// tx is either "val:pubkey/power" or "key=value" or just arbitrary bytes | |||
func (app *PersistentKVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { | |||
// if it starts with "val:", update the validator set | |||
// format is "val:pubkey/power" | |||
if isValidatorTx(tx) { | |||
// update validators in the merkle tree | |||
// and in app.ValUpdates | |||
return app.execValidatorTx(tx) | |||
} | |||
// otherwise, update the key-value store | |||
return app.app.DeliverTx(tx) | |||
} | |||
func (app *PersistentKVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { | |||
return app.app.CheckTx(tx) | |||
} | |||
// Commit will panic if InitChain was not called | |||
func (app *PersistentKVStoreApplication) Commit() types.ResponseCommit { | |||
return app.app.Commit() | |||
} | |||
func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { | |||
return app.app.Query(reqQuery) | |||
} | |||
// Save the validators in the merkle tree | |||
func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) types.ResponseInitChain { | |||
for _, v := range req.Validators { | |||
r := app.updateValidator(v) | |||
if r.IsErr() { | |||
app.logger.Error("Error updating validators", "r", r) | |||
} | |||
} | |||
return types.ResponseInitChain{} | |||
} | |||
// Track the block hash and header information | |||
func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { | |||
// reset valset changes | |||
app.ValUpdates = make([]types.Validator, 0) | |||
return types.ResponseBeginBlock{} | |||
} | |||
// Update the validator set | |||
func (app *PersistentKVStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { | |||
return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} | |||
} | |||
//--------------------------------------------- | |||
// update validators | |||
func (app *PersistentKVStoreApplication) Validators() (validators []types.Validator) { | |||
itr := app.app.state.db.Iterator(nil, nil) | |||
for ; itr.Valid(); itr.Next() { | |||
if isValidatorTx(itr.Key()) { | |||
validator := new(types.Validator) | |||
err := types.ReadMessage(bytes.NewBuffer(itr.Value()), validator) | |||
if err != nil { | |||
panic(err) | |||
} | |||
validators = append(validators, *validator) | |||
} | |||
} | |||
return | |||
} | |||
func MakeValSetChangeTx(pubkey types.PubKey, power int64) []byte { | |||
return []byte(cmn.Fmt("val:%X/%d", pubkey.Data, power)) | |||
} | |||
func isValidatorTx(tx []byte) bool { | |||
return strings.HasPrefix(string(tx), ValidatorSetChangePrefix) | |||
} | |||
// format is "val:pubkey/power" | |||
// pubkey is raw 32-byte ed25519 key | |||
func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.ResponseDeliverTx { | |||
tx = tx[len(ValidatorSetChangePrefix):] | |||
//get the pubkey and power | |||
pubKeyAndPower := strings.Split(string(tx), "/") | |||
if len(pubKeyAndPower) != 2 { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Expected 'pubkey/power'. Got %v", pubKeyAndPower)} | |||
} | |||
pubkeyS, powerS := pubKeyAndPower[0], pubKeyAndPower[1] | |||
// decode the pubkey | |||
pubkey, err := hex.DecodeString(pubkeyS) | |||
if err != nil { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Pubkey (%s) is invalid hex", pubkeyS)} | |||
} | |||
// decode the power | |||
power, err := strconv.ParseInt(powerS, 10, 64) | |||
if err != nil { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Power (%s) is not an int", powerS)} | |||
} | |||
// update | |||
return app.updateValidator(types.Ed25519Validator(pubkey, int64(power))) | |||
} | |||
// add, update, or remove a validator | |||
func (app *PersistentKVStoreApplication) updateValidator(v types.Validator) types.ResponseDeliverTx { | |||
key := []byte("val:" + string(v.PubKey.Data)) | |||
if v.Power == 0 { | |||
// remove validator | |||
if !app.app.state.db.Has(key) { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeUnauthorized, | |||
Log: fmt.Sprintf("Cannot remove non-existent validator %X", key)} | |||
} | |||
app.app.state.db.Delete(key) | |||
} else { | |||
// add or update validator | |||
value := bytes.NewBuffer(make([]byte, 0)) | |||
if err := types.WriteMessage(&v, value); err != nil { | |||
return types.ResponseDeliverTx{ | |||
Code: code.CodeTypeEncodingError, | |||
Log: fmt.Sprintf("Error encoding validator: %v", err)} | |||
} | |||
app.app.state.db.Set(key, value.Bytes()) | |||
} | |||
// we only update the changes array if we successfully updated the tree | |||
app.ValUpdates = append(app.ValUpdates, v) | |||
return types.ResponseDeliverTx{Code: code.CodeTypeOK} | |||
} |
@ -0,0 +1,50 @@ | |||
from wire import decode_string | |||
# map type_byte to message name | |||
message_types = { | |||
0x01: "echo", | |||
0x02: "flush", | |||
0x03: "info", | |||
0x04: "set_option", | |||
0x21: "deliver_tx", | |||
0x22: "check_tx", | |||
0x23: "commit", | |||
0x24: "add_listener", | |||
0x25: "rm_listener", | |||
} | |||
# return the decoded arguments of abci messages | |||
class RequestDecoder(): | |||
def __init__(self, reader): | |||
self.reader = reader | |||
def echo(self): | |||
return decode_string(self.reader) | |||
def flush(self): | |||
return | |||
def info(self): | |||
return | |||
def set_option(self): | |||
return decode_string(self.reader), decode_string(self.reader) | |||
def deliver_tx(self): | |||
return decode_string(self.reader) | |||
def check_tx(self): | |||
return decode_string(self.reader) | |||
def commit(self): | |||
return | |||
def add_listener(self): | |||
# TODO | |||
return | |||
def rm_listener(self): | |||
# TODO | |||
return |
@ -0,0 +1,56 @@ | |||
# Simple read() method around a bytearray | |||
class BytesBuffer(): | |||
def __init__(self, b): | |||
self.buf = b | |||
self.readCount = 0 | |||
def count(self): | |||
return self.readCount | |||
def reset_count(self): | |||
self.readCount = 0 | |||
def size(self): | |||
return len(self.buf) | |||
def peek(self): | |||
return self.buf[0] | |||
def write(self, b): | |||
# b should be castable to byte array | |||
self.buf += bytearray(b) | |||
def read(self, n): | |||
if len(self.buf) < n: | |||
print "reader err: buf less than n" | |||
# TODO: exception | |||
return | |||
self.readCount += n | |||
r = self.buf[:n] | |||
self.buf = self.buf[n:] | |||
return r | |||
# Buffer bytes off a tcp connection and read them off in chunks | |||
class ConnReader(): | |||
def __init__(self, conn): | |||
self.conn = conn | |||
self.buf = bytearray() | |||
# blocking | |||
def read(self, n): | |||
while n > len(self.buf): | |||
moreBuf = self.conn.recv(1024) | |||
if not moreBuf: | |||
raise IOError("dead connection") | |||
self.buf = self.buf + bytearray(moreBuf) | |||
r = self.buf[:n] | |||
self.buf = self.buf[n:] | |||
return r |
@ -0,0 +1,202 @@ | |||
import socket | |||
import select | |||
import sys | |||
from wire import decode_varint, encode | |||
from reader import BytesBuffer | |||
from msg import RequestDecoder, message_types | |||
# hold the asyncronous state of a connection | |||
# ie. we may not get enough bytes on one read to decode the message | |||
class Connection(): | |||
def __init__(self, fd, app): | |||
self.fd = fd | |||
self.app = app | |||
self.recBuf = BytesBuffer(bytearray()) | |||
self.resBuf = BytesBuffer(bytearray()) | |||
self.msgLength = 0 | |||
self.decoder = RequestDecoder(self.recBuf) | |||
self.inProgress = False # are we in the middle of a message | |||
def recv(this): | |||
data = this.fd.recv(1024) | |||
if not data: # what about len(data) == 0 | |||
raise IOError("dead connection") | |||
this.recBuf.write(data) | |||
# ABCI server responds to messges by calling methods on the app | |||
class ABCIServer(): | |||
def __init__(self, app, port=5410): | |||
self.app = app | |||
# map conn file descriptors to (app, reqBuf, resBuf, msgDecoder) | |||
self.appMap = {} | |||
self.port = port | |||
self.listen_backlog = 10 | |||
self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |||
self.listener.setblocking(0) | |||
self.listener.bind(('', port)) | |||
self.listener.listen(self.listen_backlog) | |||
self.shutdown = False | |||
self.read_list = [self.listener] | |||
self.write_list = [] | |||
def handle_new_connection(self, r): | |||
new_fd, new_addr = r.accept() | |||
new_fd.setblocking(0) # non-blocking | |||
self.read_list.append(new_fd) | |||
self.write_list.append(new_fd) | |||
print 'new connection to', new_addr | |||
self.appMap[new_fd] = Connection(new_fd, self.app) | |||
def handle_conn_closed(self, r): | |||
self.read_list.remove(r) | |||
self.write_list.remove(r) | |||
r.close() | |||
print "connection closed" | |||
def handle_recv(self, r): | |||
# app, recBuf, resBuf, conn | |||
conn = self.appMap[r] | |||
while True: | |||
try: | |||
print "recv loop" | |||
# check if we need more data first | |||
if conn.inProgress: | |||
if (conn.msgLength == 0 or conn.recBuf.size() < conn.msgLength): | |||
conn.recv() | |||
else: | |||
if conn.recBuf.size() == 0: | |||
conn.recv() | |||
conn.inProgress = True | |||
# see if we have enough to get the message length | |||
if conn.msgLength == 0: | |||
ll = conn.recBuf.peek() | |||
if conn.recBuf.size() < 1 + ll: | |||
# we don't have enough bytes to read the length yet | |||
return | |||
print "decoding msg length" | |||
conn.msgLength = decode_varint(conn.recBuf) | |||
# see if we have enough to decode the message | |||
if conn.recBuf.size() < conn.msgLength: | |||
return | |||
# now we can decode the message | |||
# first read the request type and get the particular msg | |||
# decoder | |||
typeByte = conn.recBuf.read(1) | |||
typeByte = int(typeByte[0]) | |||
resTypeByte = typeByte + 0x10 | |||
req_type = message_types[typeByte] | |||
if req_type == "flush": | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.fd.send(str(conn.resBuf.buf)) | |||
conn.msgLength = 0 | |||
conn.inProgress = False | |||
conn.resBuf = BytesBuffer(bytearray()) | |||
return | |||
decoder = getattr(conn.decoder, req_type) | |||
print "decoding args" | |||
req_args = decoder() | |||
print "got args", req_args | |||
# done decoding message | |||
conn.msgLength = 0 | |||
conn.inProgress = False | |||
req_f = getattr(conn.app, req_type) | |||
if req_args is None: | |||
res = req_f() | |||
elif isinstance(req_args, tuple): | |||
res = req_f(*req_args) | |||
else: | |||
res = req_f(req_args) | |||
if isinstance(res, tuple): | |||
res, ret_code = res | |||
else: | |||
ret_code = res | |||
res = None | |||
print "called", req_type, "ret code:", ret_code | |||
if ret_code != 0: | |||
print "non-zero retcode:", ret_code | |||
if req_type in ("echo", "info"): # these dont return a ret code | |||
enc = encode(res) | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(len(enc) + 1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.resBuf.write(enc) | |||
else: | |||
enc, encRet = encode(res), encode(ret_code) | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(len(enc) + len(encRet) + 1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.resBuf.write(encRet) | |||
conn.resBuf.write(enc) | |||
except TypeError as e: | |||
print "TypeError on reading from connection:", e | |||
self.handle_conn_closed(r) | |||
return | |||
except ValueError as e: | |||
print "ValueError on reading from connection:", e | |||
self.handle_conn_closed(r) | |||
return | |||
except IOError as e: | |||
print "IOError on reading from connection:", e | |||
self.handle_conn_closed(r) | |||
return | |||
except Exception as e: | |||
# sys.exc_info()[0] # TODO better | |||
print "error reading from connection", str(e) | |||
self.handle_conn_closed(r) | |||
return | |||
def main_loop(self): | |||
while not self.shutdown: | |||
r_list, w_list, _ = select.select( | |||
self.read_list, self.write_list, [], 2.5) | |||
for r in r_list: | |||
if (r == self.listener): | |||
try: | |||
self.handle_new_connection(r) | |||
# undo adding to read list ... | |||
except NameError as e: | |||
print "Could not connect due to NameError:", e | |||
except TypeError as e: | |||
print "Could not connect due to TypeError:", e | |||
except: | |||
print "Could not connect due to unexpected error:", sys.exc_info()[0] | |||
else: | |||
self.handle_recv(r) | |||
def handle_shutdown(self): | |||
for r in self.read_list: | |||
r.close() | |||
for w in self.write_list: | |||
try: | |||
w.close() | |||
except Exception as e: | |||
print(e) # TODO: add logging | |||
self.shutdown = True |
@ -0,0 +1,115 @@ | |||
# the decoder works off a reader | |||
# the encoder returns bytearray | |||
def hex2bytes(h): | |||
return bytearray(h.decode('hex')) | |||
def bytes2hex(b): | |||
if type(b) in (str, unicode): | |||
return "".join([hex(ord(c))[2:].zfill(2) for c in b]) | |||
else: | |||
return bytes2hex(b.decode()) | |||
# expects uvarint64 (no crazy big nums!) | |||
def uvarint_size(i): | |||
if i == 0: | |||
return 0 | |||
for j in xrange(1, 8): | |||
if i < 1 << j * 8: | |||
return j | |||
return 8 | |||
# expects i < 2**size | |||
def encode_big_endian(i, size): | |||
if size == 0: | |||
return bytearray() | |||
return encode_big_endian(i / 256, size - 1) + bytearray([i % 256]) | |||
def decode_big_endian(reader, size): | |||
if size == 0: | |||
return 0 | |||
firstByte = reader.read(1)[0] | |||
return firstByte * (256 ** (size - 1)) + decode_big_endian(reader, size - 1) | |||
# ints are max 16 bytes long | |||
def encode_varint(i): | |||
negate = False | |||
if i < 0: | |||
negate = True | |||
i = -i | |||
size = uvarint_size(i) | |||
if size == 0: | |||
return bytearray([0]) | |||
big_end = encode_big_endian(i, size) | |||
if negate: | |||
size += 0xF0 | |||
return bytearray([size]) + big_end | |||
# returns the int and whats left of the byte array | |||
def decode_varint(reader): | |||
size = reader.read(1)[0] | |||
if size == 0: | |||
return 0 | |||
negate = True if size > int(0xF0) else False | |||
if negate: | |||
size = size - 0xF0 | |||
i = decode_big_endian(reader, size) | |||
if negate: | |||
i = i * (-1) | |||
return i | |||
def encode_string(s): | |||
size = encode_varint(len(s)) | |||
return size + bytearray(s) | |||
def decode_string(reader): | |||
length = decode_varint(reader) | |||
return str(reader.read(length)) | |||
def encode_list(s): | |||
b = bytearray() | |||
map(b.extend, map(encode, s)) | |||
return encode_varint(len(s)) + b | |||
def encode(s): | |||
if s is None: | |||
return bytearray() | |||
if isinstance(s, int): | |||
return encode_varint(s) | |||
elif isinstance(s, str): | |||
return encode_string(s) | |||
elif isinstance(s, list): | |||
return encode_list(s) | |||
else: | |||
print "UNSUPPORTED TYPE!", type(s), s | |||
if __name__ == '__main__': | |||
ns = [100, 100, 1000, 256] | |||
ss = [2, 5, 5, 2] | |||
bs = map(encode_big_endian, ns, ss) | |||
ds = map(decode_big_endian, bs, ss) | |||
print ns | |||
print [i[0] for i in ds] | |||
ss = ["abc", "hi there jim", "ok now what"] | |||
e = map(encode_string, ss) | |||
d = map(decode_string, e) | |||
print ss | |||
print [i[0] for i in d] |
@ -0,0 +1,82 @@ | |||
import sys | |||
from abci.wire import hex2bytes, decode_big_endian, encode_big_endian | |||
from abci.server import ABCIServer | |||
from abci.reader import BytesBuffer | |||
class CounterApplication(): | |||
def __init__(self): | |||
sys.exit("The python example is out of date. Upgrading the Python examples is currently left as an exercise to you.") | |||
self.hashCount = 0 | |||
self.txCount = 0 | |||
self.serial = False | |||
def echo(self, msg): | |||
return msg, 0 | |||
def info(self): | |||
return ["hashes:%d, txs:%d" % (self.hashCount, self.txCount)], 0 | |||
def set_option(self, key, value): | |||
if key == "serial" and value == "on": | |||
self.serial = True | |||
return 0 | |||
def deliver_tx(self, txBytes): | |||
if self.serial: | |||
txByteArray = bytearray(txBytes) | |||
if len(txBytes) >= 2 and txBytes[:2] == "0x": | |||
txByteArray = hex2bytes(txBytes[2:]) | |||
txValue = decode_big_endian( | |||
BytesBuffer(txByteArray), len(txBytes)) | |||
if txValue != self.txCount: | |||
return None, 6 | |||
self.txCount += 1 | |||
return None, 0 | |||
def check_tx(self, txBytes): | |||
if self.serial: | |||
txByteArray = bytearray(txBytes) | |||
if len(txBytes) >= 2 and txBytes[:2] == "0x": | |||
txByteArray = hex2bytes(txBytes[2:]) | |||
txValue = decode_big_endian( | |||
BytesBuffer(txByteArray), len(txBytes)) | |||
if txValue < self.txCount: | |||
return 6 | |||
return 0 | |||
def commit(self): | |||
self.hashCount += 1 | |||
if self.txCount == 0: | |||
return "", 0 | |||
h = encode_big_endian(self.txCount, 8) | |||
h.reverse() | |||
return str(h), 0 | |||
def add_listener(self): | |||
return 0 | |||
def rm_listener(self): | |||
return 0 | |||
def event(self): | |||
return | |||
if __name__ == '__main__': | |||
l = len(sys.argv) | |||
if l == 1: | |||
port = 26658 | |||
elif l == 2: | |||
port = int(sys.argv[1]) | |||
else: | |||
print "too many arguments" | |||
quit() | |||
print 'ABCI Demo APP (Python)' | |||
app = CounterApplication() | |||
server = ABCIServer(app, port) | |||
server.main_loop() |
@ -0,0 +1,50 @@ | |||
from .wire import decode_string | |||
# map type_byte to message name | |||
message_types = { | |||
0x01: "echo", | |||
0x02: "flush", | |||
0x03: "info", | |||
0x04: "set_option", | |||
0x21: "deliver_tx", | |||
0x22: "check_tx", | |||
0x23: "commit", | |||
0x24: "add_listener", | |||
0x25: "rm_listener", | |||
} | |||
# return the decoded arguments of abci messages | |||
class RequestDecoder(): | |||
def __init__(self, reader): | |||
self.reader = reader | |||
def echo(self): | |||
return decode_string(self.reader) | |||
def flush(self): | |||
return | |||
def info(self): | |||
return | |||
def set_option(self): | |||
return decode_string(self.reader), decode_string(self.reader) | |||
def deliver_tx(self): | |||
return decode_string(self.reader) | |||
def check_tx(self): | |||
return decode_string(self.reader) | |||
def commit(self): | |||
return | |||
def add_listener(self): | |||
# TODO | |||
return | |||
def rm_listener(self): | |||
# TODO | |||
return |
@ -0,0 +1,56 @@ | |||
# Simple read() method around a bytearray | |||
class BytesBuffer(): | |||
def __init__(self, b): | |||
self.buf = b | |||
self.readCount = 0 | |||
def count(self): | |||
return self.readCount | |||
def reset_count(self): | |||
self.readCount = 0 | |||
def size(self): | |||
return len(self.buf) | |||
def peek(self): | |||
return self.buf[0] | |||
def write(self, b): | |||
# b should be castable to byte array | |||
self.buf += bytearray(b) | |||
def read(self, n): | |||
if len(self.buf) < n: | |||
print("reader err: buf less than n") | |||
# TODO: exception | |||
return | |||
self.readCount += n | |||
r = self.buf[:n] | |||
self.buf = self.buf[n:] | |||
return r | |||
# Buffer bytes off a tcp connection and read them off in chunks | |||
class ConnReader(): | |||
def __init__(self, conn): | |||
self.conn = conn | |||
self.buf = bytearray() | |||
# blocking | |||
def read(self, n): | |||
while n > len(self.buf): | |||
moreBuf = self.conn.recv(1024) | |||
if not moreBuf: | |||
raise IOError("dead connection") | |||
self.buf = self.buf + bytearray(moreBuf) | |||
r = self.buf[:n] | |||
self.buf = self.buf[n:] | |||
return r |
@ -0,0 +1,196 @@ | |||
import socket | |||
import select | |||
import sys | |||
import logging | |||
from .wire import decode_varint, encode | |||
from .reader import BytesBuffer | |||
from .msg import RequestDecoder, message_types | |||
# hold the asyncronous state of a connection | |||
# ie. we may not get enough bytes on one read to decode the message | |||
logger = logging.getLogger(__name__) | |||
class Connection(): | |||
def __init__(self, fd, app): | |||
self.fd = fd | |||
self.app = app | |||
self.recBuf = BytesBuffer(bytearray()) | |||
self.resBuf = BytesBuffer(bytearray()) | |||
self.msgLength = 0 | |||
self.decoder = RequestDecoder(self.recBuf) | |||
self.inProgress = False # are we in the middle of a message | |||
def recv(this): | |||
data = this.fd.recv(1024) | |||
if not data: # what about len(data) == 0 | |||
raise IOError("dead connection") | |||
this.recBuf.write(data) | |||
# ABCI server responds to messges by calling methods on the app | |||
class ABCIServer(): | |||
def __init__(self, app, port=5410): | |||
self.app = app | |||
# map conn file descriptors to (app, reqBuf, resBuf, msgDecoder) | |||
self.appMap = {} | |||
self.port = port | |||
self.listen_backlog = 10 | |||
self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |||
self.listener.setblocking(0) | |||
self.listener.bind(('', port)) | |||
self.listener.listen(self.listen_backlog) | |||
self.shutdown = False | |||
self.read_list = [self.listener] | |||
self.write_list = [] | |||
def handle_new_connection(self, r): | |||
new_fd, new_addr = r.accept() | |||
new_fd.setblocking(0) # non-blocking | |||
self.read_list.append(new_fd) | |||
self.write_list.append(new_fd) | |||
print('new connection to', new_addr) | |||
self.appMap[new_fd] = Connection(new_fd, self.app) | |||
def handle_conn_closed(self, r): | |||
self.read_list.remove(r) | |||
self.write_list.remove(r) | |||
r.close() | |||
print("connection closed") | |||
def handle_recv(self, r): | |||
# app, recBuf, resBuf, conn | |||
conn = self.appMap[r] | |||
while True: | |||
try: | |||
print("recv loop") | |||
# check if we need more data first | |||
if conn.inProgress: | |||
if (conn.msgLength == 0 or conn.recBuf.size() < conn.msgLength): | |||
conn.recv() | |||
else: | |||
if conn.recBuf.size() == 0: | |||
conn.recv() | |||
conn.inProgress = True | |||
# see if we have enough to get the message length | |||
if conn.msgLength == 0: | |||
ll = conn.recBuf.peek() | |||
if conn.recBuf.size() < 1 + ll: | |||
# we don't have enough bytes to read the length yet | |||
return | |||
print("decoding msg length") | |||
conn.msgLength = decode_varint(conn.recBuf) | |||
# see if we have enough to decode the message | |||
if conn.recBuf.size() < conn.msgLength: | |||
return | |||
# now we can decode the message | |||
# first read the request type and get the particular msg | |||
# decoder | |||
typeByte = conn.recBuf.read(1) | |||
typeByte = int(typeByte[0]) | |||
resTypeByte = typeByte + 0x10 | |||
req_type = message_types[typeByte] | |||
if req_type == "flush": | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.fd.send(conn.resBuf.buf) | |||
conn.msgLength = 0 | |||
conn.inProgress = False | |||
conn.resBuf = BytesBuffer(bytearray()) | |||
return | |||
decoder = getattr(conn.decoder, req_type) | |||
print("decoding args") | |||
req_args = decoder() | |||
print("got args", req_args) | |||
# done decoding message | |||
conn.msgLength = 0 | |||
conn.inProgress = False | |||
req_f = getattr(conn.app, req_type) | |||
if req_args is None: | |||
res = req_f() | |||
elif isinstance(req_args, tuple): | |||
res = req_f(*req_args) | |||
else: | |||
res = req_f(req_args) | |||
if isinstance(res, tuple): | |||
res, ret_code = res | |||
else: | |||
ret_code = res | |||
res = None | |||
print("called", req_type, "ret code:", ret_code, 'res:', res) | |||
if ret_code != 0: | |||
print("non-zero retcode:", ret_code) | |||
if req_type in ("echo", "info"): # these dont return a ret code | |||
enc = encode(res) | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(len(enc) + 1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.resBuf.write(enc) | |||
else: | |||
enc, encRet = encode(res), encode(ret_code) | |||
# messages are length prefixed | |||
conn.resBuf.write(encode(len(enc) + len(encRet) + 1)) | |||
conn.resBuf.write([resTypeByte]) | |||
conn.resBuf.write(encRet) | |||
conn.resBuf.write(enc) | |||
except IOError as e: | |||
print("IOError on reading from connection:", e) | |||
self.handle_conn_closed(r) | |||
return | |||
except Exception as e: | |||
logger.exception("error reading from connection") | |||
self.handle_conn_closed(r) | |||
return | |||
def main_loop(self): | |||
while not self.shutdown: | |||
r_list, w_list, _ = select.select( | |||
self.read_list, self.write_list, [], 2.5) | |||
for r in r_list: | |||
if (r == self.listener): | |||
try: | |||
self.handle_new_connection(r) | |||
# undo adding to read list ... | |||
except NameError as e: | |||
print("Could not connect due to NameError:", e) | |||
except TypeError as e: | |||
print("Could not connect due to TypeError:", e) | |||
except: | |||
print("Could not connect due to unexpected error:", sys.exc_info()[0]) | |||
else: | |||
self.handle_recv(r) | |||
def handle_shutdown(self): | |||
for r in self.read_list: | |||
r.close() | |||
for w in self.write_list: | |||
try: | |||
w.close() | |||
except Exception as e: | |||
print(e) # TODO: add logging | |||
self.shutdown = True |
@ -0,0 +1,119 @@ | |||
# the decoder works off a reader | |||
# the encoder returns bytearray | |||
def hex2bytes(h): | |||
return bytearray(h.decode('hex')) | |||
def bytes2hex(b): | |||
if type(b) in (str, str): | |||
return "".join([hex(ord(c))[2:].zfill(2) for c in b]) | |||
else: | |||
return bytes2hex(b.decode()) | |||
# expects uvarint64 (no crazy big nums!) | |||
def uvarint_size(i): | |||
if i == 0: | |||
return 0 | |||
for j in range(1, 8): | |||
if i < 1 << j * 8: | |||
return j | |||
return 8 | |||
# expects i < 2**size | |||
def encode_big_endian(i, size): | |||
if size == 0: | |||
return bytearray() | |||
return encode_big_endian(i // 256, size - 1) + bytearray([i % 256]) | |||
def decode_big_endian(reader, size): | |||
if size == 0: | |||
return 0 | |||
firstByte = reader.read(1)[0] | |||
return firstByte * (256 ** (size - 1)) + decode_big_endian(reader, size - 1) | |||
# ints are max 16 bytes long | |||
def encode_varint(i): | |||
negate = False | |||
if i < 0: | |||
negate = True | |||
i = -i | |||
size = uvarint_size(i) | |||
if size == 0: | |||
return bytearray([0]) | |||
big_end = encode_big_endian(i, size) | |||
if negate: | |||
size += 0xF0 | |||
return bytearray([size]) + big_end | |||
# returns the int and whats left of the byte array | |||
def decode_varint(reader): | |||
size = reader.read(1)[0] | |||
if size == 0: | |||
return 0 | |||
negate = True if size > int(0xF0) else False | |||
if negate: | |||
size = size - 0xF0 | |||
i = decode_big_endian(reader, size) | |||
if negate: | |||
i = i * (-1) | |||
return i | |||
def encode_string(s): | |||
size = encode_varint(len(s)) | |||
return size + bytearray(s, 'utf8') | |||
def decode_string(reader): | |||
length = decode_varint(reader) | |||
raw_data = reader.read(length) | |||
return raw_data.decode() | |||
def encode_list(s): | |||
b = bytearray() | |||
list(map(b.extend, list(map(encode, s)))) | |||
return encode_varint(len(s)) + b | |||
def encode(s): | |||
print('encoding', repr(s)) | |||
if s is None: | |||
return bytearray() | |||
if isinstance(s, int): | |||
return encode_varint(s) | |||
elif isinstance(s, str): | |||
return encode_string(s) | |||
elif isinstance(s, list): | |||
return encode_list(s) | |||
elif isinstance(s, bytearray): | |||
return encode_string(s) | |||
else: | |||
print("UNSUPPORTED TYPE!", type(s), s) | |||
if __name__ == '__main__': | |||
ns = [100, 100, 1000, 256] | |||
ss = [2, 5, 5, 2] | |||
bs = list(map(encode_big_endian, ns, ss)) | |||
ds = list(map(decode_big_endian, bs, ss)) | |||
print(ns) | |||
print([i[0] for i in ds]) | |||
ss = ["abc", "hi there jim", "ok now what"] | |||
e = list(map(encode_string, ss)) | |||
d = list(map(decode_string, e)) | |||
print(ss) | |||
print([i[0] for i in d]) |
@ -0,0 +1,82 @@ | |||
import sys | |||
from abci.wire import hex2bytes, decode_big_endian, encode_big_endian | |||
from abci.server import ABCIServer | |||
from abci.reader import BytesBuffer | |||
class CounterApplication(): | |||
def __init__(self): | |||
sys.exit("The python example is out of date. Upgrading the Python examples is currently left as an exercise to you.") | |||
self.hashCount = 0 | |||
self.txCount = 0 | |||
self.serial = False | |||
def echo(self, msg): | |||
return msg, 0 | |||
def info(self): | |||
return ["hashes:%d, txs:%d" % (self.hashCount, self.txCount)], 0 | |||
def set_option(self, key, value): | |||
if key == "serial" and value == "on": | |||
self.serial = True | |||
return 0 | |||
def deliver_tx(self, txBytes): | |||
if self.serial: | |||
txByteArray = bytearray(txBytes) | |||
if len(txBytes) >= 2 and txBytes[:2] == "0x": | |||
txByteArray = hex2bytes(txBytes[2:]) | |||
txValue = decode_big_endian( | |||
BytesBuffer(txByteArray), len(txBytes)) | |||
if txValue != self.txCount: | |||
return None, 6 | |||
self.txCount += 1 | |||
return None, 0 | |||
def check_tx(self, txBytes): | |||
if self.serial: | |||
txByteArray = bytearray(txBytes) | |||
if len(txBytes) >= 2 and txBytes[:2] == "0x": | |||
txByteArray = hex2bytes(txBytes[2:]) | |||
txValue = decode_big_endian( | |||
BytesBuffer(txByteArray), len(txBytes)) | |||
if txValue < self.txCount: | |||
return 6 | |||
return 0 | |||
def commit(self): | |||
self.hashCount += 1 | |||
if self.txCount == 0: | |||
return "", 0 | |||
h = encode_big_endian(self.txCount, 8) | |||
h.reverse() | |||
return h.decode(), 0 | |||
def add_listener(self): | |||
return 0 | |||
def rm_listener(self): | |||
return 0 | |||
def event(self): | |||
return | |||
if __name__ == '__main__': | |||
l = len(sys.argv) | |||
if l == 1: | |||
port = 26658 | |||
elif l == 2: | |||
port = int(sys.argv[1]) | |||
else: | |||
print("too many arguments") | |||
quit() | |||
print('ABCI Demo APP (Python)') | |||
app = CounterApplication() | |||
server = ABCIServer(app, port) | |||
server.main_loop() |
@ -0,0 +1,12 @@ | |||
FROM golang:1.9.2 | |||
RUN apt-get update && apt-get install -y --no-install-recommends \ | |||
zip \ | |||
&& rm -rf /var/lib/apt/lists/* | |||
# We want to ensure that release builds never have any cgo dependencies so we | |||
# switch that off at the highest level. | |||
ENV CGO_ENABLED 0 | |||
RUN mkdir -p $GOPATH/src/github.com/tendermint/abci | |||
WORKDIR $GOPATH/src/github.com/tendermint/abci |
@ -0,0 +1,52 @@ | |||
#!/usr/bin/env bash | |||
set -e | |||
REPO_NAME="abci" | |||
# 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" | |||
# Delete the old dir | |||
echo "==> Removing old directory..." | |||
rm -rf build/pkg | |||
mkdir -p build/pkg | |||
# Do a hermetic build inside a Docker container. | |||
docker build -t tendermint/${REPO_NAME}-builder scripts/${REPO_NAME}-builder/ | |||
docker run --rm -e "BUILD_TAGS=$BUILD_TAGS" -v "$(pwd)":/go/src/github.com/tendermint/${REPO_NAME} tendermint/${REPO_NAME}-builder ./scripts/dist_build.sh | |||
# Add $REPO_NAME 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/${REPO_NAME}_${VERSION}_${FILENAME}" | |||
done | |||
# Make the checksums. | |||
pushd ./build/dist | |||
shasum -a256 ./* > "./${REPO_NAME}_${VERSION}_SHA256SUMS" | |||
popd | |||
# Done | |||
echo | |||
echo "==> Results:" | |||
ls -hl ./build/dist | |||
exit 0 |
@ -0,0 +1,53 @@ | |||
#!/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/abci/version" | |||
# Determine the arch/os combos we're building for | |||
XC_ARCH=${XC_ARCH:-"386 amd64 arm"} | |||
XC_OS=${XC_OS:-"solaris darwin freebsd linux windows"} | |||
# Make sure build tools are available. | |||
make get_tools | |||
# Get VENDORED dependencies | |||
make get_vendor_deps | |||
BINARY="abci-cli" | |||
# 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}}/$BINARY" \ | |||
-tags="${BUILD_TAGS}" \ | |||
github.com/tendermint/abci/cmd/$BINARY | |||
# 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,7 @@ | |||
#! /bin/bash | |||
# 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 | |||
aws s3 cp --recursive build/dist s3://tendermint/binaries/abci/v${VERSION} --acl public-read |
@ -0,0 +1,57 @@ | |||
package server | |||
import ( | |||
"net" | |||
"google.golang.org/grpc" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
type GRPCServer struct { | |||
cmn.BaseService | |||
proto string | |||
addr string | |||
listener net.Listener | |||
server *grpc.Server | |||
app types.ABCIApplicationServer | |||
} | |||
// NewGRPCServer returns a new gRPC ABCI server | |||
func NewGRPCServer(protoAddr string, app types.ABCIApplicationServer) cmn.Service { | |||
proto, addr := cmn.ProtocolAndAddress(protoAddr) | |||
s := &GRPCServer{ | |||
proto: proto, | |||
addr: addr, | |||
listener: nil, | |||
app: app, | |||
} | |||
s.BaseService = *cmn.NewBaseService(nil, "ABCIServer", s) | |||
return s | |||
} | |||
// OnStart starts the gRPC service | |||
func (s *GRPCServer) OnStart() error { | |||
if err := s.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
ln, err := net.Listen(s.proto, s.addr) | |||
if err != nil { | |||
return err | |||
} | |||
s.Logger.Info("Listening", "proto", s.proto, "addr", s.addr) | |||
s.listener = ln | |||
s.server = grpc.NewServer() | |||
types.RegisterABCIApplicationServer(s.server, s.app) | |||
go s.server.Serve(s.listener) | |||
return nil | |||
} | |||
// OnStop stops the gRPC server | |||
func (s *GRPCServer) OnStop() { | |||
s.BaseService.OnStop() | |||
s.server.Stop() | |||
} |
@ -0,0 +1,31 @@ | |||
/* | |||
Package server is used to start a new ABCI server. | |||
It contains two server implementation: | |||
* gRPC server | |||
* socket server | |||
*/ | |||
package server | |||
import ( | |||
"fmt" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
func NewServer(protoAddr, transport string, app types.Application) (cmn.Service, error) { | |||
var s cmn.Service | |||
var err error | |||
switch transport { | |||
case "socket": | |||
s = NewSocketServer(protoAddr, app) | |||
case "grpc": | |||
s = NewGRPCServer(protoAddr, types.NewGRPCApplication(app)) | |||
default: | |||
err = fmt.Errorf("Unknown server type %s", transport) | |||
} | |||
return s, err | |||
} |
@ -0,0 +1,226 @@ | |||
package server | |||
import ( | |||
"bufio" | |||
"fmt" | |||
"io" | |||
"net" | |||
"sync" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
// var maxNumberConnections = 2 | |||
type SocketServer struct { | |||
cmn.BaseService | |||
proto string | |||
addr string | |||
listener net.Listener | |||
connsMtx sync.Mutex | |||
conns map[int]net.Conn | |||
nextConnID int | |||
appMtx sync.Mutex | |||
app types.Application | |||
} | |||
func NewSocketServer(protoAddr string, app types.Application) cmn.Service { | |||
proto, addr := cmn.ProtocolAndAddress(protoAddr) | |||
s := &SocketServer{ | |||
proto: proto, | |||
addr: addr, | |||
listener: nil, | |||
app: app, | |||
conns: make(map[int]net.Conn), | |||
} | |||
s.BaseService = *cmn.NewBaseService(nil, "ABCIServer", s) | |||
return s | |||
} | |||
func (s *SocketServer) OnStart() error { | |||
if err := s.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
ln, err := net.Listen(s.proto, s.addr) | |||
if err != nil { | |||
return err | |||
} | |||
s.listener = ln | |||
go s.acceptConnectionsRoutine() | |||
return nil | |||
} | |||
func (s *SocketServer) OnStop() { | |||
s.BaseService.OnStop() | |||
if err := s.listener.Close(); err != nil { | |||
s.Logger.Error("Error closing listener", "err", err) | |||
} | |||
s.connsMtx.Lock() | |||
defer s.connsMtx.Unlock() | |||
for id, conn := range s.conns { | |||
delete(s.conns, id) | |||
if err := conn.Close(); err != nil { | |||
s.Logger.Error("Error closing connection", "id", id, "conn", conn, "err", err) | |||
} | |||
} | |||
} | |||
func (s *SocketServer) addConn(conn net.Conn) int { | |||
s.connsMtx.Lock() | |||
defer s.connsMtx.Unlock() | |||
connID := s.nextConnID | |||
s.nextConnID++ | |||
s.conns[connID] = conn | |||
return connID | |||
} | |||
// deletes conn even if close errs | |||
func (s *SocketServer) rmConn(connID int) error { | |||
s.connsMtx.Lock() | |||
defer s.connsMtx.Unlock() | |||
conn, ok := s.conns[connID] | |||
if !ok { | |||
return fmt.Errorf("Connection %d does not exist", connID) | |||
} | |||
delete(s.conns, connID) | |||
return conn.Close() | |||
} | |||
func (s *SocketServer) acceptConnectionsRoutine() { | |||
for { | |||
// Accept a connection | |||
s.Logger.Info("Waiting for new connection...") | |||
conn, err := s.listener.Accept() | |||
if err != nil { | |||
if !s.IsRunning() { | |||
return // Ignore error from listener closing. | |||
} | |||
s.Logger.Error("Failed to accept connection: " + err.Error()) | |||
continue | |||
} | |||
s.Logger.Info("Accepted a new connection") | |||
connID := s.addConn(conn) | |||
closeConn := make(chan error, 2) // Push to signal connection closed | |||
responses := make(chan *types.Response, 1000) // A channel to buffer responses | |||
// Read requests from conn and deal with them | |||
go s.handleRequests(closeConn, conn, responses) | |||
// Pull responses from 'responses' and write them to conn. | |||
go s.handleResponses(closeConn, conn, responses) | |||
// Wait until signal to close connection | |||
go s.waitForClose(closeConn, connID) | |||
} | |||
} | |||
func (s *SocketServer) waitForClose(closeConn chan error, connID int) { | |||
err := <-closeConn | |||
if err == io.EOF { | |||
s.Logger.Error("Connection was closed by client") | |||
} else if err != nil { | |||
s.Logger.Error("Connection error", "error", err) | |||
} else { | |||
// never happens | |||
s.Logger.Error("Connection was closed.") | |||
} | |||
// Close the connection | |||
if err := s.rmConn(connID); err != nil { | |||
s.Logger.Error("Error in closing connection", "error", err) | |||
} | |||
} | |||
// Read requests from conn and deal with them | |||
func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, responses chan<- *types.Response) { | |||
var count int | |||
var bufReader = bufio.NewReader(conn) | |||
for { | |||
var req = &types.Request{} | |||
err := types.ReadMessage(bufReader, req) | |||
if err != nil { | |||
if err == io.EOF { | |||
closeConn <- err | |||
} else { | |||
closeConn <- fmt.Errorf("Error reading message: %v", err.Error()) | |||
} | |||
return | |||
} | |||
s.appMtx.Lock() | |||
count++ | |||
s.handleRequest(req, responses) | |||
s.appMtx.Unlock() | |||
} | |||
} | |||
func (s *SocketServer) handleRequest(req *types.Request, responses chan<- *types.Response) { | |||
switch r := req.Value.(type) { | |||
case *types.Request_Echo: | |||
responses <- types.ToResponseEcho(r.Echo.Message) | |||
case *types.Request_Flush: | |||
responses <- types.ToResponseFlush() | |||
case *types.Request_Info: | |||
res := s.app.Info(*r.Info) | |||
responses <- types.ToResponseInfo(res) | |||
case *types.Request_SetOption: | |||
res := s.app.SetOption(*r.SetOption) | |||
responses <- types.ToResponseSetOption(res) | |||
case *types.Request_DeliverTx: | |||
res := s.app.DeliverTx(r.DeliverTx.Tx) | |||
responses <- types.ToResponseDeliverTx(res) | |||
case *types.Request_CheckTx: | |||
res := s.app.CheckTx(r.CheckTx.Tx) | |||
responses <- types.ToResponseCheckTx(res) | |||
case *types.Request_Commit: | |||
res := s.app.Commit() | |||
responses <- types.ToResponseCommit(res) | |||
case *types.Request_Query: | |||
res := s.app.Query(*r.Query) | |||
responses <- types.ToResponseQuery(res) | |||
case *types.Request_InitChain: | |||
res := s.app.InitChain(*r.InitChain) | |||
responses <- types.ToResponseInitChain(res) | |||
case *types.Request_BeginBlock: | |||
res := s.app.BeginBlock(*r.BeginBlock) | |||
responses <- types.ToResponseBeginBlock(res) | |||
case *types.Request_EndBlock: | |||
res := s.app.EndBlock(*r.EndBlock) | |||
responses <- types.ToResponseEndBlock(res) | |||
default: | |||
responses <- types.ToResponseException("Unknown request") | |||
} | |||
} | |||
// Pull responses from 'responses' and write them to conn. | |||
func (s *SocketServer) handleResponses(closeConn chan error, conn net.Conn, responses <-chan *types.Response) { | |||
var count int | |||
var bufWriter = bufio.NewWriter(conn) | |||
for { | |||
var res = <-responses | |||
err := types.WriteMessage(res, bufWriter) | |||
if err != nil { | |||
closeConn <- fmt.Errorf("Error writing message: %v", err.Error()) | |||
return | |||
} | |||
if _, ok := res.Value.(*types.Response_Flush); ok { | |||
err = bufWriter.Flush() | |||
if err != nil { | |||
closeConn <- fmt.Errorf("Error flushing write buffer: %v", err.Error()) | |||
return | |||
} | |||
} | |||
count++ | |||
} | |||
} |
@ -0,0 +1,294 @@ | |||
ABCI Specification | |||
================== | |||
NOTE: this file has moved to `specification.md <./specification.md>`__. It is left to prevent link breakages for the forseable future. It can safely be deleted in a few months. | |||
Message Types | |||
~~~~~~~~~~~~~ | |||
ABCI requests/responses are defined as simple Protobuf messages in `this | |||
schema | |||
file <https://github.com/tendermint/abci/blob/master/types/types.proto>`__. | |||
TendermintCore sends the requests, and the ABCI application sends the | |||
responses. Here, we provide an overview of the messages types and how they | |||
are used by Tendermint. Then we describe each request-response pair as a | |||
function with arguments and return values, and add some notes on usage. | |||
Some messages (``Echo, Info, InitChain, BeginBlock, EndBlock, Commit``), don't | |||
return errors because an error would indicate a critical failure in the | |||
application and there's nothing Tendermint can do. The problem should be | |||
addressed and both Tendermint and the application restarted. All other | |||
messages (``SetOption, Query, CheckTx, DeliverTx``) return an | |||
application-specific response ``Code uint32``, where only ``0`` is reserved for | |||
``OK``. | |||
Some messages (``SetOption, Query, CheckTx, DeliverTx``) return | |||
non-deterministic data in the form of ``Info`` and ``Log``. The ``Log`` is | |||
intended for the literal output from the application's logger, while the | |||
``Info`` is any additional info that should be returned. | |||
The first time a new blockchain is started, Tendermint calls ``InitChain``. | |||
From then on, the Block Execution Sequence that causes the committed state to | |||
be updated is as follows: | |||
``BeginBlock, [DeliverTx], EndBlock, Commit`` | |||
where one ``DeliverTx`` is called for each transaction in the block. | |||
Cryptographic commitments to the results of DeliverTx, EndBlock, and | |||
Commit are included in the header of the next block. | |||
Tendermint opens three connections to the application to handle the different message | |||
types: | |||
- ``Consensus Connection - InitChain, BeginBlock, DeliverTx, EndBlock, Commit`` | |||
- ``Mempool Connection - CheckTx`` | |||
- ``Info Connection - Info, SetOption, Query`` | |||
The ``Flush`` message is used on every connection, and the ``Echo`` message | |||
is only used for debugging. | |||
Note that messages may be sent concurrently across all connections - | |||
a typical application will thus maintain a distinct state for each | |||
connection. They may be referred to as the ``DeliverTx state``, the | |||
``CheckTx state``, and the ``Commit state`` respectively. | |||
See below for more details on the message types and how they are used. | |||
Echo | |||
^^^^ | |||
- **Arguments**: | |||
- ``Message (string)``: A string to echo back | |||
- **Returns**: | |||
- ``Message (string)``: The input string | |||
- **Usage**: | |||
- Echo a string to test an abci client/server implementation | |||
Flush | |||
^^^^^ | |||
- **Usage**: | |||
- Signals that messages queued on the client should be flushed to | |||
the server. It is called periodically by the client implementation | |||
to ensure asynchronous requests are actually sent, and is called | |||
immediately to make a synchronous request, which returns when the | |||
Flush response comes back. | |||
Info | |||
^^^^ | |||
- **Arguments**: | |||
- ``Version (string)``: The Tendermint version | |||
- **Returns**: | |||
- ``Data (string)``: Some arbitrary information | |||
- ``Version (Version)``: Version information | |||
- ``LastBlockHeight (int64)``: Latest block for which the app has | |||
called Commit | |||
- ``LastBlockAppHash ([]byte)``: Latest result of Commit | |||
- **Usage**: | |||
- Return information about the application state. | |||
- Used to sync Tendermint with the application during a handshake that | |||
happens on startup. | |||
- Tendermint expects ``LastBlockAppHash`` and ``LastBlockHeight`` to be | |||
updated during ``Commit``, ensuring that ``Commit`` is never called twice | |||
for the same block height. | |||
SetOption | |||
^^^^^^^^^ | |||
- **Arguments**: | |||
- ``Key (string)``: Key to set | |||
- ``Value (string)``: Value to set for key | |||
- **Returns**: | |||
- ``Code (uint32)``: Response code | |||
- ``Log (string)``: The output of the application's logger. May be non-deterministic. | |||
- ``Info (string)``: Additional information. May be non-deterministic. | |||
- **Usage**: | |||
- Set non-consensus critical application specific options. | |||
- e.g. Key="min-fee", Value="100fermion" could set the minimum fee required for CheckTx | |||
(but not DeliverTx - that would be consensus critical). | |||
InitChain | |||
^^^^^^^^^ | |||
- **Arguments**: | |||
- ``Validators ([]Validator)``: Initial genesis validators | |||
- ``AppStateBytes ([]byte)``: Serialized initial application state | |||
- **Usage**: | |||
- Called once upon genesis. | |||
Query | |||
^^^^^ | |||
- **Arguments**: | |||
- ``Data ([]byte)``: Raw query bytes. Can be used with or in lieu of | |||
Path. | |||
- ``Path (string)``: Path of request, like an HTTP GET path. Can be | |||
used with or in liue of Data. | |||
- Apps MUST interpret '/store' as a query by key on the underlying | |||
store. The key SHOULD be specified in the Data field. | |||
- Apps SHOULD allow queries over specific types like '/accounts/...' | |||
or '/votes/...' | |||
- ``Height (int64)``: The block height for which you want the query | |||
(default=0 returns data for the latest committed block). Note that | |||
this is the height of the block containing the application's | |||
Merkle root hash, which represents the state as it was after | |||
committing the block at Height-1 | |||
- ``Prove (bool)``: Return Merkle proof with response if possible | |||
- **Returns**: | |||
- ``Code (uint32)``: Response code. | |||
- ``Log (string)``: The output of the application's logger. May be non-deterministic. | |||
- ``Info (string)``: Additional information. May be non-deterministic. | |||
- ``Index (int64)``: The index of the key in the tree. | |||
- ``Key ([]byte)``: The key of the matching data. | |||
- ``Value ([]byte)``: The value of the matching data. | |||
- ``Proof ([]byte)``: Proof for the data, if requested. | |||
- ``Height (int64)``: The block height from which data was derived. | |||
Note that this is the height of the block containing the | |||
application's Merkle root hash, which represents the state as it | |||
was after committing the block at Height-1 | |||
- **Usage**: | |||
- Query for data from the application at current or past height. | |||
- Optionally return Merkle proof. | |||
BeginBlock | |||
^^^^^^^^^^ | |||
- **Arguments**: | |||
- ``Hash ([]byte)``: The block's hash. This can be derived from the | |||
block header. | |||
- ``Header (struct{})``: The block header | |||
- ``AbsentValidators ([]int32)``: List of indices of validators not | |||
included in the LastCommit | |||
- ``ByzantineValidators ([]Evidence)``: List of evidence of | |||
validators that acted maliciously | |||
- **Usage**: | |||
- Signals the beginning of a new block. Called prior to any DeliverTxs. | |||
- The header is expected to at least contain the Height. | |||
- The ``AbsentValidators`` and ``ByzantineValidators`` can be used to | |||
determine rewards and punishments for the validators. | |||
CheckTx | |||
^^^^^^^ | |||
- **Arguments**: | |||
- ``Tx ([]byte)``: The request transaction bytes | |||
- **Returns**: | |||
- ``Code (uint32)``: Response code | |||
- ``Data ([]byte)``: Result bytes, if any. | |||
- ``Log (string)``: The output of the application's logger. May be non-deterministic. | |||
- ``Info (string)``: Additional information. May be non-deterministic. | |||
- ``GasWanted (int64)``: Amount of gas request for transaction. | |||
- ``GasUsed (int64)``: Amount of gas consumed by transaction. | |||
- ``Tags ([]cmn.KVPair)``: Key-Value tags for filtering and indexing transactions (eg. by account). | |||
- ``Fee (cmn.KI64Pair)``: Fee paid for the transaction. | |||
- **Usage**: Validate a mempool transaction, prior to broadcasting or | |||
proposing. CheckTx should perform stateful but light-weight checks | |||
of the validity of the transaction (like checking signatures and account balances), | |||
but need not execute in full (like running a smart contract). | |||
Tendermint runs CheckTx and DeliverTx concurrently with eachother, | |||
though on distinct ABCI connections - the mempool connection and the consensus | |||
connection, respectively. | |||
The application should maintain a separate state to support CheckTx. | |||
This state can be reset to the latest committed state during ``Commit``, | |||
where Tendermint ensures the mempool is locked and not sending new ``CheckTx``. | |||
After ``Commit``, the mempool will rerun CheckTx on all remaining | |||
transactions, throwing out any that are no longer valid. | |||
Keys and values in Tags must be UTF-8 encoded strings (e.g. "account.owner": "Bob", "balance": "100.0", "date": "2018-01-02") | |||
DeliverTx | |||
^^^^^^^^^ | |||
- **Arguments**: | |||
- ``Tx ([]byte)``: The request transaction bytes. | |||
- **Returns**: | |||
- ``Code (uint32)``: Response code. | |||
- ``Data ([]byte)``: Result bytes, if any. | |||
- ``Log (string)``: The output of the application's logger. May be non-deterministic. | |||
- ``Info (string)``: Additional information. May be non-deterministic. | |||
- ``GasWanted (int64)``: Amount of gas requested for transaction. | |||
- ``GasUsed (int64)``: Amount of gas consumed by transaction. | |||
- ``Tags ([]cmn.KVPair)``: Key-Value tags for filtering and indexing transactions (eg. by account). | |||
- ``Fee (cmn.KI64Pair)``: Fee paid for the transaction. | |||
- **Usage**: | |||
- Deliver a transaction to be executed in full by the application. If the transaction is valid, | |||
returns CodeType.OK. | |||
- Keys and values in Tags must be UTF-8 encoded strings (e.g. "account.owner": "Bob", "balance": "100.0", "time": "2018-01-02T12:30:00Z") | |||
EndBlock | |||
^^^^^^^^ | |||
- **Arguments**: | |||
- ``Height (int64)``: Height of the block just executed. | |||
- **Returns**: | |||
- ``ValidatorUpdates ([]Validator)``: Changes to validator set (set | |||
voting power to 0 to remove). | |||
- ``ConsensusParamUpdates (ConsensusParams)``: Changes to | |||
consensus-critical time, size, and other parameters. | |||
- **Usage**: | |||
- Signals the end of a block. | |||
- Called prior to each Commit, after all transactions. | |||
- Validator set and consensus params are updated with the result. | |||
- Validator pubkeys are expected to be go-wire encoded. | |||
Commit | |||
^^^^^^ | |||
- **Returns**: | |||
- ``Data ([]byte)``: The Merkle root hash | |||
- **Usage**: | |||
- Persist the application state. | |||
- Return a Merkle root hash of the application state. | |||
- It's critical that all application instances return the same hash. If not, | |||
they will not be able to agree on the next block, because the hash is | |||
included in the next block! |
@ -0,0 +1 @@ | |||
package benchmarks |
@ -0,0 +1,55 @@ | |||
package main | |||
import ( | |||
"bufio" | |||
"fmt" | |||
"log" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
func main() { | |||
conn, err := cmn.Connect("unix://test.sock") | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
// Read a bunch of responses | |||
go func() { | |||
counter := 0 | |||
for { | |||
var res = &types.Response{} | |||
err := types.ReadMessage(conn, res) | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
counter++ | |||
if counter%1000 == 0 { | |||
fmt.Println("Read", counter) | |||
} | |||
} | |||
}() | |||
// Write a bunch of requests | |||
counter := 0 | |||
for i := 0; ; i++ { | |||
var bufWriter = bufio.NewWriter(conn) | |||
var req = types.ToRequestEcho("foobar") | |||
err := types.WriteMessage(req, bufWriter) | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
err = bufWriter.Flush() | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
counter++ | |||
if counter%1000 == 0 { | |||
fmt.Println("Write", counter) | |||
} | |||
} | |||
} |
@ -0,0 +1,69 @@ | |||
package main | |||
import ( | |||
"bufio" | |||
"fmt" | |||
"log" | |||
"net" | |||
"reflect" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
func main() { | |||
conn, err := cmn.Connect("unix://test.sock") | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
// Make a bunch of requests | |||
counter := 0 | |||
for i := 0; ; i++ { | |||
req := types.ToRequestEcho("foobar") | |||
_, err := makeRequest(conn, req) | |||
if err != nil { | |||
log.Fatal(err.Error()) | |||
} | |||
counter++ | |||
if counter%1000 == 0 { | |||
fmt.Println(counter) | |||
} | |||
} | |||
} | |||
func makeRequest(conn net.Conn, req *types.Request) (*types.Response, error) { | |||
var bufWriter = bufio.NewWriter(conn) | |||
// Write desired request | |||
err := types.WriteMessage(req, bufWriter) | |||
if err != nil { | |||
return nil, err | |||
} | |||
err = types.WriteMessage(types.ToRequestFlush(), bufWriter) | |||
if err != nil { | |||
return nil, err | |||
} | |||
err = bufWriter.Flush() | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Read desired response | |||
var res = &types.Response{} | |||
err = types.ReadMessage(conn, res) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var resFlush = &types.Response{} | |||
err = types.ReadMessage(conn, resFlush) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if _, ok := resFlush.Value.(*types.Response_Flush); !ok { | |||
return nil, fmt.Errorf("Expected flush response but got something else: %v", reflect.TypeOf(resFlush)) | |||
} | |||
return res, nil | |||
} |
@ -0,0 +1,27 @@ | |||
package tests | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
abciclient "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/example/kvstore" | |||
abciserver "github.com/tendermint/tendermint/abci/server" | |||
) | |||
func TestClientServerNoAddrPrefix(t *testing.T) { | |||
addr := "localhost:26658" | |||
transport := "socket" | |||
app := kvstore.NewKVStoreApplication() | |||
server, err := abciserver.NewServer(addr, transport, app) | |||
assert.NoError(t, err, "expected no error on NewServer") | |||
err = server.Start() | |||
assert.NoError(t, err, "expected no error on server.Start") | |||
client, err := abciclient.NewClient(addr, transport, true) | |||
assert.NoError(t, err, "expected no error on NewClient") | |||
err = client.Start() | |||
assert.NoError(t, err, "expected no error on client.Start") | |||
} |
@ -0,0 +1,96 @@ | |||
package testsuite | |||
import ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
abcicli "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
func InitChain(client abcicli.Client) error { | |||
total := 10 | |||
vals := make([]types.Validator, total) | |||
for i := 0; i < total; i++ { | |||
pubkey := cmn.RandBytes(33) | |||
power := cmn.RandInt() | |||
vals[i] = types.Ed25519Validator(pubkey, int64(power)) | |||
} | |||
_, err := client.InitChainSync(types.RequestInitChain{ | |||
Validators: vals, | |||
}) | |||
if err != nil { | |||
fmt.Printf("Failed test: InitChain - %v\n", err) | |||
return err | |||
} | |||
fmt.Println("Passed test: InitChain") | |||
return nil | |||
} | |||
func SetOption(client abcicli.Client, key, value string) error { | |||
_, err := client.SetOptionSync(types.RequestSetOption{Key: key, Value: value}) | |||
if err != nil { | |||
fmt.Println("Failed test: SetOption") | |||
fmt.Printf("error while setting %v=%v: \nerror: %v\n", key, value, err) | |||
return err | |||
} | |||
fmt.Println("Passed test: SetOption") | |||
return nil | |||
} | |||
func Commit(client abcicli.Client, hashExp []byte) error { | |||
res, err := client.CommitSync() | |||
data := res.Data | |||
if err != nil { | |||
fmt.Println("Failed test: Commit") | |||
fmt.Printf("error while committing: %v\n", err) | |||
return err | |||
} | |||
if !bytes.Equal(data, hashExp) { | |||
fmt.Println("Failed test: Commit") | |||
fmt.Printf("Commit hash was unexpected. Got %X expected %X\n", data, hashExp) | |||
return errors.New("CommitTx failed") | |||
} | |||
fmt.Println("Passed test: Commit") | |||
return nil | |||
} | |||
func DeliverTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) error { | |||
res, _ := client.DeliverTxSync(txBytes) | |||
code, data, log := res.Code, res.Data, res.Log | |||
if code != codeExp { | |||
fmt.Println("Failed test: DeliverTx") | |||
fmt.Printf("DeliverTx response code was unexpected. Got %v expected %v. Log: %v\n", | |||
code, codeExp, log) | |||
return errors.New("DeliverTx error") | |||
} | |||
if !bytes.Equal(data, dataExp) { | |||
fmt.Println("Failed test: DeliverTx") | |||
fmt.Printf("DeliverTx response data was unexpected. Got %X expected %X\n", | |||
data, dataExp) | |||
return errors.New("DeliverTx error") | |||
} | |||
fmt.Println("Passed test: DeliverTx") | |||
return nil | |||
} | |||
func CheckTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) error { | |||
res, _ := client.CheckTxSync(txBytes) | |||
code, data, log := res.Code, res.Data, res.Log | |||
if code != codeExp { | |||
fmt.Println("Failed test: CheckTx") | |||
fmt.Printf("CheckTx response code was unexpected. Got %v expected %v. Log: %v\n", | |||
code, codeExp, log) | |||
return errors.New("CheckTx") | |||
} | |||
if !bytes.Equal(data, dataExp) { | |||
fmt.Println("Failed test: CheckTx") | |||
fmt.Printf("CheckTx response data was unexpected. Got %X expected %X\n", | |||
data, dataExp) | |||
return errors.New("CheckTx") | |||
} | |||
fmt.Println("Passed test: CheckTx") | |||
return nil | |||
} |
@ -0,0 +1,78 @@ | |||
package main | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"os" | |||
abcicli "github.com/tendermint/tendermint/abci/client" | |||
"github.com/tendermint/tendermint/abci/types" | |||
"github.com/tendermint/tmlibs/log" | |||
) | |||
func startClient(abciType string) abcicli.Client { | |||
// Start client | |||
client, err := abcicli.NewClient("tcp://127.0.0.1:26658", abciType, true) | |||
if err != nil { | |||
panic(err.Error()) | |||
} | |||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | |||
client.SetLogger(logger.With("module", "abcicli")) | |||
if err := client.Start(); err != nil { | |||
panicf("connecting to abci_app: %v", err.Error()) | |||
} | |||
return client | |||
} | |||
func setOption(client abcicli.Client, key, value string) { | |||
_, err := client.SetOptionSync(types.RequestSetOption{key, value}) | |||
if err != nil { | |||
panicf("setting %v=%v: \nerr: %v", key, value, err) | |||
} | |||
} | |||
func commit(client abcicli.Client, hashExp []byte) { | |||
res, err := client.CommitSync() | |||
if err != nil { | |||
panicf("client error: %v", err) | |||
} | |||
if !bytes.Equal(res.Data, hashExp) { | |||
panicf("Commit hash was unexpected. Got %X expected %X", res.Data, hashExp) | |||
} | |||
} | |||
func deliverTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) { | |||
res, err := client.DeliverTxSync(txBytes) | |||
if err != nil { | |||
panicf("client error: %v", err) | |||
} | |||
if res.Code != codeExp { | |||
panicf("DeliverTx response code was unexpected. Got %v expected %v. Log: %v", res.Code, codeExp, res.Log) | |||
} | |||
if !bytes.Equal(res.Data, dataExp) { | |||
panicf("DeliverTx response data was unexpected. Got %X expected %X", res.Data, dataExp) | |||
} | |||
} | |||
/*func checkTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) { | |||
res, err := client.CheckTxSync(txBytes) | |||
if err != nil { | |||
panicf("client error: %v", err) | |||
} | |||
if res.IsErr() { | |||
panicf("checking tx %X: %v\nlog: %v", txBytes, res.Log) | |||
} | |||
if res.Code != codeExp { | |||
panicf("CheckTx response code was unexpected. Got %v expected %v. Log: %v", | |||
res.Code, codeExp, res.Log) | |||
} | |||
if !bytes.Equal(res.Data, dataExp) { | |||
panicf("CheckTx response data was unexpected. Got %X expected %X", | |||
res.Data, dataExp) | |||
} | |||
}*/ | |||
func panicf(format string, a ...interface{}) { | |||
panic(fmt.Sprintf(format, a...)) | |||
} |
@ -0,0 +1,84 @@ | |||
package main | |||
import ( | |||
"fmt" | |||
"log" | |||
"os" | |||
"os/exec" | |||
"time" | |||
"github.com/tendermint/tendermint/abci/example/code" | |||
"github.com/tendermint/tendermint/abci/types" | |||
) | |||
var abciType string | |||
func init() { | |||
abciType = os.Getenv("ABCI") | |||
if abciType == "" { | |||
abciType = "socket" | |||
} | |||
} | |||
func main() { | |||
testCounter() | |||
} | |||
const ( | |||
maxABCIConnectTries = 10 | |||
) | |||
func ensureABCIIsUp(typ string, n int) error { | |||
var err error | |||
cmdString := "abci-cli echo hello" | |||
if typ == "grpc" { | |||
cmdString = "abci-cli --abci grpc echo hello" | |||
} | |||
for i := 0; i < n; i++ { | |||
cmd := exec.Command("bash", "-c", cmdString) // nolint: gas | |||
_, err = cmd.CombinedOutput() | |||
if err == nil { | |||
break | |||
} | |||
<-time.After(500 * time.Millisecond) | |||
} | |||
return err | |||
} | |||
func testCounter() { | |||
abciApp := os.Getenv("ABCI_APP") | |||
if abciApp == "" { | |||
panic("No ABCI_APP specified") | |||
} | |||
fmt.Printf("Running %s test with abci=%s\n", abciApp, abciType) | |||
cmd := exec.Command("bash", "-c", fmt.Sprintf("abci-cli %s", abciApp)) // nolint: gas | |||
cmd.Stdout = os.Stdout | |||
if err := cmd.Start(); err != nil { | |||
log.Fatalf("starting %q err: %v", abciApp, err) | |||
} | |||
defer cmd.Wait() | |||
defer cmd.Process.Kill() | |||
if err := ensureABCIIsUp(abciType, maxABCIConnectTries); err != nil { | |||
log.Fatalf("echo failed: %v", err) | |||
} | |||
client := startClient(abciType) | |||
defer client.Stop() | |||
setOption(client, "serial", "on") | |||
commit(client, nil) | |||
deliverTx(client, []byte("abc"), code.CodeTypeBadNonce, nil) | |||
commit(client, nil) | |||
deliverTx(client, []byte{0x00}, types.CodeTypeOK, nil) | |||
commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 1}) | |||
deliverTx(client, []byte{0x00}, code.CodeTypeBadNonce, nil) | |||
deliverTx(client, []byte{0x01}, types.CodeTypeOK, nil) | |||
deliverTx(client, []byte{0x00, 0x02}, types.CodeTypeOK, nil) | |||
deliverTx(client, []byte{0x00, 0x03}, types.CodeTypeOK, nil) | |||
deliverTx(client, []byte{0x00, 0x00, 0x04}, types.CodeTypeOK, nil) | |||
deliverTx(client, []byte{0x00, 0x00, 0x06}, code.CodeTypeBadNonce, nil) | |||
commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 5}) | |||
} |
@ -0,0 +1,27 @@ | |||
#! /bin/bash | |||
set -e | |||
# These tests spawn the counter app and server by execing the ABCI_APP command and run some simple client tests against it | |||
# Get the directory of where this script is. | |||
SOURCE="${BASH_SOURCE[0]}" | |||
while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done | |||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |||
# Change into that dir because we expect that. | |||
cd "$DIR" | |||
echo "RUN COUNTER OVER SOCKET" | |||
# test golang counter | |||
ABCI_APP="counter" go run ./*.go | |||
echo "----------------------" | |||
echo "RUN COUNTER OVER GRPC" | |||
# test golang counter via grpc | |||
ABCI_APP="counter --abci=grpc" ABCI="grpc" go run ./*.go | |||
echo "----------------------" | |||
# test nodejs counter | |||
# TODO: fix node app | |||
#ABCI_APP="node $GOPATH/src/github.com/tendermint/js-abci/example/app.js" go test -test.run TestCounter |
@ -0,0 +1,10 @@ | |||
echo hello | |||
info | |||
commit | |||
deliver_tx "abc" | |||
info | |||
commit | |||
query "abc" | |||
deliver_tx "def=xyz" | |||
commit | |||
query "def" |
@ -0,0 +1,47 @@ | |||
> echo hello | |||
-> code: OK | |||
-> data: hello | |||
-> data.hex: 0x68656C6C6F | |||
> info | |||
-> code: OK | |||
-> data: {"size":0} | |||
-> data.hex: 0x7B2273697A65223A307D | |||
> commit | |||
-> code: OK | |||
-> data.hex: 0x0000000000000000 | |||
> deliver_tx "abc" | |||
-> code: OK | |||
> info | |||
-> code: OK | |||
-> data: {"size":1} | |||
-> data.hex: 0x7B2273697A65223A317D | |||
> commit | |||
-> code: OK | |||
-> data.hex: 0x0200000000000000 | |||
> query "abc" | |||
-> code: OK | |||
-> log: exists | |||
-> height: 0 | |||
-> value: abc | |||
-> value.hex: 616263 | |||
> deliver_tx "def=xyz" | |||
-> code: OK | |||
> commit | |||
-> code: OK | |||
-> data.hex: 0x0400000000000000 | |||
> query "def" | |||
-> code: OK | |||
-> log: exists | |||
-> height: 0 | |||
-> value: xyz | |||
-> value.hex: 78797A | |||
@ -0,0 +1,8 @@ | |||
set_option serial on | |||
check_tx 0x00 | |||
check_tx 0xff | |||
deliver_tx 0x00 | |||
check_tx 0x00 | |||
deliver_tx 0x01 | |||
deliver_tx 0x04 | |||
info |
@ -0,0 +1,29 @@ | |||
> set_option serial on | |||
-> code: OK | |||
-> log: OK (SetOption doesn't return anything.) | |||
> check_tx 0x00 | |||
-> code: OK | |||
> check_tx 0xff | |||
-> code: OK | |||
> deliver_tx 0x00 | |||
-> code: OK | |||
> check_tx 0x00 | |||
-> code: 2 | |||
-> log: Invalid nonce. Expected >= 1, got 0 | |||
> deliver_tx 0x01 | |||
-> code: OK | |||
> deliver_tx 0x04 | |||
-> code: 2 | |||
-> log: Invalid nonce. Expected 2, got 4 | |||
> info | |||
-> code: OK | |||
-> data: {"hashes":0,"txs":2} | |||
-> data.hex: 0x7B22686173686573223A302C22747873223A327D | |||
@ -0,0 +1,42 @@ | |||
#! /bin/bash | |||
set -e | |||
# Get the root directory. | |||
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" || exit | |||
function testExample() { | |||
N=$1 | |||
INPUT=$2 | |||
APP="$3 $4" | |||
echo "Example $N: $APP" | |||
$APP &> /dev/null & | |||
sleep 2 | |||
abci-cli --log_level=error --verbose batch < "$INPUT" > "${INPUT}.out.new" | |||
killall "$3" | |||
pre=$(shasum < "${INPUT}.out") | |||
post=$(shasum < "${INPUT}.out.new") | |||
if [[ "$pre" != "$post" ]]; then | |||
echo "You broke the tutorial" | |||
echo "Got:" | |||
cat "${INPUT}.out.new" | |||
echo "Expected:" | |||
cat "${INPUT}.out" | |||
exit 1 | |||
fi | |||
rm "${INPUT}".out.new | |||
} | |||
testExample 1 tests/test_cli/ex1.abci abci-cli kvstore | |||
testExample 2 tests/test_cli/ex2.abci abci-cli counter | |||
echo "" | |||
echo "PASS" |
@ -0,0 +1,13 @@ | |||
#!/usr/bin/env bash | |||
set -e | |||
echo "" > coverage.txt | |||
echo "==> Running unit tests" | |||
for d in $(go list ./... | grep -v vendor); do | |||
go test -race -coverprofile=profile.out -covermode=atomic "$d" | |||
if [ -f profile.out ]; then | |||
cat profile.out >> coverage.txt | |||
rm profile.out | |||
fi | |||
done |
@ -0,0 +1 @@ | |||
package tests |
@ -0,0 +1,138 @@ | |||
package types // nolint: goimports | |||
import ( | |||
context "golang.org/x/net/context" | |||
) | |||
// Application is an interface that enables any finite, deterministic state machine | |||
// to be driven by a blockchain-based replication engine via the ABCI. | |||
// All methods take a RequestXxx argument and return a ResponseXxx argument, | |||
// except CheckTx/DeliverTx, which take `tx []byte`, and `Commit`, which takes nothing. | |||
type Application interface { | |||
// Info/Query Connection | |||
Info(RequestInfo) ResponseInfo // Return application info | |||
SetOption(RequestSetOption) ResponseSetOption // Set application option | |||
Query(RequestQuery) ResponseQuery // Query for state | |||
// Mempool Connection | |||
CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool | |||
// Consensus Connection | |||
InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore | |||
BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block | |||
DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing | |||
EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set | |||
Commit() ResponseCommit // Commit the state and return the application Merkle root hash | |||
} | |||
//------------------------------------------------------- | |||
// BaseApplication is a base form of Application | |||
var _ Application = (*BaseApplication)(nil) | |||
type BaseApplication struct { | |||
} | |||
func NewBaseApplication() *BaseApplication { | |||
return &BaseApplication{} | |||
} | |||
func (BaseApplication) Info(req RequestInfo) ResponseInfo { | |||
return ResponseInfo{} | |||
} | |||
func (BaseApplication) SetOption(req RequestSetOption) ResponseSetOption { | |||
return ResponseSetOption{} | |||
} | |||
func (BaseApplication) DeliverTx(tx []byte) ResponseDeliverTx { | |||
return ResponseDeliverTx{Code: CodeTypeOK} | |||
} | |||
func (BaseApplication) CheckTx(tx []byte) ResponseCheckTx { | |||
return ResponseCheckTx{Code: CodeTypeOK} | |||
} | |||
func (BaseApplication) Commit() ResponseCommit { | |||
return ResponseCommit{} | |||
} | |||
func (BaseApplication) Query(req RequestQuery) ResponseQuery { | |||
return ResponseQuery{Code: CodeTypeOK} | |||
} | |||
func (BaseApplication) InitChain(req RequestInitChain) ResponseInitChain { | |||
return ResponseInitChain{} | |||
} | |||
func (BaseApplication) BeginBlock(req RequestBeginBlock) ResponseBeginBlock { | |||
return ResponseBeginBlock{} | |||
} | |||
func (BaseApplication) EndBlock(req RequestEndBlock) ResponseEndBlock { | |||
return ResponseEndBlock{} | |||
} | |||
//------------------------------------------------------- | |||
// GRPCApplication is a GRPC wrapper for Application | |||
type GRPCApplication struct { | |||
app Application | |||
} | |||
func NewGRPCApplication(app Application) *GRPCApplication { | |||
return &GRPCApplication{app} | |||
} | |||
func (app *GRPCApplication) Echo(ctx context.Context, req *RequestEcho) (*ResponseEcho, error) { | |||
return &ResponseEcho{req.Message}, nil | |||
} | |||
func (app *GRPCApplication) Flush(ctx context.Context, req *RequestFlush) (*ResponseFlush, error) { | |||
return &ResponseFlush{}, nil | |||
} | |||
func (app *GRPCApplication) Info(ctx context.Context, req *RequestInfo) (*ResponseInfo, error) { | |||
res := app.app.Info(*req) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) SetOption(ctx context.Context, req *RequestSetOption) (*ResponseSetOption, error) { | |||
res := app.app.SetOption(*req) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) DeliverTx(ctx context.Context, req *RequestDeliverTx) (*ResponseDeliverTx, error) { | |||
res := app.app.DeliverTx(req.Tx) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) CheckTx(ctx context.Context, req *RequestCheckTx) (*ResponseCheckTx, error) { | |||
res := app.app.CheckTx(req.Tx) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) Query(ctx context.Context, req *RequestQuery) (*ResponseQuery, error) { | |||
res := app.app.Query(*req) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) Commit(ctx context.Context, req *RequestCommit) (*ResponseCommit, error) { | |||
res := app.app.Commit() | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) InitChain(ctx context.Context, req *RequestInitChain) (*ResponseInitChain, error) { | |||
res := app.app.InitChain(*req) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) BeginBlock(ctx context.Context, req *RequestBeginBlock) (*ResponseBeginBlock, error) { | |||
res := app.app.BeginBlock(*req) | |||
return &res, nil | |||
} | |||
func (app *GRPCApplication) EndBlock(ctx context.Context, req *RequestEndBlock) (*ResponseEndBlock, error) { | |||
res := app.app.EndBlock(*req) | |||
return &res, nil | |||
} |
@ -0,0 +1,210 @@ | |||
package types | |||
import ( | |||
"bufio" | |||
"encoding/binary" | |||
"io" | |||
"github.com/gogo/protobuf/proto" | |||
) | |||
const ( | |||
maxMsgSize = 104857600 // 100MB | |||
) | |||
// WriteMessage writes a varint length-delimited protobuf message. | |||
func WriteMessage(msg proto.Message, w io.Writer) error { | |||
bz, err := proto.Marshal(msg) | |||
if err != nil { | |||
return err | |||
} | |||
return encodeByteSlice(w, bz) | |||
} | |||
// ReadMessage reads a varint length-delimited protobuf message. | |||
func ReadMessage(r io.Reader, msg proto.Message) error { | |||
return readProtoMsg(r, msg, maxMsgSize) | |||
} | |||
func readProtoMsg(r io.Reader, msg proto.Message, maxSize int) error { | |||
// binary.ReadVarint takes an io.ByteReader, eg. a bufio.Reader | |||
reader, ok := r.(*bufio.Reader) | |||
if !ok { | |||
reader = bufio.NewReader(r) | |||
} | |||
length64, err := binary.ReadVarint(reader) | |||
if err != nil { | |||
return err | |||
} | |||
length := int(length64) | |||
if length < 0 || length > maxSize { | |||
return io.ErrShortBuffer | |||
} | |||
buf := make([]byte, length) | |||
if _, err := io.ReadFull(reader, buf); err != nil { | |||
return err | |||
} | |||
return proto.Unmarshal(buf, msg) | |||
} | |||
//----------------------------------------------------------------------- | |||
// NOTE: we copied wire.EncodeByteSlice from go-wire rather than keep | |||
// go-wire as a dep | |||
func encodeByteSlice(w io.Writer, bz []byte) (err error) { | |||
err = encodeVarint(w, int64(len(bz))) | |||
if err != nil { | |||
return | |||
} | |||
_, err = w.Write(bz) | |||
return | |||
} | |||
func encodeVarint(w io.Writer, i int64) (err error) { | |||
var buf [10]byte | |||
n := binary.PutVarint(buf[:], i) | |||
_, err = w.Write(buf[0:n]) | |||
return | |||
} | |||
//---------------------------------------- | |||
func ToRequestEcho(message string) *Request { | |||
return &Request{ | |||
Value: &Request_Echo{&RequestEcho{message}}, | |||
} | |||
} | |||
func ToRequestFlush() *Request { | |||
return &Request{ | |||
Value: &Request_Flush{&RequestFlush{}}, | |||
} | |||
} | |||
func ToRequestInfo(req RequestInfo) *Request { | |||
return &Request{ | |||
Value: &Request_Info{&req}, | |||
} | |||
} | |||
func ToRequestSetOption(req RequestSetOption) *Request { | |||
return &Request{ | |||
Value: &Request_SetOption{&req}, | |||
} | |||
} | |||
func ToRequestDeliverTx(tx []byte) *Request { | |||
return &Request{ | |||
Value: &Request_DeliverTx{&RequestDeliverTx{tx}}, | |||
} | |||
} | |||
func ToRequestCheckTx(tx []byte) *Request { | |||
return &Request{ | |||
Value: &Request_CheckTx{&RequestCheckTx{tx}}, | |||
} | |||
} | |||
func ToRequestCommit() *Request { | |||
return &Request{ | |||
Value: &Request_Commit{&RequestCommit{}}, | |||
} | |||
} | |||
func ToRequestQuery(req RequestQuery) *Request { | |||
return &Request{ | |||
Value: &Request_Query{&req}, | |||
} | |||
} | |||
func ToRequestInitChain(req RequestInitChain) *Request { | |||
return &Request{ | |||
Value: &Request_InitChain{&req}, | |||
} | |||
} | |||
func ToRequestBeginBlock(req RequestBeginBlock) *Request { | |||
return &Request{ | |||
Value: &Request_BeginBlock{&req}, | |||
} | |||
} | |||
func ToRequestEndBlock(req RequestEndBlock) *Request { | |||
return &Request{ | |||
Value: &Request_EndBlock{&req}, | |||
} | |||
} | |||
//---------------------------------------- | |||
func ToResponseException(errStr string) *Response { | |||
return &Response{ | |||
Value: &Response_Exception{&ResponseException{errStr}}, | |||
} | |||
} | |||
func ToResponseEcho(message string) *Response { | |||
return &Response{ | |||
Value: &Response_Echo{&ResponseEcho{message}}, | |||
} | |||
} | |||
func ToResponseFlush() *Response { | |||
return &Response{ | |||
Value: &Response_Flush{&ResponseFlush{}}, | |||
} | |||
} | |||
func ToResponseInfo(res ResponseInfo) *Response { | |||
return &Response{ | |||
Value: &Response_Info{&res}, | |||
} | |||
} | |||
func ToResponseSetOption(res ResponseSetOption) *Response { | |||
return &Response{ | |||
Value: &Response_SetOption{&res}, | |||
} | |||
} | |||
func ToResponseDeliverTx(res ResponseDeliverTx) *Response { | |||
return &Response{ | |||
Value: &Response_DeliverTx{&res}, | |||
} | |||
} | |||
func ToResponseCheckTx(res ResponseCheckTx) *Response { | |||
return &Response{ | |||
Value: &Response_CheckTx{&res}, | |||
} | |||
} | |||
func ToResponseCommit(res ResponseCommit) *Response { | |||
return &Response{ | |||
Value: &Response_Commit{&res}, | |||
} | |||
} | |||
func ToResponseQuery(res ResponseQuery) *Response { | |||
return &Response{ | |||
Value: &Response_Query{&res}, | |||
} | |||
} | |||
func ToResponseInitChain(res ResponseInitChain) *Response { | |||
return &Response{ | |||
Value: &Response_InitChain{&res}, | |||
} | |||
} | |||
func ToResponseBeginBlock(res ResponseBeginBlock) *Response { | |||
return &Response{ | |||
Value: &Response_BeginBlock{&res}, | |||
} | |||
} | |||
func ToResponseEndBlock(res ResponseEndBlock) *Response { | |||
return &Response{ | |||
Value: &Response_EndBlock{&res}, | |||
} | |||
} |
@ -0,0 +1,104 @@ | |||
package types | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"strings" | |||
"testing" | |||
"github.com/gogo/protobuf/proto" | |||
"github.com/stretchr/testify/assert" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
func TestMarshalJSON(t *testing.T) { | |||
b, err := json.Marshal(&ResponseDeliverTx{}) | |||
assert.Nil(t, err) | |||
// Do not include empty fields. | |||
assert.False(t, strings.Contains(string(b), "code")) | |||
r1 := ResponseCheckTx{ | |||
Code: 1, | |||
Data: []byte("hello"), | |||
GasWanted: 43, | |||
Tags: []cmn.KVPair{ | |||
{[]byte("pho"), []byte("bo")}, | |||
}, | |||
} | |||
b, err = json.Marshal(&r1) | |||
assert.Nil(t, err) | |||
var r2 ResponseCheckTx | |||
err = json.Unmarshal(b, &r2) | |||
assert.Nil(t, err) | |||
assert.Equal(t, r1, r2) | |||
} | |||
func TestWriteReadMessageSimple(t *testing.T) { | |||
cases := []proto.Message{ | |||
&RequestEcho{ | |||
Message: "Hello", | |||
}, | |||
} | |||
for _, c := range cases { | |||
buf := new(bytes.Buffer) | |||
err := WriteMessage(c, buf) | |||
assert.Nil(t, err) | |||
msg := new(RequestEcho) | |||
err = ReadMessage(buf, msg) | |||
assert.Nil(t, err) | |||
assert.Equal(t, c, msg) | |||
} | |||
} | |||
func TestWriteReadMessage(t *testing.T) { | |||
cases := []proto.Message{ | |||
&Header{ | |||
NumTxs: 4, | |||
}, | |||
// TODO: add the rest | |||
} | |||
for _, c := range cases { | |||
buf := new(bytes.Buffer) | |||
err := WriteMessage(c, buf) | |||
assert.Nil(t, err) | |||
msg := new(Header) | |||
err = ReadMessage(buf, msg) | |||
assert.Nil(t, err) | |||
assert.Equal(t, c, msg) | |||
} | |||
} | |||
func TestWriteReadMessage2(t *testing.T) { | |||
phrase := "hello-world" | |||
cases := []proto.Message{ | |||
&ResponseCheckTx{ | |||
Data: []byte(phrase), | |||
Log: phrase, | |||
GasWanted: 10, | |||
Tags: []cmn.KVPair{ | |||
cmn.KVPair{[]byte("abc"), []byte("def")}, | |||
}, | |||
// Fee: cmn.KI64Pair{ | |||
}, | |||
// TODO: add the rest | |||
} | |||
for _, c := range cases { | |||
buf := new(bytes.Buffer) | |||
err := WriteMessage(c, buf) | |||
assert.Nil(t, err) | |||
msg := new(ResponseCheckTx) | |||
err = ReadMessage(buf, msg) | |||
assert.Nil(t, err) | |||
assert.Equal(t, c, msg) | |||
} | |||
} |
@ -0,0 +1,55 @@ | |||
// +build ignore | |||
package main | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"io/ioutil" | |||
"os" | |||
"os/exec" | |||
"regexp" | |||
"strings" | |||
) | |||
// This script replaces most `[]byte` with `data.Bytes` in a `.pb.go` file. | |||
// It was written before we realized we could use `gogo/protobuf` to achieve | |||
// this more natively. So it's here for safe keeping in case we ever need to | |||
// abandon `gogo/protobuf`. | |||
func main() { | |||
bytePattern := regexp.MustCompile("[[][]]byte") | |||
const oldPath = "types/types.pb.go" | |||
const tmpPath = "types/types.pb.new" | |||
content, err := ioutil.ReadFile(oldPath) | |||
if err != nil { | |||
panic("cannot read " + oldPath) | |||
os.Exit(1) | |||
} | |||
lines := bytes.Split(content, []byte("\n")) | |||
outFile, _ := os.Create(tmpPath) | |||
wroteImport := false | |||
for _, line_bytes := range lines { | |||
line := string(line_bytes) | |||
gotPackageLine := strings.HasPrefix(line, "package ") | |||
writeImportTime := strings.HasPrefix(line, "import ") | |||
containsDescriptor := strings.Contains(line, "Descriptor") | |||
containsByteArray := strings.Contains(line, "[]byte") | |||
if containsByteArray && !containsDescriptor { | |||
line = string(bytePattern.ReplaceAll([]byte(line), []byte("data.Bytes"))) | |||
} | |||
if writeImportTime && !wroteImport { | |||
wroteImport = true | |||
fmt.Fprintf(outFile, "import \"github.com/tendermint/go-wire/data\"\n") | |||
} | |||
if gotPackageLine { | |||
fmt.Fprintf(outFile, "%s\n", "//nolint: gas") | |||
} | |||
fmt.Fprintf(outFile, "%s\n", line) | |||
} | |||
outFile.Close() | |||
os.Remove(oldPath) | |||
os.Rename(tmpPath, oldPath) | |||
exec.Command("goimports", "-w", oldPath) | |||
} |
@ -0,0 +1,16 @@ | |||
package types | |||
const ( | |||
PubKeyEd25519 = "ed25519" | |||
) | |||
func Ed25519Validator(pubkey []byte, power int64) Validator { | |||
return Validator{ | |||
// Address: | |||
PubKey: PubKey{ | |||
Type: PubKeyEd25519, | |||
Data: pubkey, | |||
}, | |||
Power: power, | |||
} | |||
} |
@ -0,0 +1,121 @@ | |||
package types | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"github.com/gogo/protobuf/jsonpb" | |||
) | |||
const ( | |||
CodeTypeOK uint32 = 0 | |||
) | |||
// IsOK returns true if Code is OK. | |||
func (r ResponseCheckTx) IsOK() bool { | |||
return r.Code == CodeTypeOK | |||
} | |||
// IsErr returns true if Code is something other than OK. | |||
func (r ResponseCheckTx) IsErr() bool { | |||
return r.Code != CodeTypeOK | |||
} | |||
// IsOK returns true if Code is OK. | |||
func (r ResponseDeliverTx) IsOK() bool { | |||
return r.Code == CodeTypeOK | |||
} | |||
// IsErr returns true if Code is something other than OK. | |||
func (r ResponseDeliverTx) IsErr() bool { | |||
return r.Code != CodeTypeOK | |||
} | |||
// IsOK returns true if Code is OK. | |||
func (r ResponseQuery) IsOK() bool { | |||
return r.Code == CodeTypeOK | |||
} | |||
// IsErr returns true if Code is something other than OK. | |||
func (r ResponseQuery) IsErr() bool { | |||
return r.Code != CodeTypeOK | |||
} | |||
//--------------------------------------------------------------------------- | |||
// override JSON marshalling so we dont emit defaults (ie. disable omitempty) | |||
// note we need Unmarshal functions too because protobuf had the bright idea | |||
// to marshal int64->string. cool. cool, cool, cool: https://developers.google.com/protocol-buffers/docs/proto3#json | |||
var ( | |||
jsonpbMarshaller = jsonpb.Marshaler{ | |||
EnumsAsInts: true, | |||
EmitDefaults: false, | |||
} | |||
jsonpbUnmarshaller = jsonpb.Unmarshaler{} | |||
) | |||
func (r *ResponseSetOption) MarshalJSON() ([]byte, error) { | |||
s, err := jsonpbMarshaller.MarshalToString(r) | |||
return []byte(s), err | |||
} | |||
func (r *ResponseSetOption) UnmarshalJSON(b []byte) error { | |||
reader := bytes.NewBuffer(b) | |||
return jsonpbUnmarshaller.Unmarshal(reader, r) | |||
} | |||
func (r *ResponseCheckTx) MarshalJSON() ([]byte, error) { | |||
s, err := jsonpbMarshaller.MarshalToString(r) | |||
return []byte(s), err | |||
} | |||
func (r *ResponseCheckTx) UnmarshalJSON(b []byte) error { | |||
reader := bytes.NewBuffer(b) | |||
return jsonpbUnmarshaller.Unmarshal(reader, r) | |||
} | |||
func (r *ResponseDeliverTx) MarshalJSON() ([]byte, error) { | |||
s, err := jsonpbMarshaller.MarshalToString(r) | |||
return []byte(s), err | |||
} | |||
func (r *ResponseDeliverTx) UnmarshalJSON(b []byte) error { | |||
reader := bytes.NewBuffer(b) | |||
return jsonpbUnmarshaller.Unmarshal(reader, r) | |||
} | |||
func (r *ResponseQuery) MarshalJSON() ([]byte, error) { | |||
s, err := jsonpbMarshaller.MarshalToString(r) | |||
return []byte(s), err | |||
} | |||
func (r *ResponseQuery) UnmarshalJSON(b []byte) error { | |||
reader := bytes.NewBuffer(b) | |||
return jsonpbUnmarshaller.Unmarshal(reader, r) | |||
} | |||
func (r *ResponseCommit) MarshalJSON() ([]byte, error) { | |||
s, err := jsonpbMarshaller.MarshalToString(r) | |||
return []byte(s), err | |||
} | |||
func (r *ResponseCommit) UnmarshalJSON(b []byte) error { | |||
reader := bytes.NewBuffer(b) | |||
return jsonpbUnmarshaller.Unmarshal(reader, r) | |||
} | |||
// Some compile time assertions to ensure we don't | |||
// have accidental runtime surprises later on. | |||
// jsonEncodingRoundTripper ensures that asserted | |||
// interfaces implement both MarshalJSON and UnmarshalJSON | |||
type jsonRoundTripper interface { | |||
json.Marshaler | |||
json.Unmarshaler | |||
} | |||
var _ jsonRoundTripper = (*ResponseCommit)(nil) | |||
var _ jsonRoundTripper = (*ResponseQuery)(nil) | |||
var _ jsonRoundTripper = (*ResponseDeliverTx)(nil) | |||
var _ jsonRoundTripper = (*ResponseCheckTx)(nil) | |||
var _ jsonRoundTripper = (*ResponseSetOption)(nil) |
@ -0,0 +1,282 @@ | |||
syntax = "proto3"; | |||
package types; | |||
// For more information on gogo.proto, see: | |||
// https://github.com/gogo/protobuf/blob/master/extensions.md | |||
import "github.com/gogo/protobuf/gogoproto/gogo.proto"; | |||
import "github.com/tendermint/tmlibs/common/types.proto"; | |||
// This file is copied from http://github.com/tendermint/abci | |||
// NOTE: When using custom types, mind the warnings. | |||
// https://github.com/gogo/protobuf/blob/master/custom_types.md#warnings-and-issues | |||
//---------------------------------------- | |||
// Request types | |||
message Request { | |||
oneof value { | |||
RequestEcho echo = 2; | |||
RequestFlush flush = 3; | |||
RequestInfo info = 4; | |||
RequestSetOption set_option = 5; | |||
RequestInitChain init_chain = 6; | |||
RequestQuery query = 7; | |||
RequestBeginBlock begin_block = 8; | |||
RequestCheckTx check_tx = 9; | |||
RequestDeliverTx deliver_tx = 19; | |||
RequestEndBlock end_block = 11; | |||
RequestCommit commit = 12; | |||
} | |||
} | |||
message RequestEcho { | |||
string message = 1; | |||
} | |||
message RequestFlush { | |||
} | |||
message RequestInfo { | |||
string version = 1; | |||
} | |||
// nondeterministic | |||
message RequestSetOption { | |||
string key = 1; | |||
string value = 2; | |||
} | |||
message RequestInitChain { | |||
int64 time = 1; | |||
string chain_id = 2; | |||
ConsensusParams consensus_params = 3; | |||
repeated Validator validators = 4 [(gogoproto.nullable)=false]; | |||
bytes app_state_bytes = 5; | |||
} | |||
message RequestQuery { | |||
bytes data = 1; | |||
string path = 2; | |||
int64 height = 3; | |||
bool prove = 4; | |||
} | |||
message RequestBeginBlock { | |||
bytes hash = 1; | |||
Header header = 2 [(gogoproto.nullable)=false]; | |||
repeated SigningValidator validators = 3 [(gogoproto.nullable)=false]; | |||
repeated Evidence byzantine_validators = 4 [(gogoproto.nullable)=false]; | |||
} | |||
message RequestCheckTx { | |||
bytes tx = 1; | |||
} | |||
message RequestDeliverTx { | |||
bytes tx = 1; | |||
} | |||
message RequestEndBlock { | |||
int64 height = 1; | |||
} | |||
message RequestCommit { | |||
} | |||
//---------------------------------------- | |||
// Response types | |||
message Response { | |||
oneof value { | |||
ResponseException exception = 1; | |||
ResponseEcho echo = 2; | |||
ResponseFlush flush = 3; | |||
ResponseInfo info = 4; | |||
ResponseSetOption set_option = 5; | |||
ResponseInitChain init_chain = 6; | |||
ResponseQuery query = 7; | |||
ResponseBeginBlock begin_block = 8; | |||
ResponseCheckTx check_tx = 9; | |||
ResponseDeliverTx deliver_tx = 10; | |||
ResponseEndBlock end_block = 11; | |||
ResponseCommit commit = 12; | |||
} | |||
} | |||
// nondeterministic | |||
message ResponseException { | |||
string error = 1; | |||
} | |||
message ResponseEcho { | |||
string message = 1; | |||
} | |||
message ResponseFlush { | |||
} | |||
message ResponseInfo { | |||
string data = 1; | |||
string version = 2; | |||
int64 last_block_height = 3; | |||
bytes last_block_app_hash = 4; | |||
} | |||
// nondeterministic | |||
message ResponseSetOption { | |||
uint32 code = 1; | |||
// bytes data = 2; | |||
string log = 3; | |||
string info = 4; | |||
} | |||
message ResponseInitChain { | |||
ConsensusParams consensus_params = 1; | |||
repeated Validator validators = 2 [(gogoproto.nullable)=false]; | |||
} | |||
message ResponseQuery { | |||
uint32 code = 1; | |||
// bytes data = 2; // use "value" instead. | |||
string log = 3; // nondeterministic | |||
string info = 4; // nondeterministic | |||
int64 index = 5; | |||
bytes key = 6; | |||
bytes value = 7; | |||
bytes proof = 8; | |||
int64 height = 9; | |||
} | |||
message ResponseBeginBlock { | |||
repeated common.KVPair tags = 1 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; | |||
} | |||
message ResponseCheckTx { | |||
uint32 code = 1; | |||
bytes data = 2; | |||
string log = 3; // nondeterministic | |||
string info = 4; // nondeterministic | |||
int64 gas_wanted = 5; | |||
int64 gas_used = 6; | |||
repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; | |||
common.KI64Pair fee = 8 [(gogoproto.nullable)=false]; | |||
} | |||
message ResponseDeliverTx { | |||
uint32 code = 1; | |||
bytes data = 2; | |||
string log = 3; // nondeterministic | |||
string info = 4; // nondeterministic | |||
int64 gas_wanted = 5; | |||
int64 gas_used = 6; | |||
repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; | |||
common.KI64Pair fee = 8 [(gogoproto.nullable)=false]; | |||
} | |||
message ResponseEndBlock { | |||
repeated Validator validator_updates = 1 [(gogoproto.nullable)=false]; | |||
ConsensusParams consensus_param_updates = 2; | |||
repeated common.KVPair tags = 3 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; | |||
} | |||
message ResponseCommit { | |||
// reserve 1 | |||
bytes data = 2; | |||
} | |||
//---------------------------------------- | |||
// Misc. | |||
// ConsensusParams contains all consensus-relevant parameters | |||
// that can be adjusted by the abci app | |||
message ConsensusParams { | |||
BlockSize block_size = 1; | |||
TxSize tx_size = 2; | |||
BlockGossip block_gossip = 3; | |||
} | |||
// BlockSize contain limits on the block size. | |||
message BlockSize { | |||
int32 max_bytes = 1; | |||
int32 max_txs = 2; | |||
int64 max_gas = 3; | |||
} | |||
// TxSize contain limits on the tx size. | |||
message TxSize { | |||
int32 max_bytes = 1; | |||
int64 max_gas = 2; | |||
} | |||
// BlockGossip determine consensus critical | |||
// elements of how blocks are gossiped | |||
message BlockGossip { | |||
// Note: must not be 0 | |||
int32 block_part_size_bytes = 1; | |||
} | |||
//---------------------------------------- | |||
// Blockchain Types | |||
// just the minimum the app might need | |||
message Header { | |||
// basics | |||
string chain_id = 1 [(gogoproto.customname)="ChainID"]; | |||
int64 height = 2; | |||
int64 time = 3; | |||
// txs | |||
int32 num_txs = 4; | |||
int64 total_txs = 5; | |||
// hashes | |||
bytes last_block_hash = 6; | |||
bytes validators_hash = 7; | |||
bytes app_hash = 8; | |||
// consensus | |||
Validator proposer = 9 [(gogoproto.nullable)=false]; | |||
} | |||
// Validator | |||
message Validator { | |||
bytes address = 1; | |||
PubKey pub_key = 2 [(gogoproto.nullable)=false]; | |||
int64 power = 3; | |||
} | |||
// Validator with an extra bool | |||
message SigningValidator { | |||
Validator validator = 1 [(gogoproto.nullable)=false]; | |||
bool signed_last_block = 2; | |||
} | |||
message PubKey { | |||
string type = 1; | |||
bytes data = 2; | |||
} | |||
message Evidence { | |||
string type = 1; | |||
Validator validator = 2 [(gogoproto.nullable)=false]; | |||
int64 height = 3; | |||
int64 time = 4; | |||
int64 total_voting_power = 5; | |||
} | |||
//---------------------------------------- | |||
// Service Definition | |||
service ABCIApplication { | |||
rpc Echo(RequestEcho) returns (ResponseEcho) ; | |||
rpc Flush(RequestFlush) returns (ResponseFlush); | |||
rpc Info(RequestInfo) returns (ResponseInfo); | |||
rpc SetOption(RequestSetOption) returns (ResponseSetOption); | |||
rpc DeliverTx(RequestDeliverTx) returns (ResponseDeliverTx); | |||
rpc CheckTx(RequestCheckTx) returns (ResponseCheckTx); | |||
rpc Query(RequestQuery) returns (ResponseQuery); | |||
rpc Commit(RequestCommit) returns (ResponseCommit); | |||
rpc InitChain(RequestInitChain) returns (ResponseInitChain); | |||
rpc BeginBlock(RequestBeginBlock) returns (ResponseBeginBlock); | |||
rpc EndBlock(RequestEndBlock) returns (ResponseEndBlock); | |||
} |
@ -0,0 +1,31 @@ | |||
package types | |||
import ( | |||
"testing" | |||
asrt "github.com/stretchr/testify/assert" | |||
) | |||
func TestConsensusParams(t *testing.T) { | |||
assert := asrt.New(t) | |||
params := &ConsensusParams{ | |||
BlockSize: &BlockSize{MaxGas: 12345}, | |||
BlockGossip: &BlockGossip{BlockPartSizeBytes: 54321}, | |||
} | |||
var noParams *ConsensusParams // nil | |||
// no error with nil fields | |||
assert.Nil(noParams.GetBlockSize()) | |||
assert.EqualValues(noParams.GetBlockSize().GetMaxGas(), 0) | |||
// get values with real fields | |||
assert.NotNil(params.GetBlockSize()) | |||
assert.EqualValues(params.GetBlockSize().GetMaxTxs(), 0) | |||
assert.EqualValues(params.GetBlockSize().GetMaxGas(), 12345) | |||
assert.NotNil(params.GetBlockGossip()) | |||
assert.EqualValues(params.GetBlockGossip().GetBlockPartSizeBytes(), 54321) | |||
assert.Nil(params.GetTxSize()) | |||
assert.EqualValues(params.GetTxSize().GetMaxBytes(), 0) | |||
} |
@ -0,0 +1,59 @@ | |||
package types | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"sort" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
//------------------------------------------------------------------------------ | |||
// Validators is a list of validators that implements the Sort interface | |||
type Validators []Validator | |||
var _ sort.Interface = (Validators)(nil) | |||
// All these methods for Validators: | |||
// Len, Less and Swap | |||
// are for Validators to implement sort.Interface | |||
// which will be used by the sort package. | |||
// See Issue https://github.com/tendermint/abci/issues/212 | |||
func (v Validators) Len() int { | |||
return len(v) | |||
} | |||
// XXX: doesn't distinguish same validator with different power | |||
func (v Validators) Less(i, j int) bool { | |||
return bytes.Compare(v[i].PubKey.Data, v[j].PubKey.Data) <= 0 | |||
} | |||
func (v Validators) Swap(i, j int) { | |||
v1 := v[i] | |||
v[i] = v[j] | |||
v[j] = v1 | |||
} | |||
func ValidatorsString(vs Validators) string { | |||
s := make([]validatorPretty, len(vs)) | |||
for i, v := range vs { | |||
s[i] = validatorPretty{ | |||
Address: v.Address, | |||
PubKey: v.PubKey.Data, | |||
Power: v.Power, | |||
} | |||
} | |||
b, err := json.Marshal(s) | |||
if err != nil { | |||
panic(err.Error()) | |||
} | |||
return string(b) | |||
} | |||
type validatorPretty struct { | |||
Address cmn.HexBytes `json:"address"` | |||
PubKey []byte `json:"pub_key"` | |||
Power int64 `json:"power"` | |||
} |
@ -0,0 +1,9 @@ | |||
package version | |||
// NOTE: we should probably be versioning the ABCI and the abci-cli separately | |||
const Maj = "0" | |||
const Min = "12" | |||
const Fix = "0" | |||
const Version = "0.12.0" |
@ -0,0 +1,324 @@ | |||
# ABCI Specification | |||
## Message Types | |||
ABCI requests/responses are defined as simple Protobuf messages in [this | |||
schema file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). | |||
TendermintCore sends the requests, and the ABCI application sends the | |||
responses. Here, we provide an overview of the messages types and how | |||
they are used by Tendermint. Then we describe each request-response pair | |||
as a function with arguments and return values, and add some notes on | |||
usage. | |||
Some messages (`Echo, Info, InitChain, BeginBlock, EndBlock, Commit`), | |||
don't return errors because an error would indicate a critical failure | |||
in the application and there's nothing Tendermint can do. The problem | |||
should be addressed and both Tendermint and the application restarted. | |||
All other messages (`SetOption, Query, CheckTx, DeliverTx`) return an | |||
application-specific response `Code uint32`, where only `0` is reserved | |||
for `OK`. | |||
Some messages (`SetOption, Query, CheckTx, DeliverTx`) return | |||
non-deterministic data in the form of `Info` and `Log`. The `Log` is | |||
intended for the literal output from the application's logger, while the | |||
`Info` is any additional info that should be returned. | |||
The first time a new blockchain is started, Tendermint calls | |||
`InitChain`. From then on, the Block Execution Sequence that causes the | |||
committed state to be updated is as follows: | |||
`BeginBlock, [DeliverTx], EndBlock, Commit` | |||
where one `DeliverTx` is called for each transaction in the block. | |||
Cryptographic commitments to the results of DeliverTx, EndBlock, and | |||
Commit are included in the header of the next block. | |||
Tendermint opens three connections to the application to handle the | |||
different message types: | |||
- `Consensus Connection - InitChain, BeginBlock, DeliverTx, EndBlock, Commit` | |||
- `Mempool Connection - CheckTx` | |||
- `Info Connection - Info, SetOption, Query` | |||
The `Flush` message is used on every connection, and the `Echo` message | |||
is only used for debugging. | |||
Note that messages may be sent concurrently across all connections -a | |||
typical application will thus maintain a distinct state for each | |||
connection. They may be referred to as the `DeliverTx state`, the | |||
`CheckTx state`, and the `Commit state` respectively. | |||
See below for more details on the message types and how they are used. | |||
## Request/Response Messages | |||
### Echo | |||
- **Request**: | |||
- `Message (string)`: A string to echo back | |||
- **Response**: | |||
- `Message (string)`: The input string | |||
- **Usage**: | |||
- Echo a string to test an abci client/server implementation | |||
### Flush | |||
- **Usage**: | |||
- Signals that messages queued on the client should be flushed to | |||
the server. It is called periodically by the client | |||
implementation to ensure asynchronous requests are actually | |||
sent, and is called immediately to make a synchronous request, | |||
which returns when the Flush response comes back. | |||
### Info | |||
- **Request**: | |||
- `Version (string)`: The Tendermint version | |||
- **Response**: | |||
- `Data (string)`: Some arbitrary information | |||
- `Version (Version)`: Version information | |||
- `LastBlockHeight (int64)`: Latest block for which the app has | |||
called Commit | |||
- `LastBlockAppHash ([]byte)`: Latest result of Commit | |||
- **Usage**: | |||
- Return information about the application state. | |||
- Used to sync Tendermint with the application during a handshake | |||
that happens on startup. | |||
- Tendermint expects `LastBlockAppHash` and `LastBlockHeight` to | |||
be updated during `Commit`, ensuring that `Commit` is never | |||
called twice for the same block height. | |||
### SetOption | |||
- **Request**: | |||
- `Key (string)`: Key to set | |||
- `Value (string)`: Value to set for key | |||
- **Response**: | |||
- `Code (uint32)`: Response code | |||
- `Log (string)`: The output of the application's logger. May | |||
be non-deterministic. | |||
- `Info (string)`: Additional information. May | |||
be non-deterministic. | |||
- **Usage**: | |||
- Set non-consensus critical application specific options. | |||
- e.g. Key="min-fee", Value="100fermion" could set the minimum fee | |||
required for CheckTx (but not DeliverTx - that would be | |||
consensus critical). | |||
### InitChain | |||
- **Request**: | |||
- `Validators ([]Validator)`: Initial genesis validators | |||
- `AppStateBytes ([]byte)`: Serialized initial application state | |||
- **Response**: | |||
- `ConsensusParams (ConsensusParams)`: Initial | |||
consensus-critical parameters. | |||
- `Validators ([]Validator)`: Initial validator set. | |||
- **Usage**: | |||
- Called once upon genesis. | |||
### Query | |||
- **Request**: | |||
- `Data ([]byte)`: Raw query bytes. Can be used with or in lieu | |||
of Path. | |||
- `Path (string)`: Path of request, like an HTTP GET path. Can be | |||
used with or in liue of Data. | |||
- Apps MUST interpret '/store' as a query by key on the | |||
underlying store. The key SHOULD be specified in the Data field. | |||
- Apps SHOULD allow queries over specific types like | |||
'/accounts/...' or '/votes/...' | |||
- `Height (int64)`: The block height for which you want the query | |||
(default=0 returns data for the latest committed block). Note | |||
that this is the height of the block containing the | |||
application's Merkle root hash, which represents the state as it | |||
was after committing the block at Height-1 | |||
- `Prove (bool)`: Return Merkle proof with response if possible | |||
- **Response**: | |||
- `Code (uint32)`: Response code. | |||
- `Log (string)`: The output of the application's logger. May | |||
be non-deterministic. | |||
- `Info (string)`: Additional information. May | |||
be non-deterministic. | |||
- `Index (int64)`: The index of the key in the tree. | |||
- `Key ([]byte)`: The key of the matching data. | |||
- `Value ([]byte)`: The value of the matching data. | |||
- `Proof ([]byte)`: Proof for the data, if requested. | |||
- `Height (int64)`: The block height from which data was derived. | |||
Note that this is the height of the block containing the | |||
application's Merkle root hash, which represents the state as it | |||
was after committing the block at Height-1 | |||
- **Usage**: | |||
- Query for data from the application at current or past height. | |||
- Optionally return Merkle proof. | |||
### BeginBlock | |||
- **Request**: | |||
- `Hash ([]byte)`: The block's hash. This can be derived from the | |||
block header. | |||
- `Header (struct{})`: The block header | |||
- `Validators ([]SigningValidator)`: List of validators in the current validator | |||
set and whether or not they signed a vote in the LastCommit | |||
- `ByzantineValidators ([]Evidence)`: List of evidence of | |||
validators that acted maliciously | |||
- **Response**: | |||
- `Tags ([]cmn.KVPair)`: Key-Value tags for filtering and indexing | |||
- **Usage**: | |||
- Signals the beginning of a new block. Called prior to | |||
any DeliverTxs. | |||
- The header is expected to at least contain the Height. | |||
- The `Validators` and `ByzantineValidators` can be used to | |||
determine rewards and punishments for the validators. | |||
### CheckTx | |||
- **Request**: | |||
- `Tx ([]byte)`: The request transaction bytes | |||
- **Response**: | |||
- `Code (uint32)`: Response code | |||
- `Data ([]byte)`: Result bytes, if any. | |||
- `Log (string)`: The output of the application's logger. May | |||
be non-deterministic. | |||
- `Info (string)`: Additional information. May | |||
be non-deterministic. | |||
- `GasWanted (int64)`: Amount of gas request for transaction. | |||
- `GasUsed (int64)`: Amount of gas consumed by transaction. | |||
- `Tags ([]cmn.KVPair)`: Key-Value tags for filtering and indexing | |||
transactions (eg. by account). | |||
- `Fee (cmn.KI64Pair)`: Fee paid for the transaction. | |||
- **Usage**: Validate a mempool transaction, prior to broadcasting | |||
or proposing. CheckTx should perform stateful but light-weight | |||
checks of the validity of the transaction (like checking signatures | |||
and account balances), but need not execute in full (like running a | |||
smart contract). | |||
Tendermint runs CheckTx and DeliverTx concurrently with eachother, | |||
though on distinct ABCI connections - the mempool connection and the | |||
consensus connection, respectively. | |||
The application should maintain a separate state to support CheckTx. | |||
This state can be reset to the latest committed state during | |||
`Commit`, where Tendermint ensures the mempool is locked and not | |||
sending new `CheckTx`. After `Commit`, the mempool will rerun | |||
CheckTx on all remaining transactions, throwing out any that are no | |||
longer valid. | |||
Keys and values in Tags must be UTF-8 encoded strings (e.g. | |||
"account.owner": "Bob", "balance": "100.0", "date": "2018-01-02") | |||
### DeliverTx | |||
- **Request**: | |||
- `Tx ([]byte)`: The request transaction bytes. | |||
- **Response**: | |||
- `Code (uint32)`: Response code. | |||
- `Data ([]byte)`: Result bytes, if any. | |||
- `Log (string)`: The output of the application's logger. May | |||
be non-deterministic. | |||
- `Info (string)`: Additional information. May | |||
be non-deterministic. | |||
- `GasWanted (int64)`: Amount of gas requested for transaction. | |||
- `GasUsed (int64)`: Amount of gas consumed by transaction. | |||
- `Tags ([]cmn.KVPair)`: Key-Value tags for filtering and indexing | |||
transactions (eg. by account). | |||
- `Fee (cmn.KI64Pair)`: Fee paid for the transaction. | |||
- **Usage**: | |||
- Deliver a transaction to be executed in full by the application. | |||
If the transaction is valid, returns CodeType.OK. | |||
- Keys and values in Tags must be UTF-8 encoded strings (e.g. | |||
"account.owner": "Bob", "balance": "100.0", | |||
"time": "2018-01-02T12:30:00Z") | |||
### EndBlock | |||
- **Request**: | |||
- `Height (int64)`: Height of the block just executed. | |||
- **Response**: | |||
- `ValidatorUpdates ([]Validator)`: Changes to validator set (set | |||
voting power to 0 to remove). | |||
- `ConsensusParamUpdates (ConsensusParams)`: Changes to | |||
consensus-critical time, size, and other parameters. | |||
- `Tags ([]cmn.KVPair)`: Key-Value tags for filtering and indexing | |||
- **Usage**: | |||
- Signals the end of a block. | |||
- Called prior to each Commit, after all transactions. | |||
- Validator set and consensus params are updated with the result. | |||
- Validator pubkeys are expected to be go-wire encoded. | |||
### Commit | |||
- **Response**: | |||
- `Data ([]byte)`: The Merkle root hash | |||
- **Usage**: | |||
- Persist the application state. | |||
- Return a Merkle root hash of the application state. | |||
- It's critical that all application instances return the | |||
same hash. If not, they will not be able to agree on the next | |||
block, because the hash is included in the next block! | |||
## Data Messages | |||
### Header | |||
- **Fields**: | |||
- `ChainID (string)`: ID of the blockchain | |||
- `Height (int64)`: Height of the block in the chain | |||
- `Time (int64)`: Unix time of the block | |||
- `NumTxs (int32)`: Number of transactions in the block | |||
- `TotalTxs (int64)`: Total number of transactions in the blockchain until | |||
now | |||
- `LastBlockHash ([]byte)`: Hash of the previous (parent) block | |||
- `ValidatorsHash ([]byte)`: Hash of the validator set for this block | |||
- `AppHash ([]byte)`: Data returned by the last call to `Commit` - typically the | |||
Merkle root of the application state after executing the previous block's | |||
transactions | |||
- `Proposer (Validator)`: Original proposer for the block | |||
- **Usage**: | |||
- Provided in RequestBeginBlock | |||
- Provides important context about the current state of the blockchain - | |||
especially height and time. | |||
- Provides the proposer of the current block, for use in proposer-based | |||
reward mechanisms. | |||
### Validator | |||
- **Fields**: | |||
- `Address ([]byte)`: Address of the validator (hash of the public key) | |||
- `PubKey (PubKey)`: Public key of the validator | |||
- `Power (int64)`: Voting power of the validator | |||
- **Usage**: | |||
- Provides all identifying information about the validator | |||
### SigningValidator | |||
- **Fields**: | |||
- `Validator (Validator)`: A validator | |||
- `SignedLastBlock (bool)`: Indicated whether or not the validator signed | |||
the last block | |||
- **Usage**: | |||
- Indicates whether a validator signed the last block, allowing for rewards | |||
based on validator availability | |||
### PubKey | |||
- **Fields**: | |||
- `Type (string)`: Type of the public key. A simple string like `"ed25519"`. | |||
In the future, may indicate a serialization algorithm to parse the `Data`, | |||
for instance `"amino"`. | |||
- `Data ([]byte)`: Public key data. For a simple public key, it's just the | |||
raw bytes. If the `Type` indicates an encoding algorithm, this is the | |||
encoded public key. | |||
- **Usage**: | |||
- A generic and extensible typed public key | |||
### Evidence | |||
- **Fields**: | |||
- `Type (string)`: Type of the evidence. A hierarchical path like | |||
"duplicate/vote". | |||
- `Validator (Validator`: The offending validator | |||
- `Height (int64)`: Height when the offense was committed | |||
- `Time (int64)`: Unix time of the block at height `Height` | |||
- `TotalVotingPower (int64)`: Total voting power of the validator set at | |||
height `Height` |
@ -1,5 +1,3 @@ | |||
# On Determinism | |||
Arguably, the most difficult part of blockchain programming is determinism - that is, ensuring that sources of indeterminism do not creep into the design of such systems. | |||
See [this issue](https://github.com/tendermint/abci/issues/56) for more information on the potential sources of indeterminism. |