Merge pull request #211 from tendermint/feature/reimplement-merkleeyes

Reimplement merkleeyes
This commit is contained in:
Ethan Frey 2017-08-07 18:56:58 +02:00 committed by GitHub
commit 13d739ac48
13 changed files with 616 additions and 6 deletions

View File

@ -32,6 +32,7 @@ test_cli: tests/cli/shunit2
./tests/cli/rpc.sh ./tests/cli/rpc.sh
./tests/cli/init.sh ./tests/cli/init.sh
./tests/cli/basictx.sh ./tests/cli/basictx.sh
./tests/cli/eyes.sh
./tests/cli/roles.sh ./tests/cli/roles.sh
./tests/cli/counter.sh ./tests/cli/counter.sh
./tests/cli/restart.sh ./tests/cli/restart.sh

View File

@ -27,13 +27,12 @@ import (
// BaseCli - main basecoin client command // BaseCli - main basecoin client command
var BaseCli = &cobra.Command{ var BaseCli = &cobra.Command{
Use: "basecli", Use: "basecli",
Short: "Light client for tendermint", Short: "Light client for Tendermint",
Long: `Basecli is an version of tmcli including custom logic to Long: `Basecli is a certifying light client for the basecoin abci app.
present a nice (not raw hex) interface to the basecoin blockchain structure.
This is a useful tool, but also serves to demonstrate how one can configure It leverages the power of the tendermint consensus algorithm get full
tmcli to work for any custom abci app. cryptographic proof of all queries while only syncing a fraction of the
`, block headers.`,
} }
func main() { func main() {

58
cmd/eyes/init.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
"github.com/tendermint/basecoin/cmd/basecoin/commands"
)
// InitCmd - node initialization command
var InitCmd = &cobra.Command{
Use: "init",
Short: "Initialize eyes abci server",
RunE: initCmd,
}
//nolint - flags
var (
FlagChainID = "chain-id" //TODO group with other flags or remove? is this already a flag here?
)
func init() {
InitCmd.Flags().String(FlagChainID, "eyes_test_id", "Chain ID")
}
func initCmd(cmd *cobra.Command, args []string) error {
// this will ensure that config.toml is there if not yet created, and create dir
cfg, err := tcmd.ParseConfig()
if err != nil {
return err
}
genesis := getGenesisJSON(viper.GetString(commands.FlagChainID))
return commands.CreateGenesisValidatorFiles(cfg, genesis, cmd.Root().Name())
}
// TODO: better, auto-generate validator...
func getGenesisJSON(chainID string) string {
return fmt.Sprintf(`{
"app_hash": "",
"chain_id": "%s",
"genesis_time": "0001-01-01T00:00:00.000Z",
"validators": [
{
"amount": 10,
"name": "",
"pub_key": {
"type": "ed25519",
"data": "7B90EA87E7DC0C7145C8C48C08992BE271C7234134343E8A8E8008E617DE7B30"
}
}
]
}`, chainID)
}

44
cmd/eyes/main.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"os"
"github.com/tendermint/tmlibs/cli"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/cmd/basecoin/commands"
"github.com/tendermint/basecoin/modules/base"
"github.com/tendermint/basecoin/modules/eyes"
"github.com/tendermint/basecoin/stack"
)
// BuildApp constructs the stack we want to use for this app
func BuildApp() basecoin.Handler {
return stack.New(
base.Logger{},
stack.Recovery{},
).
// We do this to demo real usage, also embeds it under it's own namespace
Dispatch(
stack.WrapHandler(etc.NewHandler()),
)
}
func main() {
rt := commands.RootCmd
rt.Short = "eyes"
rt.Long = "A demo app to show key-value store with proofs over abci"
commands.Handler = BuildApp()
rt.AddCommand(
// out own init command to not require argument
InitCmd,
commands.StartCmd,
commands.UnsafeResetAllCmd,
commands.VersionCmd,
)
cmd := cli.PrepareMainCmd(rt, "EYE", os.ExpandEnv("$HOME/.eyes"))
cmd.Execute()
}

67
cmd/eyescli/main.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"os"
"github.com/spf13/cobra"
keycmd "github.com/tendermint/go-crypto/cmd"
"github.com/tendermint/tmlibs/cli"
"github.com/tendermint/basecoin/client/commands"
"github.com/tendermint/basecoin/client/commands/auto"
"github.com/tendermint/basecoin/client/commands/proxy"
"github.com/tendermint/basecoin/client/commands/query"
rpccmd "github.com/tendermint/basecoin/client/commands/rpc"
"github.com/tendermint/basecoin/client/commands/seeds"
txcmd "github.com/tendermint/basecoin/client/commands/txs"
etccmd "github.com/tendermint/basecoin/modules/eyes/commands"
)
// EyesCli - main basecoin client command
var EyesCli = &cobra.Command{
Use: "eyescli",
Short: "Light client for Tendermint",
Long: `EyesCli is the light client for a merkle key-value store (eyes)`,
}
func main() {
commands.AddBasicFlags(EyesCli)
// Prepare queries
query.RootCmd.AddCommand(
// These are default parsers, but optional in your app (you can remove key)
query.TxQueryCmd,
query.KeyQueryCmd,
// this is our custom parser
etccmd.EtcQueryCmd,
)
// no middleware wrapers
txcmd.Middleware = txcmd.Wrappers{}
// txcmd.Middleware.Register(txcmd.RootCmd.PersistentFlags())
// just the etc commands
txcmd.RootCmd.AddCommand(
etccmd.SetTxCmd,
etccmd.RemoveTxCmd,
)
// Set up the various commands to use
EyesCli.AddCommand(
// we use out own init command to not require address arg
commands.InitCmd,
commands.ResetCmd,
keycmd.RootCmd,
seeds.RootCmd,
rpccmd.RootCmd,
query.RootCmd,
txcmd.RootCmd,
proxy.RootCmd,
commands.VersionCmd,
auto.AutoCompleteCmd,
)
cmd := cli.PrepareMainCmd(EyesCli, "EYE", os.ExpandEnv("$HOME/.eyescli"))
cmd.Execute()
}

View File

@ -0,0 +1,44 @@
package commands
import (
"encoding/hex"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cmn "github.com/tendermint/tmlibs/common"
"github.com/tendermint/basecoin/client/commands"
"github.com/tendermint/basecoin/client/commands/query"
"github.com/tendermint/basecoin/modules/eyes"
"github.com/tendermint/basecoin/stack"
)
// EtcQueryCmd - command to query raw data
var EtcQueryCmd = &cobra.Command{
Use: "etc [key]",
Short: "Get data stored under key in etc",
RunE: commands.RequireInit(etcQueryCmd),
}
func etcQueryCmd(cmd *cobra.Command, args []string) error {
var res etc.Data
arg, err := commands.GetOneArg(args, "key")
if err != nil {
return err
}
key, err := hex.DecodeString(cmn.StripHex(arg))
if err != nil {
return err
}
key = stack.PrefixedKey(etc.Name, key)
prove := !viper.GetBool(commands.FlagTrustNode)
height, err := query.GetParsed(key, &res, prove)
if err != nil {
return err
}
return query.OutputProof(res, height)
}

View File

@ -0,0 +1,63 @@
package commands
import (
"github.com/spf13/cobra"
"github.com/tendermint/basecoin/client/commands"
"github.com/tendermint/basecoin/client/commands/txs"
"github.com/tendermint/basecoin/modules/eyes"
)
// SetTxCmd is CLI command to set data
var SetTxCmd = &cobra.Command{
Use: "set",
Short: "Sets a key value pair",
RunE: commands.RequireInit(setTxCmd),
}
// RemoveTxCmd is CLI command to remove data
var RemoveTxCmd = &cobra.Command{
Use: "remove",
Short: "Removes a key value pair",
RunE: commands.RequireInit(removeTxCmd),
}
const (
// FlagKey is the cli flag to set the key
FlagKey = "key"
// FlagValue is the cli flag to set the value
FlagValue = "value"
)
func init() {
SetTxCmd.Flags().String(FlagKey, "", "Key to store data under (hex)")
SetTxCmd.Flags().String(FlagValue, "", "Data to store (hex)")
RemoveTxCmd.Flags().String(FlagKey, "", "Key under which to remove data (hex)")
}
// setTxCmd creates a SetTx, wraps, signs, and delivers it
func setTxCmd(cmd *cobra.Command, args []string) error {
key, err := commands.ParseHexFlag(FlagKey)
if err != nil {
return err
}
value, err := commands.ParseHexFlag(FlagValue)
if err != nil {
return err
}
tx := etc.NewSetTx(key, value)
return txs.DoTx(tx)
}
// removeTxCmd creates a RemoveTx, wraps, signs, and delivers it
func removeTxCmd(cmd *cobra.Command, args []string) error {
key, err := commands.ParseHexFlag(FlagKey)
if err != nil {
return err
}
tx := etc.NewRemoveTx(key)
return txs.DoTx(tx)
}

23
modules/eyes/errors.go Normal file
View File

@ -0,0 +1,23 @@
package etc
import (
"fmt"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/errors"
)
var (
errMissingData = fmt.Errorf("All tx fields must be filled")
malformed = abci.CodeType_EncodingError
)
//nolint
func ErrMissingData() errors.TMError {
return errors.WithCode(errMissingData, malformed)
}
func IsMissingDataErr(err error) bool {
return errors.IsSameError(errMissingData, err)
}

93
modules/eyes/handler.go Normal file
View File

@ -0,0 +1,93 @@
package etc
import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/state"
wire "github.com/tendermint/go-wire"
)
const (
// Name of the module for registering it
Name = "etc"
// CostSet is the gas needed for the set operation
CostSet uint64 = 10
// CostRemove is the gas needed for the remove operation
CostRemove = 10
)
// Handler allows us to set and remove data
type Handler struct {
basecoin.NopInitState
basecoin.NopInitValidate
}
var _ basecoin.Handler = Handler{}
// NewHandler makes a role handler to modify data
func NewHandler() Handler {
return Handler{}
}
// Name - return name space
func (Handler) Name() string {
return Name
}
// CheckTx verifies if the transaction is properly formated
func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.CheckResult, err error) {
err = tx.ValidateBasic()
if err != nil {
return
}
switch tx.Unwrap().(type) {
case SetTx:
res = basecoin.NewCheck(CostSet, "")
case RemoveTx:
res = basecoin.NewCheck(CostRemove, "")
default:
err = errors.ErrUnknownTxType(tx)
}
return
}
// DeliverTx tries to create a new role.
//
// Returns an error if the role already exists
func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.DeliverResult, err error) {
err = tx.ValidateBasic()
if err != nil {
return
}
switch t := tx.Unwrap().(type) {
case SetTx:
res, err = h.doSetTx(ctx, store, t)
case RemoveTx:
res, err = h.doRemoveTx(ctx, store, t)
default:
err = errors.ErrUnknownTxType(tx)
}
return
}
// doSetTx writes to the store, overwriting any previous value
// note that an empty response in DeliverTx is OK with no log or data returned
func (h Handler) doSetTx(ctx basecoin.Context, store state.SimpleDB, tx SetTx) (res basecoin.DeliverResult, err error) {
data := NewData(tx.Value, ctx.BlockHeight())
store.Set(tx.Key, wire.BinaryBytes(data))
return
}
// doRemoveTx deletes the value from the store and returns the last value
// here we let res.Data to return the value over abci
func (h Handler) doRemoveTx(ctx basecoin.Context, store state.SimpleDB, tx RemoveTx) (res basecoin.DeliverResult, err error) {
// we set res.Data so it gets returned to the client over the abci interface
res.Data = store.Get(tx.Key)
if len(res.Data) != 0 {
store.Remove(tx.Key)
}
return
}

View File

@ -0,0 +1,61 @@
package etc
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
wire "github.com/tendermint/go-wire"
)
func TestHandler(t *testing.T) {
assert, require := assert.New(t), require.New(t)
key := []byte("one")
val := []byte("foo")
var height uint64 = 123
h := NewHandler()
ctx := stack.MockContext("role-chain", height)
store := state.NewMemKVStore()
set := SetTx{Key: key, Value: val}.Wrap()
remove := RemoveTx{Key: key}.Wrap()
invalid := SetTx{}.Wrap()
// make sure pricing makes sense
cres, err := h.CheckTx(ctx, store, set)
require.Nil(err, "%+v", err)
require.True(cres.GasAllocated > 5, "%#v", cres)
// set the value, no error
dres, err := h.DeliverTx(ctx, store, set)
require.Nil(err, "%+v", err)
// get the data
var data Data
bs := store.Get(key)
require.NotEmpty(bs)
err = wire.ReadBinaryBytes(bs, &data)
require.Nil(err, "%+v", err)
assert.Equal(height, data.SetAt)
assert.EqualValues(val, data.Value)
// make sure pricing makes sense
cres, err = h.CheckTx(ctx, store, remove)
require.Nil(err, "%+v", err)
require.True(cres.GasAllocated > 5, "%#v", cres)
// remove the data returns the same as the above query
dres, err = h.DeliverTx(ctx, store, remove)
require.Nil(err, "%+v", err)
require.EqualValues(bs, dres.Data)
// make sure invalid fails both ways
_, err = h.CheckTx(ctx, store, invalid)
require.NotNil(err)
_, err = h.DeliverTx(ctx, store, invalid)
require.NotNil(err)
}

20
modules/eyes/store.go Normal file
View File

@ -0,0 +1,20 @@
package etc
import "github.com/tendermint/go-wire/data"
// Data is the struct we use to store in the merkle tree
type Data struct {
// SetAt is the block height this was set at
SetAt uint64 `json:"set_at"`
// Value is the data that was stored.
// data.Bytes is like []byte but json encodes as hex not base64
Value data.Bytes `json:"value"`
}
// NewData creates a new Data item
func NewData(value []byte, setAt uint64) Data {
return Data{
SetAt: setAt,
Value: value,
}
}

66
modules/eyes/tx.go Normal file
View File

@ -0,0 +1,66 @@
package etc
import (
"github.com/tendermint/basecoin"
"github.com/tendermint/go-wire/data"
)
// nolint
const (
TypeSet = Name + "/set"
TypeRemove = Name + "/remove"
ByteSet = 0xF4
ByteRemove = 0xF5
)
func init() {
basecoin.TxMapper.
RegisterImplementation(SetTx{}, TypeSet, ByteSet).
RegisterImplementation(RemoveTx{}, TypeRemove, ByteRemove)
}
// SetTx sets a key-value pair
type SetTx struct {
Key data.Bytes `json:"key"`
Value data.Bytes `json:"value"`
}
func NewSetTx(key, value []byte) basecoin.Tx {
return SetTx{Key: key, Value: value}.Wrap()
}
// Wrap - fulfills TxInner interface
func (t SetTx) Wrap() basecoin.Tx {
return basecoin.Tx{t}
}
// ValidateBasic makes sure it is valid
func (t SetTx) ValidateBasic() error {
if len(t.Key) == 0 || len(t.Value) == 0 {
return ErrMissingData()
}
return nil
}
// RemoveTx deletes the value at this key, returns old value
type RemoveTx struct {
Key data.Bytes `json:"key"`
}
func NewRemoveTx(key []byte) basecoin.Tx {
return RemoveTx{Key: key}.Wrap()
}
// Wrap - fulfills TxInner interface
func (t RemoveTx) Wrap() basecoin.Tx {
return basecoin.Tx{t}
}
// ValidateBasic makes sure it is valid
func (t RemoveTx) ValidateBasic() error {
if len(t.Key) == 0 {
return ErrMissingData()
}
return nil
}

71
tests/cli/eyes.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
# These global variables are required for common.sh
SERVER_EXE=eyes
CLIENT_EXE=eyescli
oneTimeSetUp() {
# These are passed in as args
BASE_DIR=$HOME/.test_eyes
CHAIN_ID="eyes-cli-test"
rm -rf $BASE_DIR 2>/dev/null
mkdir -p $BASE_DIR
echo "Setting up genesis..."
SERVE_DIR=${BASE_DIR}/server
SERVER_LOG=${BASE_DIR}/${SERVER_EXE}.log
echo "Starting ${SERVER_EXE} server..."
export EYE_HOME=${SERVE_DIR}
${SERVER_EXE} init --chain-id=$CHAIN_ID >>$SERVER_LOG
startServer $SERVE_DIR $SERVER_LOG
[ $? = 0 ] || return 1
# Set up client - make sure you use the proper prefix if you set
# a custom CLIENT_EXE
export EYE_HOME=${BASE_DIR}/client
initClient $CHAIN_ID
[ $? = 0 ] || return 1
printf "...Testing may begin!\n\n\n"
}
oneTimeTearDown() {
quickTearDown
}
test00SetGetRemove() {
KEY="CAFE6000"
VALUE="F00D4200"
assertFalse "line=${LINENO} data present" "${CLIENT_EXE} query etc ${KEY}"
# set data
TXRES=$(${CLIENT_EXE} tx set --key=${KEY} --value=${VALUE})
txSucceeded $? "$TXRES" "set cafe"
HASH=$(echo $TXRES | jq .hash | tr -d \")
TX_HEIGHT=$(echo $TXRES | jq .height)
# make sure it is set
DATA=$(${CLIENT_EXE} query etc ${KEY})
assertTrue "line=${LINENO} data not set" $?
assertEquals "line=${LINENO}" "\"${VALUE}\"" $(echo $DATA | jq .data.value)
# query the tx
TX=$(${CLIENT_EXE} query tx $HASH)
assertTrue "line=${LINENO}, found tx" $?
if [ -n "$DEBUG" ]; then echo $TX; echo; fi
assertEquals "line=${LINENO}, proper type" "\"etc/set\"" $(echo $TX | jq .data.type)
assertEquals "line=${LINENO}, proper key" "\"${KEY}\"" $(echo $TX | jq .data.data.key)
assertEquals "line=${LINENO}, proper value" "\"${VALUE}\"" $(echo $TX | jq .data.data.value)
}
# Load common then run these tests with shunit2!
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory
. $DIR/common.sh
. $DIR/shunit2