From b200b82418ead1effd488e80bba19532bebce2ac Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 21 Nov 2016 23:42:42 -0500 Subject: [PATCH] dummy: valset changes and tests --- example/counter/counter.go | 15 +-- example/dummy/README.md | 31 +++++ example/dummy/dummy.go | 11 +- example/dummy/dummy_test.go | 203 ++++++++++++++++++++++++++++++ example/dummy/persistent_dummy.go | 110 +++++++++++++++- types/validators.go | 24 ++++ 6 files changed, 378 insertions(+), 16 deletions(-) create mode 100644 example/dummy/README.md create mode 100644 example/dummy/dummy_test.go create mode 100644 types/validators.go diff --git a/example/counter/counter.go b/example/counter/counter.go index 2596eb107..230556e18 100644 --- a/example/counter/counter.go +++ b/example/counter/counter.go @@ -2,7 +2,6 @@ package counter import ( "encoding/binary" - "fmt" . "github.com/tendermint/go-common" "github.com/tendermint/tmsp/types" @@ -35,11 +34,7 @@ func (app *CounterApplication) AppendTx(tx []byte) types.Result { copy(tx8[len(tx8)-len(tx):], tx) txValue := binary.BigEndian.Uint64(tx8) if txValue != uint64(app.txCount) { - return types.Result{ - Code: types.CodeType_BadNonce, - Data: nil, - Log: fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.txCount, txValue), - } + return types.ErrBadNonce.SetLog(Fmt("Invalid nonce. Expected %v, got %v", app.txCount, txValue)) } } app.txCount += 1 @@ -52,11 +47,7 @@ func (app *CounterApplication) CheckTx(tx []byte) types.Result { copy(tx8[len(tx8)-len(tx):], tx) txValue := binary.BigEndian.Uint64(tx8) if txValue < uint64(app.txCount) { - return types.Result{ - Code: types.CodeType_BadNonce, - Data: nil, - Log: fmt.Sprintf("Invalid nonce. Expected >= %v, got %v", app.txCount, txValue), - } + return types.ErrBadNonce.SetLog(Fmt("Invalid nonce. Expected >= %v, got %v", app.txCount, txValue)) } } return types.OK @@ -75,5 +66,5 @@ func (app *CounterApplication) Commit() types.Result { } func (app *CounterApplication) Query(query []byte) types.Result { - return types.NewResultOK(nil, fmt.Sprintf("Query is not supported")) + return types.NewResultOK(nil, Fmt("Query is not supported")) } diff --git a/example/dummy/README.md b/example/dummy/README.md new file mode 100644 index 000000000..19f82fda6 --- /dev/null +++ b/example/dummy/README.md @@ -0,0 +1,31 @@ +# Dummy + +There are two app's here: the DummyApplication and the PersistentDummyApplication. + +## DummyApplication + +The DummyApplication 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). + +## PersistentDummyApplication + +The PersistentDummyApplication wraps the DummyApplication +and provides two additional features: + +1) persistence of state across app restarts (using Tendermint's TMSP-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`. + diff --git a/example/dummy/dummy.go b/example/dummy/dummy.go index 0c6a90893..9bed40895 100644 --- a/example/dummy/dummy.go +++ b/example/dummy/dummy.go @@ -5,6 +5,7 @@ import ( . "github.com/tendermint/go-common" "github.com/tendermint/go-merkle" + "github.com/tendermint/go-wire" "github.com/tendermint/tmsp/types" ) @@ -51,6 +52,12 @@ func (app *DummyApplication) Commit() types.Result { func (app *DummyApplication) Query(query []byte) types.Result { index, value, exists := app.state.Get(query) - resStr := Fmt("Index=%v value=%v exists=%v", index, string(value), exists) - return types.NewResultOK([]byte(resStr), "") + queryResult := QueryResult{index, string(value), exists} + return types.NewResultOK(wire.JSONBytes(queryResult), "") +} + +type QueryResult struct { + Index int `json:"index"` + Value string `json:"value"` + Exists bool `json:"exists"` } diff --git a/example/dummy/dummy_test.go b/example/dummy/dummy_test.go new file mode 100644 index 000000000..e284f1258 --- /dev/null +++ b/example/dummy/dummy_test.go @@ -0,0 +1,203 @@ +package dummy + +import ( + "bytes" + "io/ioutil" + "sort" + "testing" + + . "github.com/tendermint/go-common" + "github.com/tendermint/go-crypto" + "github.com/tendermint/go-wire" + "github.com/tendermint/tmsp/types" +) + +func testDummy(t *testing.T, dummy types.Application, tx []byte, key, value string) { + if r := dummy.AppendTx(tx); r.IsErr() { + t.Fatal(r) + } + if r := dummy.AppendTx(tx); r.IsErr() { + t.Fatal(r) + } + + r := dummy.Query([]byte(key)) + if r.IsErr() { + t.Fatal(r) + } + + q := new(QueryResult) + if err := wire.ReadJSONBytes(r.Data, q); err != nil { + t.Fatal(err) + } + + if q.Value != value { + t.Fatalf("Got %s, expected %s", q.Value, 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", "tmsp-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", "tmsp-dummy-test") // TODO + if err != nil { + t.Fatal(err) + } + dummy := NewPersistentDummyApplication(dir) + height := uint64(0) + + _, _, lastBlockInfo, _ := dummy.Info() + if lastBlockInfo.BlockHeight != height { + t.Fatalf("expected height of %d, got %d", height, lastBlockInfo.BlockHeight) + } + + // make and apply block + height = uint64(1) + hash := []byte("foo") + header := &types.Header{ + Height: uint64(height), + } + dummy.BeginBlock(hash, header) + dummy.EndBlock(height) + dummy.Commit() + + _, _, lastBlockInfo, _ = dummy.Info() + if lastBlockInfo.BlockHeight != height { + t.Fatalf("expected height of %d, got %d", height, lastBlockInfo.BlockHeight) + } + +} + +// add a validator, remove a validator, update a validator +func TestValSetChanges(t *testing.T) { + dir, err := ioutil.TempDir("/tmp", "tmsp-dummy-test") // TODO + if err != nil { + t.Fatal(err) + } + dummy := NewPersistentDummyApplication(dir) + + // init with some validators + total := 10 + nInit := 5 + vals := make([]*types.Validator, total) + for i := 0; i < total; i++ { + pubkey := crypto.GenPrivKeyEd25519FromSecret([]byte(Fmt("test%d", i))).PubKey().Bytes() + power := RandInt() + vals[i] = &types.Validator{pubkey, uint64(power)} + } + // iniitalize with the first nInit + dummy.InitChain(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:len(vals1)]...) + 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 := uint64(heightInt) + hash := []byte("foo") + header := &types.Header{ + Height: height, + } + + dummyChain := dummy.(types.BlockchainAware) // hmm... + dummyChain.BeginBlock(hash, header) + for _, tx := range txs { + if r := dummy.AppendTx(tx); r.IsErr() { + t.Fatal(r) + } + } + diff2 := dummyChain.EndBlock(height) + dummy.Commit() + + valsEqual(t, diff, diff2) + +} + +// 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) + } + } +} diff --git a/example/dummy/persistent_dummy.go b/example/dummy/persistent_dummy.go index cb2171f55..7fdb0a248 100644 --- a/example/dummy/persistent_dummy.go +++ b/example/dummy/persistent_dummy.go @@ -2,6 +2,9 @@ package dummy import ( "bytes" + "encoding/hex" + "strconv" + "strings" . "github.com/tendermint/go-common" dbm "github.com/tendermint/go-db" @@ -10,6 +13,10 @@ import ( "github.com/tendermint/tmsp/types" ) +const ( + ValidatorSetChangePrefix string = "val:" +) + //----------------------------------------- type PersistentDummyApplication struct { @@ -17,8 +24,12 @@ type PersistentDummyApplication struct { db dbm.DB // latest received + // TODO: move to merkle tree? blockHash []byte blockHeader *types.Header + + // validator set + changes []*types.Validator } func NewPersistentDummyApplication(dbDir string) *PersistentDummyApplication { @@ -51,6 +62,15 @@ func (app *PersistentDummyApplication) SetOption(key string, value string) (log // tx is either "key=value" or just arbitrary bytes func (app *PersistentDummyApplication) AppendTx(tx []byte) types.Result { + // 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.changes + return app.execValidatorTx(tx) + } + + // otherwise, update the key-value store return app.app.AppendTx(tx) } @@ -76,17 +96,29 @@ func (app *PersistentDummyApplication) Query(query []byte) types.Result { return app.app.Query(query) } +// Save the validators in the merkle tree func (app *PersistentDummyApplication) InitChain(validators []*types.Validator) { - return + for _, v := range validators { + r := app.updateValidator(v) + if r.IsErr() { + log.Error("Error updating validators", "r", r) + } + } } +// Track the block hash and header information func (app *PersistentDummyApplication) BeginBlock(hash []byte, header *types.Header) { + // update latest block info app.blockHash = hash app.blockHeader = header + + // reset valset changes + app.changes = make([]*types.Validator, 0) } +// Update the validator set func (app *PersistentDummyApplication) EndBlock(height uint64) (diffs []*types.Validator) { - return nil + return app.changes } //----------------------------------------- @@ -120,3 +152,77 @@ func SaveLastBlock(db dbm.DB, lastBlock types.LastBlockInfo) { } db.Set(lastBlockKey, buf.Bytes()) } + +//--------------------------------------------- +// update validators + +func (app *PersistentDummyApplication) Validators() (validators []*types.Validator) { + app.app.state.Iterate(func(key, value []byte) bool { + if isValidatorTx(key) { + validator := new(types.Validator) + err := types.ReadMessage(bytes.NewBuffer(value), validator) + if err != nil { + panic(err) + } + validators = append(validators, validator) + } + return false + }) + return +} + +func MakeValSetChangeTx(pubkey []byte, power uint64) []byte { + return []byte(Fmt("val:%X/%d", pubkey, power)) +} + +func isValidatorTx(tx []byte) bool { + if strings.HasPrefix(string(tx), ValidatorSetChangePrefix) { + return true + } + return false +} + +// format is "val:pubkey1/power1,addr2/power2,addr3/power3"tx +func (app *PersistentDummyApplication) execValidatorTx(tx []byte) types.Result { + tx = tx[len(ValidatorSetChangePrefix):] + pubKeyAndPower := strings.Split(string(tx), "/") + if len(pubKeyAndPower) != 2 { + return types.ErrEncodingError.SetLog(Fmt("Expected 'pubkey/power'. Got %v", pubKeyAndPower)) + } + pubkeyS, powerS := pubKeyAndPower[0], pubKeyAndPower[1] + pubkey, err := hex.DecodeString(pubkeyS) + if err != nil { + return types.ErrEncodingError.SetLog(Fmt("Pubkey (%s) is invalid hex", pubkeyS)) + } + power, err := strconv.Atoi(powerS) + if err != nil { + return types.ErrEncodingError.SetLog(Fmt("Power (%s) is not an int", powerS)) + } + + // update + return app.updateValidator(&types.Validator{pubkey, uint64(power)}) +} + +// add, update, or remove a validator +func (app *PersistentDummyApplication) updateValidator(v *types.Validator) types.Result { + key := []byte("val:" + string(v.PubKey)) + if v.Power == 0 { + // remove validator + if !app.app.state.Has(key) { + return types.ErrUnauthorized.SetLog(Fmt("Cannot remove non-existent validator %X", key)) + } + app.app.state.Remove(key) + } else { + // add or update validator + value := bytes.NewBuffer(make([]byte, 0)) + if err := types.WriteMessage(v, value); err != nil { + return types.ErrInternalError.SetLog(Fmt("Error encoding validator: %v", err)) + } + app.app.state.Set(key, value.Bytes()) + } + + // we only update the changes array if we succesfully updated the tree + app.changes = append(app.changes, v) + + return types.OK +} diff --git a/types/validators.go b/types/validators.go new file mode 100644 index 000000000..6494394ee --- /dev/null +++ b/types/validators.go @@ -0,0 +1,24 @@ +package types + +import ( + "bytes" +) + +// validators implements sort + +type Validators []*Validator + +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, v[j].PubKey) <= 0 +} + +func (v Validators) Swap(i, j int) { + v1 := v[i] + v[i] = v[j] + v[j] = v1 +}