Merge pull request #21 from tendermint/develop

Develop
This commit is contained in:
Ethan Buchman 2016-07-23 19:23:33 -04:00 committed by GitHub
commit 58bacfb04e
15 changed files with 296 additions and 98 deletions

View File

@ -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/...

View File

@ -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).

23
circle.yml Normal file
View File

@ -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"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

14
tests/test.sh Executable file → Normal file
View File

@ -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

View File

@ -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() {

48
tests/test_app/main.go Normal file
View File

@ -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})
}

17
tests/test_app/test.sh Executable file
View File

@ -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

10
tests/test_cli/ex1.tmsp Normal file
View File

@ -0,0 +1,10 @@
echo hello
info
commit
append_tx abc
info
commit
query abc
append_tx def=xyz
commit
query def

View File

@ -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}

8
tests/test_cli/ex2.tmsp Normal file
View File

@ -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

View File

@ -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}

29
tests/test_cli/test.sh Normal file
View File

@ -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"