mirror of
https://github.com/fluencelabs/tendermint
synced 2025-04-25 14:52:17 +00:00
dummy: valset changes and tests
This commit is contained in:
parent
9a2d3e51ed
commit
b200b82418
@ -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"))
|
||||
}
|
||||
|
31
example/dummy/README.md
Normal file
31
example/dummy/README.md
Normal 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`.
|
||||
|
@ -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"`
|
||||
}
|
||||
|
203
example/dummy/dummy_test.go
Normal file
203
example/dummy/dummy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
24
types/validators.go
Normal file
24
types/validators.go
Normal 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…
x
Reference in New Issue
Block a user