diff --git a/Makefile b/Makefile index 20fb8a23..b47dc0e2 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ install: get_deps test: go test github.com/tendermint/tmsp/... + bash tests/test.sh get_deps: go get -d github.com/tendermint/tmsp/... diff --git a/README.md b/README.md index 6c842083..fc5c7bb2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Tendermint Socket Protocol (TMSP) +[![CircleCI](https://circleci.com/gh/tendermint/tmsp.svg?style=svg)](https://circleci.com/gh/tendermint/tmsp) + Blockchains are a system for creating shared multi-master application state. **TMSP** is a socket protocol enabling a blockchain consensus engine, running in one process, to manage a blockchain application state, running in another. @@ -10,12 +12,39 @@ Other implementations: * [cpp-tmsp](https://github.com/mdyring/cpp-tmsp) by Martin Dyring-Andersen * [js-tmsp](https://github.com/tendermint/js-tmsp) +## Contents + +This repository holds a number of important pieces: + +- `types/types.proto` + - the protobuf file defining TMSP message types, and the optional grpc interface. + - run `protoc --go_out=plugins=grpc:. types.proto` in the `types` dir to generate the `types/types.pb.go` file + - see `protoc --help` and [the grpc docs](https://www.grpc.io/docs) for examples and details of other languages + +- golang implementation of TMSP client and server + - two implementations: + - asynchronous, ordered message passing over unix or tcp; + - messages are serialized using protobuf and length prefixed + - grpc + - TendermintCore runs a client, and the application runs a server + +- `cmd/tmsp-cli` + - command line tool wrapping the client for probing/testing a TMSP application + - use `tmsp-cli --version` to get the TMSP version + +- examples: + - the `cmd/counter` application, which illustrates nonce checking in txs + - the `cmd/dummy` application, which illustrates a simple key-value merkle tree + + ## Message format Since this is a streaming protocol, all messages are encoded with a length-prefix followed by the message encoded in Protobuf3. Protobuf3 doesn't have an official length-prefix standard, so we use our own. The first byte represents the length of the big-endian encoded length. For example, if the Protobuf3 encoded TMSP message is `0xDEADBEEF` (4 bytes), the length-prefixed message is `0x0104DEADBEEF`. If the Protobuf3 encoded TMSP message is 65535 bytes long, the length-prefixed message would be like `0x02FFFF...`. +Note this prefixing does not apply for grpc. + ## Message types TMSP requests/responses are simple Protobuf messages. Check out the [schema file](https://github.com/tendermint/tmsp/blob/master/types/types.proto). diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..33e50dc7 --- /dev/null +++ b/circle.yml @@ -0,0 +1,23 @@ +machine: + environment: + GOPATH: /home/ubuntu/.go_workspace + REPO: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME + hosts: + circlehost: 127.0.0.1 + localhost: 127.0.0.1 + +checkout: + post: + - rm -rf $REPO + - mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME + - mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO + # - git submodule sync + # - git submodule update --init # use submodules + +dependencies: + override: + - "cd $REPO && go get -t ./..." + +test: + override: + - "cd $REPO && make test" diff --git a/client/grpc_client.go b/client/grpc_client.go index a1c51410..813be658 100644 --- a/client/grpc_client.go +++ b/client/grpc_client.go @@ -90,7 +90,10 @@ func (cli *grpcClient) Error() error { } //---------------------------------------- -// async calls are really sync. +// 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 accomodate, 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 @@ -199,15 +202,18 @@ func (cli *grpcClient) finishAsyncCall(req *types.Request, res *types.Response) reqres.Done() // Release waiters reqres.SetDone() // so reqRes.SetCallback will run the callback - // Notify reqRes listener if set - if cb := reqres.GetCallback(); cb != nil { - cb(res) - } + // 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) - } + // Notify client listener if set + if cli.resCb != nil { + cli.resCb(reqres.Request, res) + } + }() return reqres } diff --git a/cmd/tmsp-cli/tmsp-cli.go b/cmd/tmsp-cli/tmsp-cli.go index 373266ef..964dbe69 100644 --- a/cmd/tmsp-cli/tmsp-cli.go +++ b/cmd/tmsp-cli/tmsp-cli.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "encoding/hex" "errors" "fmt" "io" @@ -10,7 +11,6 @@ import ( "github.com/codegangsta/cli" . "github.com/tendermint/go-common" - "github.com/tendermint/go-wire/expr" "github.com/tendermint/tmsp/client" "github.com/tendermint/tmsp/types" ) @@ -22,6 +22,7 @@ func main() { app := cli.NewApp() app.Name = "tmsp-cli" app.Usage = "tmsp-cli [command] [args...]" + app.Version = "0.2" app.Flags = []cli.Flag{ cli.StringFlag{ Name: "address", @@ -33,6 +34,10 @@ func main() { Value: "socket", Usage: "socket or grpc", }, + cli.BoolFlag{ + Name: "verbose", + Usage: "print the command and results as if it were a console session", + }, } app.Commands = []cli.Command{ { @@ -133,7 +138,10 @@ func cmdBatch(app *cli.App, c *cli.Context) error { } else if err != nil { return err } - args := []string{"tmsp"} + args := []string{"tmsp-cli"} + if c.GlobalBool("verbose") { + args = append(args, "--verbose") + } args = append(args, strings.Split(string(line), " ")...) app.Run(args) } @@ -151,7 +159,7 @@ func cmdConsole(app *cli.App, c *cli.Context) error { return err } - args := []string{"tmsp"} + args := []string{"tmsp-cli"} args = append(args, strings.Split(string(line), " ")...) if err := app.Run(args); err != nil { return err @@ -167,14 +175,14 @@ func cmdEcho(c *cli.Context) error { return errors.New("Command echo takes 1 argument") } res := client.EchoSync(args[0]) - printResponse(res, string(res.Data), false) + printResponse(c, res, string(res.Data), false) return nil } // Get some info from the application func cmdInfo(c *cli.Context) error { res := client.InfoSync() - printResponse(res, string(res.Data), false) + printResponse(c, res, string(res.Data), false) return nil } @@ -185,7 +193,7 @@ func cmdSetOption(c *cli.Context) error { return errors.New("Command set_option takes 2 arguments (key, value)") } res := client.SetOptionSync(args[0], args[1]) - printResponse(res, Fmt("%s=%s", args[0], args[1]), false) + printResponse(c, res, Fmt("%s=%s", args[0], args[1]), false) return nil } @@ -195,14 +203,9 @@ func cmdAppendTx(c *cli.Context) error { if len(args) != 1 { return errors.New("Command append_tx takes 1 argument") } - txExprString := c.Args()[0] - txBytes, err := expr.Compile(txExprString) - if err != nil { - return err - } - + txBytes := stringOrHexToBytes(c.Args()[0]) res := client.AppendTxSync(txBytes) - printResponse(res, string(res.Data), true) + printResponse(c, res, string(res.Data), true) return nil } @@ -212,21 +215,16 @@ func cmdCheckTx(c *cli.Context) error { if len(args) != 1 { return errors.New("Command check_tx takes 1 argument") } - txExprString := c.Args()[0] - txBytes, err := expr.Compile(txExprString) - if err != nil { - return err - } - + txBytes := stringOrHexToBytes(c.Args()[0]) res := client.CheckTxSync(txBytes) - printResponse(res, string(res.Data), true) + printResponse(c, res, string(res.Data), true) return nil } // Get application Merkle root hash func cmdCommit(c *cli.Context) error { res := client.CommitSync() - printResponse(res, Fmt("%X", res.Data), false) + printResponse(c, res, Fmt("%X", res.Data), false) return nil } @@ -236,20 +234,19 @@ func cmdQuery(c *cli.Context) error { if len(args) != 1 { return errors.New("Command query takes 1 argument") } - queryExprString := args[0] - queryBytes, err := expr.Compile(queryExprString) - if err != nil { - return err - } - + queryBytes := stringOrHexToBytes(c.Args()[0]) res := client.QuerySync(queryBytes) - printResponse(res, string(res.Data), true) + printResponse(c, res, string(res.Data), true) return nil } //-------------------------------------------------------------------------------- -func printResponse(res types.Result, s string, printCode bool) { +func printResponse(c *cli.Context, res types.Result, s string, printCode bool) { + if c.GlobalBool("verbose") { + fmt.Println(">", c.Command.Name, strings.Join(c.Args(), " ")) + } + if printCode { fmt.Printf("-> code: %s\n", res.Code.String()) } @@ -263,4 +260,20 @@ func printResponse(res types.Result, s string, printCode bool) { fmt.Printf("-> log: %s\n", res.Log) } + if c.GlobalBool("verbose") { + fmt.Println("") + } + +} + +// NOTE: s is interpreted as a string unless prefixed with 0x +func stringOrHexToBytes(s string) []byte { + if len(s) > 2 && s[:2] == "0x" { + b, err := hex.DecodeString(s[2:]) + if err != nil { + fmt.Println("Error decoding hex argument:", err.Error()) + } + return b + } + return []byte(s) } diff --git a/example/dummy/dummy.go b/example/dummy/dummy.go index 3fa237f0..ca3a8c03 100644 --- a/example/dummy/dummy.go +++ b/example/dummy/dummy.go @@ -28,6 +28,7 @@ func (app *DummyApplication) SetOption(key string, value string) (log string) { return "" } +// tx is either "key=value" or just arbitrary bytes func (app *DummyApplication) AppendTx(tx []byte) types.Result { parts := strings.Split(string(tx), "=") if len(parts) == 2 { diff --git a/tests/test.sh b/tests/test.sh old mode 100755 new mode 100644 index ebdab0c8..4087cdce --- a/tests/test.sh +++ b/tests/test.sh @@ -1,12 +1,8 @@ +#! /bin/bash -ROOT=$GOPATH/src/github.com/tendermint/tmsp -cd $ROOT +# test the counter using a go test script +bash tests/test_app/test.sh -# test golang counter -COUNTER_APP="counter" go run $ROOT/tests/test_counter.go +# test the cli against the examples in the tutorial at tendermint.com +bash tests/test_cli/test.sh -# test golang counter via grpc -COUNTER_APP="counter -tmsp=grpc" go run $ROOT/tests/test_counter.go -tmsp=grpc - -# test nodejs counter -COUNTER_APP="node ../js-tmsp/example/app.js" go run $ROOT/tests/test_counter.go diff --git a/tests/test_counter.go b/tests/test_app/app.go similarity index 51% rename from tests/test_counter.go rename to tests/test_app/app.go index 2f78b696..2a1bf15a 100644 --- a/tests/test_counter.go +++ b/tests/test_app/app.go @@ -2,8 +2,6 @@ package main import ( "bytes" - "flag" - "fmt" "os" "time" @@ -13,57 +11,19 @@ import ( "github.com/tendermint/tmsp/types" ) -var tmspPtr = flag.String("tmsp", "socket", "socket or grpc") - -func main() { - flag.Parse() - - // Run tests - testBasic() - - fmt.Println("Success!") -} - -func testBasic() { - fmt.Println("Running basic tests") - appProc := startApp() - defer appProc.StopProcess(true) - client := startClient() - defer client.Stop() - - setOption(client, "serial", "on") - commit(client, nil) - appendTx(client, []byte("abc"), types.CodeType_BadNonce, nil) - commit(client, nil) - appendTx(client, []byte{0x00}, types.CodeType_OK, nil) - commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 1}) - appendTx(client, []byte{0x00}, types.CodeType_BadNonce, nil) - appendTx(client, []byte{0x01}, types.CodeType_OK, nil) - appendTx(client, []byte{0x00, 0x02}, types.CodeType_OK, nil) - appendTx(client, []byte{0x00, 0x03}, types.CodeType_OK, nil) - appendTx(client, []byte{0x00, 0x00, 0x04}, types.CodeType_OK, nil) - appendTx(client, []byte{0x00, 0x00, 0x06}, types.CodeType_BadNonce, nil) - commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 5}) -} - //---------------------------------------- -func startApp() *process.Process { - counterApp := os.Getenv("COUNTER_APP") - if counterApp == "" { - panic("No COUNTER_APP specified") - } - +func StartApp(tmspApp string) *process.Process { // Start the app //outBuf := NewBufferCloser(nil) - proc, err := process.StartProcess("counter_app", + proc, err := process.StartProcess("tmsp_app", "bash", - []string{"-c", counterApp}, + []string{"-c", tmspApp}, nil, os.Stdout, ) if err != nil { - panic("running counter_app: " + err.Error()) + panic("running tmsp_app: " + err.Error()) } // TODO a better way to handle this? @@ -72,16 +32,16 @@ func startApp() *process.Process { return proc } -func startClient() tmspcli.Client { +func StartClient(tmspType string) tmspcli.Client { // Start client - client, err := tmspcli.NewClient("tcp://127.0.0.1:46658", *tmspPtr, true) + client, err := tmspcli.NewClient("tcp://127.0.0.1:46658", tmspType, true) if err != nil { - panic("connecting to counter_app: " + err.Error()) + panic("connecting to tmsp_app: " + err.Error()) } return client } -func setOption(client tmspcli.Client, key, value string) { +func SetOption(client tmspcli.Client, key, value string) { res := client.SetOptionSync(key, value) _, _, log := res.Code, res.Data, res.Log if res.IsErr() { @@ -89,7 +49,7 @@ func setOption(client tmspcli.Client, key, value string) { } } -func commit(client tmspcli.Client, hashExp []byte) { +func Commit(client tmspcli.Client, hashExp []byte) { res := client.CommitSync() _, data, log := res.Code, res.Data, res.Log if res.IsErr() { @@ -101,7 +61,7 @@ func commit(client tmspcli.Client, hashExp []byte) { } } -func appendTx(client tmspcli.Client, txBytes []byte, codeExp types.CodeType, dataExp []byte) { +func AppendTx(client tmspcli.Client, txBytes []byte, codeExp types.CodeType, dataExp []byte) { res := client.AppendTxSync(txBytes) code, data, log := res.Code, res.Data, res.Log if code != codeExp { @@ -114,7 +74,7 @@ func appendTx(client tmspcli.Client, txBytes []byte, codeExp types.CodeType, dat } } -func checkTx(client tmspcli.Client, txBytes []byte, codeExp types.CodeType, dataExp []byte) { +func CheckTx(client tmspcli.Client, txBytes []byte, codeExp types.CodeType, dataExp []byte) { res := client.CheckTxSync(txBytes) code, data, log := res.Code, res.Data, res.Log if res.IsErr() { diff --git a/tests/test_app/main.go b/tests/test_app/main.go new file mode 100644 index 00000000..ada63718 --- /dev/null +++ b/tests/test_app/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "os" + + "github.com/tendermint/tmsp/types" +) + +var tmspType string + +func init() { + tmspType = os.Getenv("TMSP") + if tmspType == "" { + tmspType = "socket" + } +} + +func main() { + testCounter() +} + +func testCounter() { + tmspApp := os.Getenv("TMSP_APP") + if tmspApp == "" { + panic("No TMSP_APP specified") + } + + fmt.Printf("Running %s test with tmsp=%s\n", tmspApp, tmspType) + appProc := StartApp(tmspApp) + defer appProc.StopProcess(true) + client := StartClient(tmspType) + defer client.Stop() + + SetOption(client, "serial", "on") + Commit(client, nil) + AppendTx(client, []byte("abc"), types.CodeType_BadNonce, nil) + Commit(client, nil) + AppendTx(client, []byte{0x00}, types.CodeType_OK, nil) + Commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 1}) + AppendTx(client, []byte{0x00}, types.CodeType_BadNonce, nil) + AppendTx(client, []byte{0x01}, types.CodeType_OK, nil) + AppendTx(client, []byte{0x00, 0x02}, types.CodeType_OK, nil) + AppendTx(client, []byte{0x00, 0x03}, types.CodeType_OK, nil) + AppendTx(client, []byte{0x00, 0x00, 0x04}, types.CodeType_OK, nil) + AppendTx(client, []byte{0x00, 0x00, 0x06}, types.CodeType_BadNonce, nil) + Commit(client, []byte{0, 0, 0, 0, 0, 0, 0, 5}) +} diff --git a/tests/test_app/test.sh b/tests/test_app/test.sh new file mode 100755 index 00000000..4c28a831 --- /dev/null +++ b/tests/test_app/test.sh @@ -0,0 +1,17 @@ +#! /bin/bash +set -e + +# These tests spawn the counter app and server by execing the TMSP_APP command and run some simple client tests against it + +ROOT=$GOPATH/src/github.com/tendermint/tmsp/tests/test_app +cd $ROOT + +# test golang counter +TMSP_APP="counter" go run *.go + +# test golang counter via grpc +TMSP_APP="counter -tmsp=grpc" TMSP="grpc" go run *.go + +# test nodejs counter +# TODO: fix node app +#TMSP_APP="node $GOPATH/src/github.com/tendermint/js-tmsp/example/app.js" go test -test.run TestCounter diff --git a/tests/test_cli/ex1.tmsp b/tests/test_cli/ex1.tmsp new file mode 100644 index 00000000..de61c8b2 --- /dev/null +++ b/tests/test_cli/ex1.tmsp @@ -0,0 +1,10 @@ +echo hello +info +commit +append_tx abc +info +commit +query abc +append_tx def=xyz +commit +query def diff --git a/tests/test_cli/ex1.tmsp.out b/tests/test_cli/ex1.tmsp.out new file mode 100644 index 00000000..89bd61c2 --- /dev/null +++ b/tests/test_cli/ex1.tmsp.out @@ -0,0 +1,31 @@ +> echo hello +-> data: {hello} + +> info +-> data: {size:0} + +> commit + +> append_tx abc +-> code: OK + +> info +-> data: {size:1} + +> commit +-> data: {750502FC7E84BBD788ED589624F06CFA871845D1} + +> query abc +-> code: OK +-> data: {Index=0 value=abc exists=true} + +> append_tx def=xyz +-> code: OK + +> commit +-> data: {76393B8A182E450286B0694C629ECB51B286EFD5} + +> query def +-> code: OK +-> data: {Index=1 value=xyz exists=true} + diff --git a/tests/test_cli/ex2.tmsp b/tests/test_cli/ex2.tmsp new file mode 100644 index 00000000..e9550db8 --- /dev/null +++ b/tests/test_cli/ex2.tmsp @@ -0,0 +1,8 @@ +set_option serial on +check_tx 0x00 +check_tx 0xff +append_tx 0x00 +check_tx 0x00 +append_tx 0x01 +append_tx 0x04 +info diff --git a/tests/test_cli/ex2.tmsp.out b/tests/test_cli/ex2.tmsp.out new file mode 100644 index 00000000..3aa7744b --- /dev/null +++ b/tests/test_cli/ex2.tmsp.out @@ -0,0 +1,26 @@ +> set_option serial on +-> data: {serial=on} + +> check_tx 0x00 +-> code: OK + +> check_tx 0xff +-> code: OK + +> append_tx 0x00 +-> code: OK + +> check_tx 0x00 +-> code: BadNonce +-> log: Invalid nonce. Expected >= 1, got 0 + +> append_tx 0x01 +-> code: OK + +> append_tx 0x04 +-> code: BadNonce +-> log: Invalid nonce. Expected 2, got 4 + +> info +-> data: {hashes:0, txs:2} + diff --git a/tests/test_cli/test.sh b/tests/test_cli/test.sh new file mode 100644 index 00000000..f8920958 --- /dev/null +++ b/tests/test_cli/test.sh @@ -0,0 +1,29 @@ +#! /bin/bash + +function testExample() { + N=$1 + INPUT=$2 + APP=$3 + + echo "Example $N" + $APP &> /dev/null & + sleep 2 + tmsp-cli --verbose batch < $INPUT > "${INPUT}.out.new" + killall "$APP" > /dev/null + + pre=`shasum < "${INPUT}.out"` + post=`shasum < "${INPUT}.out.new"` + + if [[ "$pre" != "$post" ]]; then + echo "You broke the tutorial" + exit 1 + fi + + rm "${INPUT}".out.new +} + +testExample 1 tests/test_cli/ex1.tmsp dummy +testExample 2 tests/test_cli/ex2.tmsp counter + +echo "" +echo "PASS"