Browse Source

dummy: valset changes and tests

pull/1780/head
Ethan Buchman 8 years ago
parent
commit
b200b82418
6 changed files with 378 additions and 16 deletions
  1. +3
    -12
      example/counter/counter.go
  2. +31
    -0
      example/dummy/README.md
  3. +9
    -2
      example/dummy/dummy.go
  4. +203
    -0
      example/dummy/dummy_test.go
  5. +108
    -2
      example/dummy/persistent_dummy.go
  6. +24
    -0
      types/validators.go

+ 3
- 12
example/counter/counter.go View File

@ -2,7 +2,6 @@ package counter
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
. "github.com/tendermint/go-common" . "github.com/tendermint/go-common"
"github.com/tendermint/tmsp/types" "github.com/tendermint/tmsp/types"
@ -35,11 +34,7 @@ func (app *CounterApplication) AppendTx(tx []byte) types.Result {
copy(tx8[len(tx8)-len(tx):], tx) copy(tx8[len(tx8)-len(tx):], tx)
txValue := binary.BigEndian.Uint64(tx8) txValue := binary.BigEndian.Uint64(tx8)
if txValue != uint64(app.txCount) { 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 app.txCount += 1
@ -52,11 +47,7 @@ func (app *CounterApplication) CheckTx(tx []byte) types.Result {
copy(tx8[len(tx8)-len(tx):], tx) copy(tx8[len(tx8)-len(tx):], tx)
txValue := binary.BigEndian.Uint64(tx8) txValue := binary.BigEndian.Uint64(tx8)
if txValue < uint64(app.txCount) { 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 return types.OK
@ -75,5 +66,5 @@ func (app *CounterApplication) Commit() types.Result {
} }
func (app *CounterApplication) Query(query []byte) 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"))
} }

+ 31
- 0
example/dummy/README.md View File

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

+ 9
- 2
example/dummy/dummy.go View File

@ -5,6 +5,7 @@ import (
. "github.com/tendermint/go-common" . "github.com/tendermint/go-common"
"github.com/tendermint/go-merkle" "github.com/tendermint/go-merkle"
"github.com/tendermint/go-wire"
"github.com/tendermint/tmsp/types" "github.com/tendermint/tmsp/types"
) )
@ -51,6 +52,12 @@ func (app *DummyApplication) Commit() types.Result {
func (app *DummyApplication) Query(query []byte) types.Result { func (app *DummyApplication) Query(query []byte) types.Result {
index, value, exists := app.state.Get(query) 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"`
} }

+ 203
- 0
example/dummy/dummy_test.go View File

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

+ 108
- 2
example/dummy/persistent_dummy.go View File

@ -2,6 +2,9 @@ package dummy
import ( import (
"bytes" "bytes"
"encoding/hex"
"strconv"
"strings"
. "github.com/tendermint/go-common" . "github.com/tendermint/go-common"
dbm "github.com/tendermint/go-db" dbm "github.com/tendermint/go-db"
@ -10,6 +13,10 @@ import (
"github.com/tendermint/tmsp/types" "github.com/tendermint/tmsp/types"
) )
const (
ValidatorSetChangePrefix string = "val:"
)
//----------------------------------------- //-----------------------------------------
type PersistentDummyApplication struct { type PersistentDummyApplication struct {
@ -17,8 +24,12 @@ type PersistentDummyApplication struct {
db dbm.DB db dbm.DB
// latest received // latest received
// TODO: move to merkle tree?
blockHash []byte blockHash []byte
blockHeader *types.Header blockHeader *types.Header
// validator set
changes []*types.Validator
} }
func NewPersistentDummyApplication(dbDir string) *PersistentDummyApplication { 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 // tx is either "key=value" or just arbitrary bytes
func (app *PersistentDummyApplication) AppendTx(tx []byte) types.Result { 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) return app.app.AppendTx(tx)
} }
@ -76,17 +96,29 @@ func (app *PersistentDummyApplication) Query(query []byte) types.Result {
return app.app.Query(query) return app.app.Query(query)
} }
// Save the validators in the merkle tree
func (app *PersistentDummyApplication) InitChain(validators []*types.Validator) { 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) { func (app *PersistentDummyApplication) BeginBlock(hash []byte, header *types.Header) {
// update latest block info
app.blockHash = hash app.blockHash = hash
app.blockHeader = header 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) { 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()) 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
}

+ 24
- 0
types/validators.go View File

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

Loading…
Cancel
Save