v0.3.0, for Tendermint v0.8.0pull/1780/head
@ -1,7 +1,7 @@ | |||
package tmspcli | |||
package abcicli | |||
import ( | |||
"github.com/tendermint/go-logger" | |||
) | |||
var log = logger.New("module", "tmspcli") | |||
var log = logger.New("module", "abcicli") |
@ -0,0 +1,76 @@ | |||
package main | |||
import ( | |||
"flag" | |||
. "github.com/tendermint/go-common" | |||
"github.com/tendermint/abci/server" | |||
"github.com/tendermint/abci/types" | |||
) | |||
func main() { | |||
addrPtr := flag.String("addr", "tcp://0.0.0.0:46658", "Listen address") | |||
abciPtr := flag.String("abci", "socket", "socket | grpc") | |||
flag.Parse() | |||
// Start the listener | |||
srv, err := server.NewServer(*addrPtr, *abciPtr, NewChainAwareApplication()) | |||
if err != nil { | |||
Exit(err.Error()) | |||
} | |||
// Wait forever | |||
TrapSignal(func() { | |||
// Cleanup | |||
srv.Stop() | |||
}) | |||
} | |||
type ChainAwareApplication struct { | |||
beginCount int | |||
endCount int | |||
} | |||
func NewChainAwareApplication() *ChainAwareApplication { | |||
return &ChainAwareApplication{} | |||
} | |||
func (app *ChainAwareApplication) Info() types.ResponseInfo { | |||
return types.ResponseInfo{} | |||
} | |||
func (app *ChainAwareApplication) SetOption(key string, value string) (log string) { | |||
return "" | |||
} | |||
func (app *ChainAwareApplication) DeliverTx(tx []byte) types.Result { | |||
return types.NewResultOK(nil, "") | |||
} | |||
func (app *ChainAwareApplication) CheckTx(tx []byte) types.Result { | |||
return types.NewResultOK(nil, "") | |||
} | |||
func (app *ChainAwareApplication) Commit() types.Result { | |||
return types.NewResultOK([]byte("nil"), "") | |||
} | |||
func (app *ChainAwareApplication) Query(query []byte) types.Result { | |||
return types.NewResultOK([]byte(Fmt("%d,%d", app.beginCount, app.endCount)), "") | |||
} | |||
func (app *ChainAwareApplication) BeginBlock(hash []byte, header *types.Header) { | |||
app.beginCount += 1 | |||
return | |||
} | |||
func (app *ChainAwareApplication) EndBlock(height uint64) (resEndBlock types.ResponseEndBlock) { | |||
app.endCount += 1 | |||
return | |||
} | |||
func (app *ChainAwareApplication) InitChain(vals []*types.Validator) { | |||
return | |||
} |
@ -0,0 +1,54 @@ | |||
package main | |||
import ( | |||
"strconv" | |||
"strings" | |||
"testing" | |||
. "github.com/tendermint/go-common" | |||
"github.com/tendermint/abci/client" | |||
"github.com/tendermint/abci/server" | |||
"github.com/tendermint/abci/types" | |||
) | |||
func TestChainAware(t *testing.T) { | |||
app := NewChainAwareApplication() | |||
// Start the listener | |||
srv, err := server.NewServer("unix://test.sock", "socket", app) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
defer srv.Stop() | |||
// Connect to the socket | |||
client, err := abcicli.NewSocketClient("unix://test.sock", false) | |||
if err != nil { | |||
Exit(Fmt("Error starting socket client: %v", err.Error())) | |||
} | |||
client.Start() | |||
defer client.Stop() | |||
n := uint64(5) | |||
hash := []byte("fake block hash") | |||
header := &types.Header{} | |||
for i := uint64(0); i < n; i++ { | |||
client.BeginBlockSync(hash, header) | |||
client.EndBlockSync(i) | |||
client.CommitSync() | |||
} | |||
r := app.Query(nil) | |||
spl := strings.Split(string(r.Data), ",") | |||
if len(spl) != 2 { | |||
t.Fatal("expected %d,%d ; got %s", n, n, string(r.Data)) | |||
} | |||
beginCount, _ := strconv.Atoi(spl[0]) | |||
endCount, _ := strconv.Atoi(spl[1]) | |||
if uint64(beginCount) != n { | |||
t.Fatalf("expected beginCount of %d, got %d", n, beginCount) | |||
} else if uint64(endCount) != n { | |||
t.Fatalf("expected endCount of %d, got %d", n, endCount) | |||
} | |||
} |
@ -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 ABCI-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`. | |||
@ -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/abci/types" | |||
) | |||
func testDummy(t *testing.T, dummy types.Application, tx []byte, key, value string) { | |||
if r := dummy.DeliverTx(tx); r.IsErr() { | |||
t.Fatal(r) | |||
} | |||
if r := dummy.DeliverTx(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", "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) | |||
height := uint64(0) | |||
resInfo := dummy.Info() | |||
if resInfo.LastBlockHeight != height { | |||
t.Fatalf("expected height of %d, got %d", height, resInfo.LastBlockHeight) | |||
} | |||
// 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() | |||
resInfo = dummy.Info() | |||
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 TestValSetChanges(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 := 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.DeliverTx(tx); r.IsErr() { | |||
t.Fatal(r) | |||
} | |||
} | |||
resEndBlock := dummyChain.EndBlock(height) | |||
dummy.Commit() | |||
valsEqual(t, diff, resEndBlock.Diffs) | |||
} | |||
// 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) | |||
} | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
package dummy | |||
import ( | |||
"github.com/tendermint/go-logger" | |||
) | |||
var log = logger.New("module", "dummy") |
@ -0,0 +1,229 @@ | |||
package dummy | |||
import ( | |||
"bytes" | |||
"encoding/hex" | |||
"strconv" | |||
"strings" | |||
. "github.com/tendermint/go-common" | |||
dbm "github.com/tendermint/go-db" | |||
"github.com/tendermint/go-merkle" | |||
"github.com/tendermint/go-wire" | |||
"github.com/tendermint/abci/types" | |||
) | |||
const ( | |||
ValidatorSetChangePrefix string = "val:" | |||
) | |||
//----------------------------------------- | |||
type PersistentDummyApplication struct { | |||
app *DummyApplication | |||
db dbm.DB | |||
// latest received | |||
// TODO: move to merkle tree? | |||
blockHeader *types.Header | |||
// validator set | |||
changes []*types.Validator | |||
} | |||
func NewPersistentDummyApplication(dbDir string) *PersistentDummyApplication { | |||
db := dbm.NewDB("dummy", "leveldb", dbDir) | |||
lastBlock := LoadLastBlock(db) | |||
stateTree := merkle.NewIAVLTree(0, db) | |||
stateTree.Load(lastBlock.AppHash) | |||
log.Notice("Loaded state", "block", lastBlock.Height, "root", stateTree.Hash()) | |||
return &PersistentDummyApplication{ | |||
app: &DummyApplication{state: stateTree}, | |||
db: db, | |||
} | |||
} | |||
func (app *PersistentDummyApplication) Info() (resInfo types.ResponseInfo) { | |||
resInfo = app.app.Info() | |||
lastBlock := LoadLastBlock(app.db) | |||
resInfo.LastBlockHeight = lastBlock.Height | |||
resInfo.LastBlockAppHash = lastBlock.AppHash | |||
return resInfo | |||
} | |||
func (app *PersistentDummyApplication) SetOption(key string, value string) (log string) { | |||
return app.app.SetOption(key, value) | |||
} | |||
// tx is either "key=value" or just arbitrary bytes | |||
func (app *PersistentDummyApplication) DeliverTx(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.DeliverTx(tx) | |||
} | |||
func (app *PersistentDummyApplication) CheckTx(tx []byte) types.Result { | |||
return app.app.CheckTx(tx) | |||
} | |||
func (app *PersistentDummyApplication) Commit() types.Result { | |||
// Save | |||
appHash := app.app.state.Save() | |||
log.Info("Saved state", "root", appHash) | |||
lastBlock := LastBlockInfo{ | |||
Height: app.blockHeader.Height, | |||
AppHash: appHash, // this hash will be in the next block header | |||
} | |||
SaveLastBlock(app.db, lastBlock) | |||
return types.NewResultOK(appHash, "") | |||
} | |||
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) { | |||
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.blockHeader = header | |||
// reset valset changes | |||
app.changes = make([]*types.Validator, 0) | |||
} | |||
// Update the validator set | |||
func (app *PersistentDummyApplication) EndBlock(height uint64) (resEndBlock types.ResponseEndBlock) { | |||
return types.ResponseEndBlock{Diffs: app.changes} | |||
} | |||
//----------------------------------------- | |||
// persist the last block info | |||
var lastBlockKey = []byte("lastblock") | |||
type LastBlockInfo struct { | |||
Height uint64 | |||
AppHash []byte | |||
} | |||
// Get the last block from the db | |||
func LoadLastBlock(db dbm.DB) (lastBlock LastBlockInfo) { | |||
buf := db.Get(lastBlockKey) | |||
if len(buf) != 0 { | |||
r, n, err := bytes.NewReader(buf), new(int), new(error) | |||
wire.ReadBinaryPtr(&lastBlock, r, 0, n, err) | |||
if *err != nil { | |||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED | |||
Exit(Fmt("Data has been corrupted or its spec has changed: %v\n", *err)) | |||
} | |||
// TODO: ensure that buf is completely read. | |||
} | |||
return lastBlock | |||
} | |||
func SaveLastBlock(db dbm.DB, lastBlock LastBlockInfo) { | |||
log.Notice("Saving block", "height", lastBlock.Height, "root", lastBlock.AppHash) | |||
buf, n, err := new(bytes.Buffer), new(int), new(error) | |||
wire.WriteBinary(lastBlock, buf, n, err) | |||
if *err != nil { | |||
// TODO | |||
PanicCrisis(*err) | |||
} | |||
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 | |||
} |
@ -1 +1 @@ | |||
This example has been moved here: https://github.com/tendermint/js-tmsp/tree/master/example | |||
This example has been moved here: https://github.com/tendermint/js-abci/tree/master/example |
@ -1,17 +1,17 @@ | |||
#! /bin/bash | |||
set -e | |||
# These tests spawn the counter app and server by execing the TMSP_APP command and run some simple client tests against it | |||
# These tests spawn the counter app and server by execing the ABCI_APP command and run some simple client tests against it | |||
ROOT=$GOPATH/src/github.com/tendermint/tmsp/tests/test_app | |||
ROOT=$GOPATH/src/github.com/tendermint/abci/tests/test_app | |||
cd $ROOT | |||
# test golang counter | |||
TMSP_APP="counter" go run *.go | |||
ABCI_APP="counter" go run *.go | |||
# test golang counter via grpc | |||
TMSP_APP="counter -tmsp=grpc" TMSP="grpc" go run *.go | |||
ABCI_APP="counter -abci=grpc" ABCI="grpc" go run *.go | |||
# test nodejs counter | |||
# TODO: fix node app | |||
#TMSP_APP="node $GOPATH/src/github.com/tendermint/js-tmsp/example/app.js" go test -test.run TestCounter | |||
#ABCI_APP="node $GOPATH/src/github.com/tendermint/js-abci/example/app.js" go test -test.run TestCounter |
@ -0,0 +1,10 @@ | |||
echo hello | |||
info | |||
commit | |||
deliver_tx "abc" | |||
info | |||
commit | |||
query "abc" | |||
deliver_tx "def=xyz" | |||
commit | |||
query "def" |
@ -0,0 +1,32 @@ | |||
> echo hello | |||
-> data: hello | |||
> info | |||
-> data: {"size":0} | |||
> commit | |||
-> data: 0x | |||
> deliver_tx "abc" | |||
-> code: OK | |||
> info | |||
-> data: {"size":1} | |||
> commit | |||
-> data: 0x750502FC7E84BBD788ED589624F06CFA871845D1 | |||
> query "abc" | |||
-> code: OK | |||
-> data: {"index":0,"value":"abc","valueHex":"616263","exists":true} | |||
> deliver_tx "def=xyz" | |||
-> code: OK | |||
> commit | |||
-> data: 0x76393B8A182E450286B0694C629ECB51B286EFD5 | |||
> query "def" | |||
-> code: OK | |||
-> data: {"index":1,"value":"xyz","valueHex":"78797a","exists":true} | |||
@ -1,10 +0,0 @@ | |||
echo hello | |||
info | |||
commit | |||
append_tx abc | |||
info | |||
commit | |||
query abc | |||
append_tx def=xyz | |||
commit | |||
query def |
@ -1,31 +0,0 @@ | |||
> echo hello | |||
-> data: {hello} | |||
> info | |||
-> data: {size:0} | |||
> commit | |||
> append_tx abc | |||
-> code: OK | |||
> info | |||
-> data: {size:1} | |||
> commit | |||
-> data: {750502FC7E84BBD788ED589624F06CFA871845D1} | |||
> query abc | |||
-> code: OK | |||
-> data: {Index=0 value=abc exists=true} | |||
> append_tx def=xyz | |||
-> code: OK | |||
> commit | |||
-> data: {76393B8A182E450286B0694C629ECB51B286EFD5} | |||
> query def | |||
-> code: OK | |||
-> data: {Index=1 value=xyz exists=true} | |||
@ -0,0 +1,41 @@ | |||
package types | |||
import ( | |||
"bytes" | |||
"github.com/tendermint/go-wire" | |||
) | |||
// 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 | |||
} | |||
//------------------------------------- | |||
type validatorPretty struct { | |||
PubKey []byte `json:"pub_key"` | |||
Power uint64 `json:"power"` | |||
} | |||
func ValidatorsString(vs Validators) string { | |||
s := make([]validatorPretty, len(vs)) | |||
for i, v := range vs { | |||
s[i] = validatorPretty{v.PubKey, v.Power} | |||
} | |||
return string(wire.JSONBytes(s)) | |||
} |
@ -0,0 +1,7 @@ | |||
package types | |||
const Maj = "0" | |||
const Min = "3" // ResponseInfo, ResponseEndBlock | |||
const Fix = "0" // | |||
const Version = Maj + "." + Min + "." + Fix |