diff --git a/consensus/istanbul/validator/default.go b/consensus/istanbul/validator/default.go new file mode 100644 index 0000000000..17edda5521 --- /dev/null +++ b/consensus/istanbul/validator/default.go @@ -0,0 +1,201 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package validator + +import ( + "math" + "reflect" + "sort" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +type defaultValidator struct { + address common.Address +} + +func (val *defaultValidator) Address() common.Address { + return val.address +} + +func (val *defaultValidator) String() string { + return val.Address().String() +} + +// ---------------------------------------------------------------------------- + +type defaultSet struct { + validators istanbul.Validators + policy istanbul.ProposerPolicy + + proposer istanbul.Validator + validatorMu sync.RWMutex + selector istanbul.ProposalSelector +} + +func newDefaultSet(addrs []common.Address, policy istanbul.ProposerPolicy) *defaultSet { + valSet := &defaultSet{} + + valSet.policy = policy + // init validators + valSet.validators = make([]istanbul.Validator, len(addrs)) + for i, addr := range addrs { + valSet.validators[i] = New(addr) + } + // sort validator + sort.Sort(valSet.validators) + // init proposer + if valSet.Size() > 0 { + valSet.proposer = valSet.GetByIndex(0) + } + valSet.selector = roundRobinProposer + if policy == istanbul.Sticky { + valSet.selector = stickyProposer + } + + return valSet +} + +func (valSet *defaultSet) Size() int { + valSet.validatorMu.RLock() + defer valSet.validatorMu.RUnlock() + return len(valSet.validators) +} + +func (valSet *defaultSet) List() []istanbul.Validator { + valSet.validatorMu.RLock() + defer valSet.validatorMu.RUnlock() + return valSet.validators +} + +func (valSet *defaultSet) GetByIndex(i uint64) istanbul.Validator { + valSet.validatorMu.RLock() + defer valSet.validatorMu.RUnlock() + if i < uint64(valSet.Size()) { + return valSet.validators[i] + } + return nil +} + +func (valSet *defaultSet) GetByAddress(addr common.Address) (int, istanbul.Validator) { + for i, val := range valSet.List() { + if addr == val.Address() { + return i, val + } + } + return -1, nil +} + +func (valSet *defaultSet) GetProposer() istanbul.Validator { + return valSet.proposer +} + +func (valSet *defaultSet) IsProposer(address common.Address) bool { + _, val := valSet.GetByAddress(address) + return reflect.DeepEqual(valSet.GetProposer(), val) +} + +func (valSet *defaultSet) CalcProposer(lastProposer common.Address, round uint64) { + valSet.validatorMu.RLock() + defer valSet.validatorMu.RUnlock() + valSet.proposer = valSet.selector(valSet, lastProposer, round) +} + +func calcSeed(valSet istanbul.ValidatorSet, proposer common.Address, round uint64) uint64 { + offset := 0 + if idx, val := valSet.GetByAddress(proposer); val != nil { + offset = idx + } + return uint64(offset) + round +} + +func emptyAddress(addr common.Address) bool { + return addr == common.Address{} +} + +func roundRobinProposer(valSet istanbul.ValidatorSet, proposer common.Address, round uint64) istanbul.Validator { + if valSet.Size() == 0 { + return nil + } + seed := uint64(0) + if emptyAddress(proposer) { + seed = round + } else { + seed = calcSeed(valSet, proposer, round) + 1 + } + pick := seed % uint64(valSet.Size()) + return valSet.GetByIndex(pick) +} + +func stickyProposer(valSet istanbul.ValidatorSet, proposer common.Address, round uint64) istanbul.Validator { + if valSet.Size() == 0 { + return nil + } + seed := uint64(0) + if emptyAddress(proposer) { + seed = round + } else { + seed = calcSeed(valSet, proposer, round) + } + pick := seed % uint64(valSet.Size()) + return valSet.GetByIndex(pick) +} + +func (valSet *defaultSet) AddValidator(address common.Address) bool { + valSet.validatorMu.Lock() + defer valSet.validatorMu.Unlock() + for _, v := range valSet.validators { + if v.Address() == address { + return false + } + } + valSet.validators = append(valSet.validators, New(address)) + // TODO: we may not need to re-sort it again + // sort validator + sort.Sort(valSet.validators) + return true +} + +func (valSet *defaultSet) RemoveValidator(address common.Address) bool { + valSet.validatorMu.Lock() + defer valSet.validatorMu.Unlock() + + for i, v := range valSet.validators { + if v.Address() == address { + valSet.validators = append(valSet.validators[:i], valSet.validators[i+1:]...) + return true + } + } + return false +} + +func (valSet *defaultSet) Copy() istanbul.ValidatorSet { + valSet.validatorMu.RLock() + defer valSet.validatorMu.RUnlock() + + addresses := make([]common.Address, 0, len(valSet.validators)) + for _, v := range valSet.validators { + addresses = append(addresses, v.Address()) + } + return NewSet(addresses, valSet.policy) +} + +func (valSet *defaultSet) F() int { return int(math.Ceil(float64(valSet.Size())/3)) - 1 } + +func (valSet *defaultSet) Policy() istanbul.ProposerPolicy { return valSet.policy } diff --git a/consensus/istanbul/validator/default_test.go b/consensus/istanbul/validator/default_test.go new file mode 100644 index 0000000000..987ed12c84 --- /dev/null +++ b/consensus/istanbul/validator/default_test.go @@ -0,0 +1,208 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package validator + +import ( + "reflect" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + testAddress = "70524d664ffe731100208a0154e556f9bb679ae6" + testAddress2 = "b37866a925bccd69cfa98d43b510f1d23d78a851" +) + +func TestValidatorSet(t *testing.T) { + testNewValidatorSet(t) + testNormalValSet(t) + testEmptyValSet(t) + testStickyProposer(t) + testAddAndRemoveValidator(t) +} + +func testNewValidatorSet(t *testing.T) { + var validators []istanbul.Validator + const ValCnt = 100 + + // Create 100 validators with random addresses + b := []byte{} + for i := 0; i < ValCnt; i++ { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + val := New(addr) + validators = append(validators, val) + b = append(b, val.Address().Bytes()...) + } + + // Create ValidatorSet + valSet := NewSet(ExtractValidators(b), istanbul.RoundRobin) + if valSet == nil { + t.Errorf("the validator byte array cannot be parsed") + t.FailNow() + } + + // Check validators sorting: should be in ascending order + for i := 0; i < ValCnt-1; i++ { + val := valSet.GetByIndex(uint64(i)) + nextVal := valSet.GetByIndex(uint64(i + 1)) + if strings.Compare(val.String(), nextVal.String()) >= 0 { + t.Errorf("validator set is not sorted in ascending order") + } + } +} + +func testNormalValSet(t *testing.T) { + b1 := common.Hex2Bytes(testAddress) + b2 := common.Hex2Bytes(testAddress2) + addr1 := common.BytesToAddress(b1) + addr2 := common.BytesToAddress(b2) + val1 := New(addr1) + val2 := New(addr2) + + valSet := newDefaultSet([]common.Address{addr1, addr2}, istanbul.RoundRobin) + if valSet == nil { + t.Errorf("the format of validator set is invalid") + t.FailNow() + } + + // check size + if size := valSet.Size(); size != 2 { + t.Errorf("the size of validator set is wrong: have %v, want 2", size) + } + // test get by index + if val := valSet.GetByIndex(uint64(0)); !reflect.DeepEqual(val, val1) { + t.Errorf("validator mismatch: have %v, want %v", val, val1) + } + // test get by invalid index + if val := valSet.GetByIndex(uint64(2)); val != nil { + t.Errorf("validator mismatch: have %v, want nil", val) + } + // test get by address + if _, val := valSet.GetByAddress(addr2); !reflect.DeepEqual(val, val2) { + t.Errorf("validator mismatch: have %v, want %v", val, val2) + } + // test get by invalid address + invalidAddr := common.HexToAddress("0x9535b2e7faaba5288511d89341d94a38063a349b") + if _, val := valSet.GetByAddress(invalidAddr); val != nil { + t.Errorf("validator mismatch: have %v, want nil", val) + } + // test get proposer + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val1) { + t.Errorf("proposer mismatch: have %v, want %v", val, val1) + } + // test calculate proposer + lastProposer := addr1 + valSet.CalcProposer(lastProposer, uint64(0)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val2) { + t.Errorf("proposer mismatch: have %v, want %v", val, val2) + } + valSet.CalcProposer(lastProposer, uint64(3)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val1) { + t.Errorf("proposer mismatch: have %v, want %v", val, val1) + } + // test empty last proposer + lastProposer = common.Address{} + valSet.CalcProposer(lastProposer, uint64(3)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val2) { + t.Errorf("proposer mismatch: have %v, want %v", val, val2) + } +} + +func testEmptyValSet(t *testing.T) { + valSet := NewSet(ExtractValidators([]byte{}), istanbul.RoundRobin) + if valSet == nil { + t.Errorf("validator set should not be nil") + } +} + +func testAddAndRemoveValidator(t *testing.T) { + valSet := NewSet(ExtractValidators([]byte{}), istanbul.RoundRobin) + if !valSet.AddValidator(common.StringToAddress(string(2))) { + t.Error("the validator should be added") + } + if valSet.AddValidator(common.StringToAddress(string(2))) { + t.Error("the existing validator should not be added") + } + valSet.AddValidator(common.StringToAddress(string(1))) + valSet.AddValidator(common.StringToAddress(string(0))) + if len(valSet.List()) != 3 { + t.Error("the size of validator set should be 3") + } + + for i, v := range valSet.List() { + expected := common.StringToAddress(string(i)) + if v.Address() != expected { + t.Errorf("the order of validators is wrong: have %v, want %v", v.Address().Hex(), expected.Hex()) + } + } + + if !valSet.RemoveValidator(common.StringToAddress(string(2))) { + t.Error("the validator should be removed") + } + if valSet.RemoveValidator(common.StringToAddress(string(2))) { + t.Error("the non-existing validator should not be removed") + } + if len(valSet.List()) != 2 { + t.Error("the size of validator set should be 2") + } + valSet.RemoveValidator(common.StringToAddress(string(1))) + if len(valSet.List()) != 1 { + t.Error("the size of validator set should be 1") + } + valSet.RemoveValidator(common.StringToAddress(string(0))) + if len(valSet.List()) != 0 { + t.Error("the size of validator set should be 0") + } +} + +func testStickyProposer(t *testing.T) { + b1 := common.Hex2Bytes(testAddress) + b2 := common.Hex2Bytes(testAddress2) + addr1 := common.BytesToAddress(b1) + addr2 := common.BytesToAddress(b2) + val1 := New(addr1) + val2 := New(addr2) + + valSet := newDefaultSet([]common.Address{addr1, addr2}, istanbul.Sticky) + + // test get proposer + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val1) { + t.Errorf("proposer mismatch: have %v, want %v", val, val1) + } + // test calculate proposer + lastProposer := addr1 + valSet.CalcProposer(lastProposer, uint64(0)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val1) { + t.Errorf("proposer mismatch: have %v, want %v", val, val1) + } + + valSet.CalcProposer(lastProposer, uint64(1)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val2) { + t.Errorf("proposer mismatch: have %v, want %v", val, val2) + } + // test empty last proposer + lastProposer = common.Address{} + valSet.CalcProposer(lastProposer, uint64(3)) + if val := valSet.GetProposer(); !reflect.DeepEqual(val, val2) { + t.Errorf("proposer mismatch: have %v, want %v", val, val2) + } +} diff --git a/consensus/istanbul/validator/validator.go b/consensus/istanbul/validator/validator.go new file mode 100644 index 0000000000..9a1e15c2d8 --- /dev/null +++ b/consensus/istanbul/validator/validator.go @@ -0,0 +1,47 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package validator + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/istanbul" +) + +func New(addr common.Address) istanbul.Validator { + return &defaultValidator{ + address: addr, + } +} + +func NewSet(addrs []common.Address, policy istanbul.ProposerPolicy) istanbul.ValidatorSet { + return newDefaultSet(addrs, policy) +} + +func ExtractValidators(extraData []byte) []common.Address { + // get the validator addresses + addrs := make([]common.Address, (len(extraData) / common.AddressLength)) + for i := 0; i < len(addrs); i++ { + copy(addrs[i][:], extraData[i*common.AddressLength:]) + } + + return addrs +} + +// Check whether the extraData is presented in prescribed form +func ValidExtraData(extraData []byte) bool { + return len(extraData)%common.AddressLength == 0 +}