From 7579abd7101dd11ed180f5c35d8dd4ee4c67288e Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sat, 3 Mar 2018 20:40:43 -0500 Subject: [PATCH] bring back dummy for backwards compatibility ... --- example/dummy/README.md | 4 + example/dummy/helpers.go | 36 ++++ example/dummy/kvstore.go | 126 +++++++++++ example/dummy/kvstore_test.go | 310 ++++++++++++++++++++++++++++ example/dummy/persistent_kvstore.go | 205 ++++++++++++++++++ 5 files changed, 681 insertions(+) create mode 100644 example/dummy/README.md create mode 100644 example/dummy/helpers.go create mode 100644 example/dummy/kvstore.go create mode 100644 example/dummy/kvstore_test.go create mode 100644 example/dummy/persistent_kvstore.go diff --git a/example/dummy/README.md b/example/dummy/README.md new file mode 100644 index 000000000..fe9d1c2db --- /dev/null +++ b/example/dummy/README.md @@ -0,0 +1,4 @@ +# Dummy + +DEPRECATED. See KVStore + diff --git a/example/dummy/helpers.go b/example/dummy/helpers.go new file mode 100644 index 000000000..927319867 --- /dev/null +++ b/example/dummy/helpers.go @@ -0,0 +1,36 @@ +package dummy + +import ( + "github.com/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 { + pubkey := cmn.RandBytes(33) + power := cmn.RandUint16() + 1 + return types.Validator{pubkey, int64(power)} +} + +// 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 +} + +// InitDummy initializes the dummy 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 InitDummy(app *PersistentDummyApplication) { + app.InitChain(types.RequestInitChain{ + Validators: RandVals(1), + AppStateBytes: []byte("[]"), + }) +} diff --git a/example/dummy/kvstore.go b/example/dummy/kvstore.go new file mode 100644 index 000000000..79aa43978 --- /dev/null +++ b/example/dummy/kvstore.go @@ -0,0 +1,126 @@ +package dummy + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + + "github.com/tendermint/abci/example/code" + "github.com/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 = (*DummyApplication)(nil) + +type DummyApplication struct { + types.BaseApplication + + state State +} + +func NewDummyApplication() *DummyApplication { + state := loadState(dbm.NewMemDB()) + return &DummyApplication{state: state} +} + +func (app *DummyApplication) 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 *DummyApplication) 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 *DummyApplication) CheckTx(tx []byte) types.ResponseCheckTx { + return types.ResponseCheckTx{Code: code.CodeTypeOK} +} + +func (app *DummyApplication) 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 *DummyApplication) 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 + } +} diff --git a/example/dummy/kvstore_test.go b/example/dummy/kvstore_test.go new file mode 100644 index 000000000..548fe7422 --- /dev/null +++ b/example/dummy/kvstore_test.go @@ -0,0 +1,310 @@ +package dummy + +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/abci/client" + "github.com/tendermint/abci/example/code" + abciserver "github.com/tendermint/abci/server" + "github.com/tendermint/abci/types" +) + +func testDummy(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 TestDummyKV(t *testing.T) { + dummy := NewDummyApplication() + key := "abc" + value := key + tx := []byte(key) + testDummy(t, dummy, tx, key, value) + + value = "def" + tx = []byte(key + "=" + value) + testDummy(t, dummy, tx, key, value) +} + +func TestPersistentDummyKV(t *testing.T) { + dir, err := ioutil.TempDir("/tmp", "abci-dummy-test") // TODO + if err != nil { + t.Fatal(err) + } + dummy := NewPersistentDummyApplication(dir) + key := "abc" + value := key + tx := []byte(key) + testDummy(t, dummy, tx, key, value) + + value = "def" + tx = []byte(key + "=" + value) + testDummy(t, dummy, tx, key, value) +} + +func TestPersistentDummyInfo(t *testing.T) { + dir, err := ioutil.TempDir("/tmp", "abci-dummy-test") // TODO + if err != nil { + t.Fatal(err) + } + dummy := NewPersistentDummyApplication(dir) + InitDummy(dummy) + height := int64(0) + + resInfo := dummy.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), + } + dummy.BeginBlock(types.RequestBeginBlock{hash, header, nil, nil}) + dummy.EndBlock(types.RequestEndBlock{header.Height}) + dummy.Commit() + + resInfo = dummy.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-dummy-test") // TODO + if err != nil { + t.Fatal(err) + } + dummy := NewPersistentDummyApplication(dir) + + // init with some validators + total := 10 + nInit := 5 + vals := RandVals(total) + // iniitalize with the first nInit + dummy.InitChain(types.RequestInitChain{ + Validators: vals[:nInit], + }) + + vals1, vals2 := vals[:nInit], dummy.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, dummy, 1, diff, tx1, tx2) + + vals1, vals2 = vals[:nInit+2], dummy.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, dummy, 2, diff, tx1, tx2, tx3) + + vals1 = append(vals[:nInit-2], vals[nInit+1]) + vals2 = dummy.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, dummy, 3, diff, tx1) + + vals1 = append([]types.Validator{v1}, vals1[1:]...) + vals2 = dummy.Validators() + valsEqual(t, vals1, vals2) + +} + +func makeApplyBlock(t *testing.T, dummy types.Application, heightInt int, diff []types.Validator, txs ...[]byte) { + // make and apply block + height := int64(heightInt) + hash := []byte("foo") + header := types.Header{ + Height: height, + } + + dummy.BeginBlock(types.RequestBeginBlock{hash, header, nil, nil}) + for _, tx := range txs { + if r := dummy.DeliverTx(tx); r.IsErr() { + t.Fatal(r) + } + } + resEndBlock := dummy.EndBlock(types.RequestEndBlock{header.Height}) + dummy.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, v2.PubKey) || + 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 + dummy := NewDummyApplication() + client, server, err := makeSocketClientServer(dummy, "dummy-socket") + require.Nil(t, err) + defer server.Stop() + defer client.Stop() + + runClientTests(t, client) + + // set up grpc app + dummy = NewDummyApplication() + gclient, gserver, err := makeGRPCClientServer(dummy, "dummy-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)) +} diff --git a/example/dummy/persistent_kvstore.go b/example/dummy/persistent_kvstore.go new file mode 100644 index 000000000..30885bc1e --- /dev/null +++ b/example/dummy/persistent_kvstore.go @@ -0,0 +1,205 @@ +package dummy + +import ( + "bytes" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/tendermint/abci/example/code" + "github.com/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 = (*PersistentDummyApplication)(nil) + +type PersistentDummyApplication struct { + app *DummyApplication + + // validator set + ValUpdates []types.Validator + + logger log.Logger +} + +func NewPersistentDummyApplication(dbDir string) *PersistentDummyApplication { + name := "dummy" + db, err := dbm.NewGoLevelDB(name, dbDir) + if err != nil { + panic(err) + } + + state := loadState(db) + + return &PersistentDummyApplication{ + app: &DummyApplication{state: state}, + logger: log.NewNopLogger(), + } +} + +func (app *PersistentDummyApplication) SetLogger(l log.Logger) { + app.logger = l +} + +func (app *PersistentDummyApplication) 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 *PersistentDummyApplication) 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 *PersistentDummyApplication) 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 *PersistentDummyApplication) CheckTx(tx []byte) types.ResponseCheckTx { + return app.app.CheckTx(tx) +} + +// Commit will panic if InitChain was not called +func (app *PersistentDummyApplication) Commit() types.ResponseCommit { + return app.app.Commit() +} + +func (app *PersistentDummyApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { + return app.app.Query(reqQuery) +} + +// Save the validators in the merkle tree +func (app *PersistentDummyApplication) 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 *PersistentDummyApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { + // reset valset changes + app.ValUpdates = make([]types.Validator, 0) + return types.ResponseBeginBlock{} +} + +// Update the validator set +func (app *PersistentDummyApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { + return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} +} + +//--------------------------------------------- +// update validators + +func (app *PersistentDummyApplication) 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 []byte, power int64) []byte { + return []byte(cmn.Fmt("val:%X/%d", pubkey, power)) +} + +func isValidatorTx(tx []byte) bool { + return strings.HasPrefix(string(tx), ValidatorSetChangePrefix) +} + +// format is "val:pubkey1/power1,addr2/power2,addr3/power3"tx +func (app *PersistentDummyApplication) 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, ensuring its go-crypto encoded + pubkey, err := hex.DecodeString(pubkeyS) + if err != nil { + return types.ResponseDeliverTx{ + Code: code.CodeTypeEncodingError, + Log: fmt.Sprintf("Pubkey (%s) is invalid hex", pubkeyS)} + } + /*_, err = crypto.PubKeyFromBytes(pubkey) + if err != nil { + return types.ResponseDeliverTx{ + Code: code.CodeTypeEncodingError, + Log: fmt.Sprintf("Pubkey (%X) is invalid go-crypto encoded", pubkey)} + }*/ + + // 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.Validator{pubkey, power}) +} + +// add, update, or remove a validator +func (app *PersistentDummyApplication) updateValidator(v types.Validator) types.ResponseDeliverTx { + key := []byte("val:" + string(v.PubKey)) + 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} +}