mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-18 18:02:24 +00:00
consensus/istanbul: Istanbul consensus backend implementation
This commit is contained in:
parent
efd8b5be6f
commit
72ae3ec221
9 changed files with 2886 additions and 0 deletions
118
consensus/istanbul/backend/api.go
Normal file
118
consensus/istanbul/backend/api.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// API is a user facing RPC API to dump Istanbul state
|
||||
type API struct {
|
||||
chain consensus.ChainReader
|
||||
istanbul *backend
|
||||
}
|
||||
|
||||
// GetSnapshot retrieves the state snapshot at a given block.
|
||||
func (api *API) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, error) {
|
||||
// Retrieve the requested block number (or current if none requested)
|
||||
var header *types.Header
|
||||
if number == nil || *number == rpc.LatestBlockNumber {
|
||||
header = api.chain.CurrentHeader()
|
||||
} else {
|
||||
header = api.chain.GetHeaderByNumber(uint64(number.Int64()))
|
||||
}
|
||||
// Ensure we have an actually valid block and return its snapshot
|
||||
if header == nil {
|
||||
return nil, errUnknownBlock
|
||||
}
|
||||
return api.istanbul.snapshot(api.chain, header.Number.Uint64(), header.Hash(), nil)
|
||||
}
|
||||
|
||||
// GetSnapshotAtHash retrieves the state snapshot at a given block.
|
||||
func (api *API) GetSnapshotAtHash(hash common.Hash) (*Snapshot, error) {
|
||||
header := api.chain.GetHeaderByHash(hash)
|
||||
if header == nil {
|
||||
return nil, errUnknownBlock
|
||||
}
|
||||
return api.istanbul.snapshot(api.chain, header.Number.Uint64(), header.Hash(), nil)
|
||||
}
|
||||
|
||||
// GetValidators retrieves the list of authorized validators at the specified block.
|
||||
func (api *API) GetValidators(number *rpc.BlockNumber) ([]common.Address, error) {
|
||||
// Retrieve the requested block number (or current if none requested)
|
||||
var header *types.Header
|
||||
if number == nil || *number == rpc.LatestBlockNumber {
|
||||
header = api.chain.CurrentHeader()
|
||||
} else {
|
||||
header = api.chain.GetHeaderByNumber(uint64(number.Int64()))
|
||||
}
|
||||
// Ensure we have an actually valid block and return the validators from its snapshot
|
||||
if header == nil {
|
||||
return nil, errUnknownBlock
|
||||
}
|
||||
snap, err := api.istanbul.snapshot(api.chain, header.Number.Uint64(), header.Hash(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snap.validators(), nil
|
||||
}
|
||||
|
||||
// GetValidatorsAtHash retrieves the state snapshot at a given block.
|
||||
func (api *API) GetValidatorsAtHash(hash common.Hash) ([]common.Address, error) {
|
||||
header := api.chain.GetHeaderByHash(hash)
|
||||
if header == nil {
|
||||
return nil, errUnknownBlock
|
||||
}
|
||||
snap, err := api.istanbul.snapshot(api.chain, header.Number.Uint64(), header.Hash(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snap.validators(), nil
|
||||
}
|
||||
|
||||
// Candidates returns the current candidates the node tries to uphold and vote on.
|
||||
func (api *API) Candidates() map[common.Address]bool {
|
||||
api.istanbul.candidatesLock.RLock()
|
||||
defer api.istanbul.candidatesLock.RUnlock()
|
||||
|
||||
proposals := make(map[common.Address]bool)
|
||||
for address, auth := range api.istanbul.candidates {
|
||||
proposals[address] = auth
|
||||
}
|
||||
return proposals
|
||||
}
|
||||
|
||||
// Propose injects a new authorization candidate that the validator will attempt to
|
||||
// push through.
|
||||
func (api *API) Propose(address common.Address, auth bool) {
|
||||
api.istanbul.candidatesLock.Lock()
|
||||
defer api.istanbul.candidatesLock.Unlock()
|
||||
|
||||
api.istanbul.candidates[address] = auth
|
||||
}
|
||||
|
||||
// Discard drops a currently running candidate, stopping the validator from casting
|
||||
// further votes (either for or against).
|
||||
func (api *API) Discard(address common.Address) {
|
||||
api.istanbul.candidatesLock.Lock()
|
||||
defer api.istanbul.candidatesLock.Unlock()
|
||||
|
||||
delete(api.istanbul.candidates, address)
|
||||
}
|
||||
312
consensus/istanbul/backend/backend.go
Normal file
312
consensus/istanbul/backend/backend.go
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
istanbulCore "github.com/ethereum/go-ethereum/consensus/istanbul/core"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
const (
|
||||
// fetcherID is the ID indicates the block is from Istanbul engine
|
||||
fetcherID = "istanbul"
|
||||
)
|
||||
|
||||
// New creates an Ethereum backend for Istanbul core engine.
|
||||
func New(config *istanbul.Config, privateKey *ecdsa.PrivateKey, db ethdb.Database) consensus.Istanbul {
|
||||
// Allocate the snapshot caches and create the engine
|
||||
recents, _ := lru.NewARC(inmemorySnapshots)
|
||||
recentMessages, _ := lru.NewARC(inmemoryPeers)
|
||||
knownMessages, _ := lru.NewARC(inmemoryMessages)
|
||||
backend := &backend{
|
||||
config: config,
|
||||
istanbulEventMux: new(event.TypeMux),
|
||||
privateKey: privateKey,
|
||||
address: crypto.PubkeyToAddress(privateKey.PublicKey),
|
||||
logger: log.New(),
|
||||
db: db,
|
||||
commitCh: make(chan *types.Block, 1),
|
||||
recents: recents,
|
||||
candidates: make(map[common.Address]bool),
|
||||
coreStarted: false,
|
||||
recentMessages: recentMessages,
|
||||
knownMessages: knownMessages,
|
||||
}
|
||||
backend.core = istanbulCore.New(backend, backend.config)
|
||||
return backend
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type backend struct {
|
||||
config *istanbul.Config
|
||||
istanbulEventMux *event.TypeMux
|
||||
privateKey *ecdsa.PrivateKey
|
||||
address common.Address
|
||||
core istanbulCore.Engine
|
||||
logger log.Logger
|
||||
db ethdb.Database
|
||||
chain consensus.ChainReader
|
||||
currentBlock func() *types.Block
|
||||
hasBadBlock func(hash common.Hash) bool
|
||||
|
||||
// the channels for istanbul engine notifications
|
||||
commitCh chan *types.Block
|
||||
proposedBlockHash common.Hash
|
||||
sealMu sync.Mutex
|
||||
coreStarted bool
|
||||
coreMu sync.RWMutex
|
||||
|
||||
// Current list of candidates we are pushing
|
||||
candidates map[common.Address]bool
|
||||
// Protects the signer fields
|
||||
candidatesLock sync.RWMutex
|
||||
// Snapshots for recent block to speed up reorgs
|
||||
recents *lru.ARCCache
|
||||
|
||||
// event subscription for ChainHeadEvent event
|
||||
broadcaster consensus.Broadcaster
|
||||
|
||||
recentMessages *lru.ARCCache // the cache of peer's messages
|
||||
knownMessages *lru.ARCCache // the cache of self messages
|
||||
}
|
||||
|
||||
// Address implements istanbul.Backend.Address
|
||||
func (sb *backend) Address() common.Address {
|
||||
return sb.address
|
||||
}
|
||||
|
||||
// Validators implements istanbul.Backend.Validators
|
||||
func (sb *backend) Validators(proposal istanbul.Proposal) istanbul.ValidatorSet {
|
||||
return sb.getValidators(proposal.Number().Uint64(), proposal.Hash())
|
||||
}
|
||||
|
||||
// Broadcast implements istanbul.Backend.Broadcast
|
||||
func (sb *backend) Broadcast(valSet istanbul.ValidatorSet, payload []byte) error {
|
||||
// send to others
|
||||
sb.Gossip(valSet, payload)
|
||||
// send to self
|
||||
msg := istanbul.MessageEvent{
|
||||
Payload: payload,
|
||||
}
|
||||
go sb.istanbulEventMux.Post(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Broadcast implements istanbul.Backend.Gossip
|
||||
func (sb *backend) Gossip(valSet istanbul.ValidatorSet, payload []byte) error {
|
||||
hash := istanbul.RLPHash(payload)
|
||||
sb.knownMessages.Add(hash, true)
|
||||
|
||||
targets := make(map[common.Address]bool)
|
||||
for _, val := range valSet.List() {
|
||||
if val.Address() != sb.Address() {
|
||||
targets[val.Address()] = true
|
||||
}
|
||||
}
|
||||
|
||||
if sb.broadcaster != nil && len(targets) > 0 {
|
||||
ps := sb.broadcaster.FindPeers(targets)
|
||||
for addr, p := range ps {
|
||||
ms, ok := sb.recentMessages.Get(addr)
|
||||
var m *lru.ARCCache
|
||||
if ok {
|
||||
m, _ = ms.(*lru.ARCCache)
|
||||
if _, k := m.Get(hash); k {
|
||||
// This peer had this event, skip it
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
m, _ = lru.NewARC(inmemoryMessages)
|
||||
}
|
||||
|
||||
m.Add(hash, true)
|
||||
sb.recentMessages.Add(addr, m)
|
||||
|
||||
go p.Send(istanbulMsg, payload)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit implements istanbul.Backend.Commit
|
||||
func (sb *backend) Commit(proposal istanbul.Proposal, seals [][]byte) error {
|
||||
// Check if the proposal is a valid block
|
||||
block := &types.Block{}
|
||||
block, ok := proposal.(*types.Block)
|
||||
if !ok {
|
||||
sb.logger.Error("Invalid proposal, %v", proposal)
|
||||
return errInvalidProposal
|
||||
}
|
||||
|
||||
h := block.Header()
|
||||
// Append seals into extra-data
|
||||
err := writeCommittedSeals(h, seals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// update block's header
|
||||
block = block.WithSeal(h)
|
||||
|
||||
sb.logger.Info("Committed", "address", sb.Address(), "hash", proposal.Hash(), "number", proposal.Number().Uint64())
|
||||
// - if the proposed and committed blocks are the same, send the proposed hash
|
||||
// to commit channel, which is being watched inside the engine.Seal() function.
|
||||
// - otherwise, we try to insert the block.
|
||||
// -- if success, the ChainHeadEvent event will be broadcasted, try to build
|
||||
// the next block and the previous Seal() will be stopped.
|
||||
// -- otherwise, a error will be returned and a round change event will be fired.
|
||||
if sb.proposedBlockHash == block.Hash() {
|
||||
// feed block hash to Seal() and wait the Seal() result
|
||||
sb.commitCh <- block
|
||||
return nil
|
||||
}
|
||||
|
||||
if sb.broadcaster != nil {
|
||||
sb.broadcaster.Enqueue(fetcherID, block)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EventMux implements istanbul.Backend.EventMux
|
||||
func (sb *backend) EventMux() *event.TypeMux {
|
||||
return sb.istanbulEventMux
|
||||
}
|
||||
|
||||
// Verify implements istanbul.Backend.Verify
|
||||
func (sb *backend) Verify(proposal istanbul.Proposal) (time.Duration, error) {
|
||||
// Check if the proposal is a valid block
|
||||
block := &types.Block{}
|
||||
block, ok := proposal.(*types.Block)
|
||||
if !ok {
|
||||
sb.logger.Error("Invalid proposal, %v", proposal)
|
||||
return 0, errInvalidProposal
|
||||
}
|
||||
|
||||
// check bad block
|
||||
if sb.HasBadProposal(block.Hash()) {
|
||||
return 0, core.ErrBlacklistedHash
|
||||
}
|
||||
|
||||
// check block body
|
||||
txnHash := types.DeriveSha(block.Transactions())
|
||||
uncleHash := types.CalcUncleHash(block.Uncles())
|
||||
if txnHash != block.Header().TxHash {
|
||||
return 0, errMismatchTxhashes
|
||||
}
|
||||
if uncleHash != nilUncleHash {
|
||||
return 0, errInvalidUncleHash
|
||||
}
|
||||
|
||||
// verify the header of proposed block
|
||||
err := sb.VerifyHeader(sb.chain, block.Header(), false)
|
||||
// ignore errEmptyCommittedSeals error because we don't have the committed seals yet
|
||||
if err == nil || err == errEmptyCommittedSeals {
|
||||
return 0, nil
|
||||
} else if err == consensus.ErrFutureBlock {
|
||||
return time.Unix(block.Header().Time.Int64(), 0).Sub(now()), consensus.ErrFutureBlock
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Sign implements istanbul.Backend.Sign
|
||||
func (sb *backend) Sign(data []byte) ([]byte, error) {
|
||||
hashData := crypto.Keccak256(data)
|
||||
return crypto.Sign(hashData, sb.privateKey)
|
||||
}
|
||||
|
||||
// CheckSignature implements istanbul.Backend.CheckSignature
|
||||
func (sb *backend) CheckSignature(data []byte, address common.Address, sig []byte) error {
|
||||
signer, err := istanbul.GetSignatureAddress(data, sig)
|
||||
if err != nil {
|
||||
log.Error("Failed to get signer address", "err", err)
|
||||
return err
|
||||
}
|
||||
// Compare derived addresses
|
||||
if signer != address {
|
||||
return errInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasPropsal implements istanbul.Backend.HashBlock
|
||||
func (sb *backend) HasPropsal(hash common.Hash, number *big.Int) bool {
|
||||
return sb.chain.GetHeader(hash, number.Uint64()) != nil
|
||||
}
|
||||
|
||||
// GetProposer implements istanbul.Backend.GetProposer
|
||||
func (sb *backend) GetProposer(number uint64) common.Address {
|
||||
if h := sb.chain.GetHeaderByNumber(number); h != nil {
|
||||
a, _ := sb.Author(h)
|
||||
return a
|
||||
}
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
// ParentValidators implements istanbul.Backend.GetParentValidators
|
||||
func (sb *backend) ParentValidators(proposal istanbul.Proposal) istanbul.ValidatorSet {
|
||||
if block, ok := proposal.(*types.Block); ok {
|
||||
return sb.getValidators(block.Number().Uint64()-1, block.ParentHash())
|
||||
}
|
||||
return validator.NewSet(nil, sb.config.ProposerPolicy)
|
||||
}
|
||||
|
||||
func (sb *backend) getValidators(number uint64, hash common.Hash) istanbul.ValidatorSet {
|
||||
snap, err := sb.snapshot(sb.chain, number, hash, nil)
|
||||
if err != nil {
|
||||
return validator.NewSet(nil, sb.config.ProposerPolicy)
|
||||
}
|
||||
return snap.ValSet
|
||||
}
|
||||
|
||||
func (sb *backend) LastProposal() (istanbul.Proposal, common.Address) {
|
||||
block := sb.currentBlock()
|
||||
|
||||
var proposer common.Address
|
||||
if block.Number().Cmp(common.Big0) > 0 {
|
||||
var err error
|
||||
proposer, err = sb.Author(block.Header())
|
||||
if err != nil {
|
||||
sb.logger.Error("Failed to get block proposer", "err", err)
|
||||
return nil, common.Address{}
|
||||
}
|
||||
}
|
||||
|
||||
// Return header only block here since we don't need block body
|
||||
return block, proposer
|
||||
}
|
||||
|
||||
func (sb *backend) HasBadProposal(hash common.Hash) bool {
|
||||
if sb.hasBadBlock == nil {
|
||||
return false
|
||||
}
|
||||
return sb.hasBadBlock(hash)
|
||||
}
|
||||
239
consensus/istanbul/backend/backend_test.go
Normal file
239
consensus/istanbul/backend/backend_test.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
b := newBackend()
|
||||
data := []byte("Here is a string....")
|
||||
sig, err := b.Sign(data)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
//Check signature recover
|
||||
hashData := crypto.Keccak256([]byte(data))
|
||||
pubkey, _ := crypto.Ecrecover(hashData, sig)
|
||||
var signer common.Address
|
||||
copy(signer[:], crypto.Keccak256(pubkey[1:])[12:])
|
||||
if signer != getAddress() {
|
||||
t.Errorf("address mismatch: have %v, want %s", signer.Hex(), getAddress().Hex())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSignature(t *testing.T) {
|
||||
key, _ := generatePrivateKey()
|
||||
data := []byte("Here is a string....")
|
||||
hashData := crypto.Keccak256([]byte(data))
|
||||
sig, _ := crypto.Sign(hashData, key)
|
||||
b := newBackend()
|
||||
a := getAddress()
|
||||
err := b.CheckSignature(data, a, sig)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
a = getInvalidAddress()
|
||||
err = b.CheckSignature(data, a, sig)
|
||||
if err != errInvalidSignature {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckValidatorSignature(t *testing.T) {
|
||||
vset, keys := newTestValidatorSet(5)
|
||||
|
||||
// 1. Positive test: sign with validator's key should succeed
|
||||
data := []byte("dummy data")
|
||||
hashData := crypto.Keccak256([]byte(data))
|
||||
for i, k := range keys {
|
||||
// Sign
|
||||
sig, err := crypto.Sign(hashData, k)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
// CheckValidatorSignature should succeed
|
||||
addr, err := istanbul.CheckValidatorSignature(vset, data, sig)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
validator := vset.GetByIndex(uint64(i))
|
||||
if addr != validator.Address() {
|
||||
t.Errorf("validator address mismatch: have %v, want %v", addr, validator.Address())
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Negative test: sign with any key other than validator's key should return error
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
// Sign
|
||||
sig, err := crypto.Sign(hashData, key)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
|
||||
// CheckValidatorSignature should return ErrUnauthorizedAddress
|
||||
addr, err := istanbul.CheckValidatorSignature(vset, data, sig)
|
||||
if err != istanbul.ErrUnauthorizedAddress {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, istanbul.ErrUnauthorizedAddress)
|
||||
}
|
||||
emptyAddr := common.Address{}
|
||||
if addr != emptyAddr {
|
||||
t.Errorf("address mismatch: have %v, want %v", addr, emptyAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommit(t *testing.T) {
|
||||
backend := newBackend()
|
||||
|
||||
commitCh := make(chan *types.Block)
|
||||
// Case: it's a proposer, so the backend.commit will receive channel result from backend.Commit function
|
||||
testCases := []struct {
|
||||
expectedErr error
|
||||
expectedSignature [][]byte
|
||||
expectedBlock func() *types.Block
|
||||
}{
|
||||
{
|
||||
// normal case
|
||||
nil,
|
||||
[][]byte{append([]byte{1}, bytes.Repeat([]byte{0x00}, types.IstanbulExtraSeal-1)...)},
|
||||
func() *types.Block {
|
||||
chain, engine := newBlockChain(1)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
expectedBlock, _ := engine.updateBlock(engine.chain.GetHeader(block.ParentHash(), block.NumberU64()-1), block)
|
||||
return expectedBlock
|
||||
},
|
||||
},
|
||||
{
|
||||
// invalid signature
|
||||
errInvalidCommittedSeals,
|
||||
nil,
|
||||
func() *types.Block {
|
||||
chain, engine := newBlockChain(1)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
expectedBlock, _ := engine.updateBlock(engine.chain.GetHeader(block.ParentHash(), block.NumberU64()-1), block)
|
||||
return expectedBlock
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
expBlock := test.expectedBlock()
|
||||
go func() {
|
||||
result := <-backend.commitCh
|
||||
commitCh <- result
|
||||
}()
|
||||
|
||||
backend.proposedBlockHash = expBlock.Hash()
|
||||
if err := backend.Commit(expBlock, test.expectedSignature); err != nil {
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, test.expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
if test.expectedErr == nil {
|
||||
// to avoid race condition is occurred by goroutine
|
||||
select {
|
||||
case result := <-commitCh:
|
||||
if result.Hash() != expBlock.Hash() {
|
||||
t.Errorf("hash mismatch: have %v, want %v", result.Hash(), expBlock.Hash())
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProposer(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
block := makeBlock(chain, engine, chain.Genesis())
|
||||
chain.InsertChain(types.Blocks{block})
|
||||
expected := engine.GetProposer(1)
|
||||
actual := engine.Address()
|
||||
if actual != expected {
|
||||
t.Errorf("proposer mismatch: have %v, want %v", actual.Hex(), expected.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SimpleBackend
|
||||
* Private key: bb047e5940b6d83354d9432db7c449ac8fca2248008aaa7271369880f9f11cc1
|
||||
* Public key: 04a2bfb0f7da9e1b9c0c64e14f87e8fb82eb0144e97c25fe3a977a921041a50976984d18257d2495e7bfd3d4b280220217f429287d25ecdf2b0d7c0f7aae9aa624
|
||||
* Address: 0x70524d664ffe731100208a0154e556f9bb679ae6
|
||||
*/
|
||||
func getAddress() common.Address {
|
||||
return common.HexToAddress("0x70524d664ffe731100208a0154e556f9bb679ae6")
|
||||
}
|
||||
|
||||
func getInvalidAddress() common.Address {
|
||||
return common.HexToAddress("0x9535b2e7faaba5288511d89341d94a38063a349b")
|
||||
}
|
||||
|
||||
func generatePrivateKey() (*ecdsa.PrivateKey, error) {
|
||||
key := "bb047e5940b6d83354d9432db7c449ac8fca2248008aaa7271369880f9f11cc1"
|
||||
return crypto.HexToECDSA(key)
|
||||
}
|
||||
|
||||
func newTestValidatorSet(n int) (istanbul.ValidatorSet, []*ecdsa.PrivateKey) {
|
||||
// generate validators
|
||||
keys := make(Keys, n)
|
||||
addrs := make([]common.Address, n)
|
||||
for i := 0; i < n; i++ {
|
||||
privateKey, _ := crypto.GenerateKey()
|
||||
keys[i] = privateKey
|
||||
addrs[i] = crypto.PubkeyToAddress(privateKey.PublicKey)
|
||||
}
|
||||
vset := validator.NewSet(addrs, istanbul.RoundRobin)
|
||||
sort.Sort(keys) //Keys need to be sorted by its public key address
|
||||
return vset, keys
|
||||
}
|
||||
|
||||
type Keys []*ecdsa.PrivateKey
|
||||
|
||||
func (slice Keys) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
func (slice Keys) Less(i, j int) bool {
|
||||
return strings.Compare(crypto.PubkeyToAddress(slice[i].PublicKey).String(), crypto.PubkeyToAddress(slice[j].PublicKey).String()) < 0
|
||||
}
|
||||
|
||||
func (slice Keys) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
|
||||
func newBackend() (b *backend) {
|
||||
_, b = newBlockChain(4)
|
||||
key, _ := generatePrivateKey()
|
||||
b.privateKey = key
|
||||
return
|
||||
}
|
||||
717
consensus/istanbul/backend/engine.go
Normal file
717
consensus/istanbul/backend/engine.go
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
istanbulCore "github.com/ethereum/go-ethereum/consensus/istanbul/core"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto/sha3"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
const (
|
||||
checkpointInterval = 1024 // Number of blocks after which to save the vote snapshot to the database
|
||||
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
|
||||
inmemoryPeers = 40
|
||||
inmemoryMessages = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
// errInvalidProposal is returned when a prposal is malformed.
|
||||
errInvalidProposal = errors.New("invalid proposal")
|
||||
// errInvalidSignature is returned when given signature is not signed by given
|
||||
// address.
|
||||
errInvalidSignature = errors.New("invalid signature")
|
||||
// errUnknownBlock is returned when the list of validators is requested for a block
|
||||
// that is not part of the local blockchain.
|
||||
errUnknownBlock = errors.New("unknown block")
|
||||
// errUnauthorized is returned if a header is signed by a non authorized entity.
|
||||
errUnauthorized = errors.New("unauthorized")
|
||||
// errInvalidDifficulty is returned if the difficulty of a block is not 1
|
||||
errInvalidDifficulty = errors.New("invalid difficulty")
|
||||
// errInvalidExtraDataFormat is returned when the extra data format is incorrect
|
||||
errInvalidExtraDataFormat = errors.New("invalid extra data format")
|
||||
// errInvalidMixDigest is returned if a block's mix digest is not Istanbul digest.
|
||||
errInvalidMixDigest = errors.New("invalid Istanbul mix digest")
|
||||
// errInvalidNonce is returned if a block's nonce is invalid
|
||||
errInvalidNonce = errors.New("invalid nonce")
|
||||
// errInvalidUncleHash is returned if a block contains an non-empty uncle list.
|
||||
errInvalidUncleHash = errors.New("non empty uncle hash")
|
||||
// errInconsistentValidatorSet is returned if the validator set is inconsistent
|
||||
errInconsistentValidatorSet = errors.New("non empty uncle hash")
|
||||
// errInvalidTimestamp is returned if the timestamp of a block is lower than the previous block's timestamp + the minimum block period.
|
||||
errInvalidTimestamp = errors.New("invalid timestamp")
|
||||
// errInvalidVotingChain is returned if an authorization list is attempted to
|
||||
// be modified via out-of-range or non-contiguous headers.
|
||||
errInvalidVotingChain = errors.New("invalid voting chain")
|
||||
// errInvalidVote is returned if a nonce value is something else that the two
|
||||
// allowed constants of 0x00..0 or 0xff..f.
|
||||
errInvalidVote = errors.New("vote nonce not 0x00..0 or 0xff..f")
|
||||
// errInvalidCommittedSeals is returned if the committed seal is not signed by any of parent validators.
|
||||
errInvalidCommittedSeals = errors.New("invalid committed seals")
|
||||
// errEmptyCommittedSeals is returned if the field of committed seals is zero.
|
||||
errEmptyCommittedSeals = errors.New("zero committed seals")
|
||||
// errMismatchTxhashes is returned if the TxHash in header is mismatch.
|
||||
errMismatchTxhashes = errors.New("mismatch transactions hashes")
|
||||
)
|
||||
var (
|
||||
defaultDifficulty = big.NewInt(1)
|
||||
nilUncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW.
|
||||
emptyNonce = types.BlockNonce{}
|
||||
now = time.Now
|
||||
|
||||
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") // Magic nonce number to vote on adding a new validator
|
||||
nonceDropVote = hexutil.MustDecode("0x0000000000000000") // Magic nonce number to vote on removing a validator.
|
||||
|
||||
inmemoryAddresses = 20 // Number of recent addresses from ecrecover
|
||||
recentAddresses, _ = lru.NewARC(inmemoryAddresses)
|
||||
)
|
||||
|
||||
// Author retrieves the Ethereum address of the account that minted the given
|
||||
// block, which may be different from the header's coinbase if a consensus
|
||||
// engine is based on signatures.
|
||||
func (sb *backend) Author(header *types.Header) (common.Address, error) {
|
||||
return ecrecover(header)
|
||||
}
|
||||
|
||||
// VerifyHeader checks whether a header conforms to the consensus rules of a
|
||||
// given engine. Verifying the seal may be done optionally here, or explicitly
|
||||
// via the VerifySeal method.
|
||||
func (sb *backend) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error {
|
||||
return sb.verifyHeader(chain, header, nil)
|
||||
}
|
||||
|
||||
// verifyHeader checks whether a header conforms to the consensus rules.The
|
||||
// caller may optionally pass in a batch of parents (ascending order) to avoid
|
||||
// looking those up from the database. This is useful for concurrently verifying
|
||||
// a batch of new headers.
|
||||
func (sb *backend) verifyHeader(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
|
||||
if header.Number == nil {
|
||||
return errUnknownBlock
|
||||
}
|
||||
|
||||
// Don't waste time checking blocks from the future
|
||||
if header.Time.Cmp(big.NewInt(now().Unix())) > 0 {
|
||||
return consensus.ErrFutureBlock
|
||||
}
|
||||
|
||||
// Ensure that the extra data format is satisfied
|
||||
if _, err := types.ExtractIstanbulExtra(header); err != nil {
|
||||
return errInvalidExtraDataFormat
|
||||
}
|
||||
|
||||
// Ensure that the coinbase is valid
|
||||
if header.Nonce != (emptyNonce) && !bytes.Equal(header.Nonce[:], nonceAuthVote) && !bytes.Equal(header.Nonce[:], nonceDropVote) {
|
||||
return errInvalidNonce
|
||||
}
|
||||
// Ensure that the mix digest is zero as we don't have fork protection currently
|
||||
if header.MixDigest != types.IstanbulDigest {
|
||||
return errInvalidMixDigest
|
||||
}
|
||||
// Ensure that the block doesn't contain any uncles which are meaningless in Istanbul
|
||||
if header.UncleHash != nilUncleHash {
|
||||
return errInvalidUncleHash
|
||||
}
|
||||
// Ensure that the block's difficulty is meaningful (may not be correct at this point)
|
||||
if header.Difficulty == nil || header.Difficulty.Cmp(defaultDifficulty) != 0 {
|
||||
return errInvalidDifficulty
|
||||
}
|
||||
|
||||
return sb.verifyCascadingFields(chain, header, parents)
|
||||
}
|
||||
|
||||
// verifyCascadingFields verifies all the header fields that are not standalone,
|
||||
// rather depend on a batch of previous headers. The caller may optionally pass
|
||||
// in a batch of parents (ascending order) to avoid looking those up from the
|
||||
// database. This is useful for concurrently verifying a batch of new headers.
|
||||
func (sb *backend) verifyCascadingFields(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
|
||||
// The genesis block is the always valid dead-end
|
||||
number := header.Number.Uint64()
|
||||
if number == 0 {
|
||||
return nil
|
||||
}
|
||||
// Ensure that the block's timestamp isn't too close to it's parent
|
||||
var parent *types.Header
|
||||
if len(parents) > 0 {
|
||||
parent = parents[len(parents)-1]
|
||||
} else {
|
||||
parent = chain.GetHeader(header.ParentHash, number-1)
|
||||
}
|
||||
if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash {
|
||||
return consensus.ErrUnknownAncestor
|
||||
}
|
||||
if parent.Time.Uint64()+sb.config.BlockPeriod > header.Time.Uint64() {
|
||||
return errInvalidTimestamp
|
||||
}
|
||||
// Verify validators in extraData. Validators in snapshot and extraData should be the same.
|
||||
snap, err := sb.snapshot(chain, number-1, header.ParentHash, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validators := make([]byte, len(snap.validators())*common.AddressLength)
|
||||
for i, validator := range snap.validators() {
|
||||
copy(validators[i*common.AddressLength:], validator[:])
|
||||
}
|
||||
if err := sb.verifySigner(chain, header, parents); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sb.verifyCommittedSeals(chain, header, parents)
|
||||
}
|
||||
|
||||
// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers
|
||||
// concurrently. The method returns a quit channel to abort the operations and
|
||||
// a results channel to retrieve the async verifications (the order is that of
|
||||
// the input slice).
|
||||
func (sb *backend) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) {
|
||||
abort := make(chan struct{})
|
||||
results := make(chan error, len(headers))
|
||||
go func() {
|
||||
for i, header := range headers {
|
||||
err := sb.verifyHeader(chain, header, headers[:i])
|
||||
|
||||
select {
|
||||
case <-abort:
|
||||
return
|
||||
case results <- err:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return abort, results
|
||||
}
|
||||
|
||||
// VerifyUncles verifies that the given block's uncles conform to the consensus
|
||||
// rules of a given engine.
|
||||
func (sb *backend) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
|
||||
if len(block.Uncles()) > 0 {
|
||||
return errInvalidUncleHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySigner checks whether the signer is in parent's validator set
|
||||
func (sb *backend) verifySigner(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
|
||||
// Verifying the genesis block is not supported
|
||||
number := header.Number.Uint64()
|
||||
if number == 0 {
|
||||
return errUnknownBlock
|
||||
}
|
||||
|
||||
// Retrieve the snapshot needed to verify this header and cache it
|
||||
snap, err := sb.snapshot(chain, number-1, header.ParentHash, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// resolve the authorization key and check against signers
|
||||
signer, err := ecrecover(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Signer should be in the validator set of previous block's extraData.
|
||||
if _, v := snap.ValSet.GetByAddress(signer); v == nil {
|
||||
return errUnauthorized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyCommittedSeals checks whether every committed seal is signed by one of the parent's validators
|
||||
func (sb *backend) verifyCommittedSeals(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
|
||||
number := header.Number.Uint64()
|
||||
// We don't need to verify committed seals in the genesis block
|
||||
if number == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieve the snapshot needed to verify this header and cache it
|
||||
snap, err := sb.snapshot(chain, number-1, header.ParentHash, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extra, err := types.ExtractIstanbulExtra(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The length of Committed seals should be larger than 0
|
||||
if len(extra.CommittedSeal) == 0 {
|
||||
return errEmptyCommittedSeals
|
||||
}
|
||||
|
||||
validators := snap.ValSet.Copy()
|
||||
// Check whether the committed seals are generated by parent's validators
|
||||
validSeal := 0
|
||||
proposalSeal := istanbulCore.PrepareCommittedSeal(header.Hash())
|
||||
// 1. Get committed seals from current header
|
||||
for _, seal := range extra.CommittedSeal {
|
||||
// 2. Get the original address by seal and parent block hash
|
||||
addr, err := istanbul.GetSignatureAddress(proposalSeal, seal)
|
||||
if err != nil {
|
||||
sb.logger.Error("not a valid address", "err", err)
|
||||
return errInvalidSignature
|
||||
}
|
||||
// Every validator can have only one seal. If more than one seals are signed by a
|
||||
// validator, the validator cannot be found and errInvalidCommittedSeals is returned.
|
||||
if validators.RemoveValidator(addr) {
|
||||
validSeal += 1
|
||||
} else {
|
||||
return errInvalidCommittedSeals
|
||||
}
|
||||
}
|
||||
|
||||
// The length of validSeal should be larger than number of faulty node + 1
|
||||
if validSeal <= 2*snap.ValSet.F() {
|
||||
return errInvalidCommittedSeals
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySeal checks whether the crypto seal on a header is valid according to
|
||||
// the consensus rules of the given engine.
|
||||
func (sb *backend) VerifySeal(chain consensus.ChainReader, header *types.Header) error {
|
||||
// get parent header and ensure the signer is in parent's validator set
|
||||
number := header.Number.Uint64()
|
||||
if number == 0 {
|
||||
return errUnknownBlock
|
||||
}
|
||||
|
||||
// ensure that the difficulty equals to defaultDifficulty
|
||||
if header.Difficulty.Cmp(defaultDifficulty) != 0 {
|
||||
return errInvalidDifficulty
|
||||
}
|
||||
return sb.verifySigner(chain, header, nil)
|
||||
}
|
||||
|
||||
// Prepare initializes the consensus fields of a block header according to the
|
||||
// rules of a particular engine. The changes are executed inline.
|
||||
func (sb *backend) Prepare(chain consensus.ChainReader, header *types.Header) error {
|
||||
// unused fields, force to set to empty
|
||||
header.Coinbase = common.Address{}
|
||||
header.Nonce = emptyNonce
|
||||
header.MixDigest = types.IstanbulDigest
|
||||
|
||||
// copy the parent extra data as the header extra data
|
||||
number := header.Number.Uint64()
|
||||
parent := chain.GetHeader(header.ParentHash, number-1)
|
||||
if parent == nil {
|
||||
return consensus.ErrUnknownAncestor
|
||||
}
|
||||
// use the same difficulty for all blocks
|
||||
header.Difficulty = defaultDifficulty
|
||||
|
||||
// Assemble the voting snapshot
|
||||
snap, err := sb.snapshot(chain, number-1, header.ParentHash, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get valid candidate list
|
||||
sb.candidatesLock.RLock()
|
||||
var addresses []common.Address
|
||||
var authorizes []bool
|
||||
for address, authorize := range sb.candidates {
|
||||
if snap.checkVote(address, authorize) {
|
||||
addresses = append(addresses, address)
|
||||
authorizes = append(authorizes, authorize)
|
||||
}
|
||||
}
|
||||
sb.candidatesLock.RUnlock()
|
||||
|
||||
// pick one of the candidates randomly
|
||||
if len(addresses) > 0 {
|
||||
index := rand.Intn(len(addresses))
|
||||
// add validator voting in coinbase
|
||||
header.Coinbase = addresses[index]
|
||||
if authorizes[index] {
|
||||
copy(header.Nonce[:], nonceAuthVote)
|
||||
} else {
|
||||
copy(header.Nonce[:], nonceDropVote)
|
||||
}
|
||||
}
|
||||
|
||||
// add validators in snapshot to extraData's validators section
|
||||
extra, err := prepareExtra(header, snap.validators())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Extra = extra
|
||||
|
||||
// set header's timestamp
|
||||
header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(sb.config.BlockPeriod))
|
||||
if header.Time.Int64() < time.Now().Unix() {
|
||||
header.Time = big.NewInt(time.Now().Unix())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finalize runs any post-transaction state modifications (e.g. block rewards)
|
||||
// and assembles the final block.
|
||||
//
|
||||
// Note, the block header and state database might be updated to reflect any
|
||||
// consensus rules that happen at finalization (e.g. block rewards).
|
||||
func (sb *backend) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
|
||||
uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) {
|
||||
// No block rewards in Istanbul, so the state remains as is and uncles are dropped
|
||||
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
|
||||
header.UncleHash = nilUncleHash
|
||||
|
||||
// Assemble and return the final block for sealing
|
||||
return types.NewBlock(header, txs, nil, receipts), nil
|
||||
}
|
||||
|
||||
// Seal generates a new block for the given input block with the local miner's
|
||||
// seal place on top.
|
||||
func (sb *backend) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
|
||||
// update the block header timestamp and signature and propose the block to core engine
|
||||
header := block.Header()
|
||||
number := header.Number.Uint64()
|
||||
|
||||
// Bail out if we're unauthorized to sign a block
|
||||
snap, err := sb.snapshot(chain, number-1, header.ParentHash, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, v := snap.ValSet.GetByAddress(sb.address); v == nil {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
|
||||
parent := chain.GetHeader(header.ParentHash, number-1)
|
||||
if parent == nil {
|
||||
return nil, consensus.ErrUnknownAncestor
|
||||
}
|
||||
block, err = sb.updateBlock(parent, block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// wait for the timestamp of header, use this to adjust the block period
|
||||
delay := time.Unix(block.Header().Time.Int64(), 0).Sub(now())
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
case <-stop:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the proposed block hash and clear it if the seal() is completed.
|
||||
sb.sealMu.Lock()
|
||||
sb.proposedBlockHash = block.Hash()
|
||||
clear := func() {
|
||||
sb.proposedBlockHash = common.Hash{}
|
||||
sb.sealMu.Unlock()
|
||||
}
|
||||
defer clear()
|
||||
|
||||
// post block into Istanbul engine
|
||||
go sb.EventMux().Post(istanbul.RequestEvent{
|
||||
Proposal: block,
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case result := <-sb.commitCh:
|
||||
// if the block hash and the hash from channel are the same,
|
||||
// return the result. Otherwise, keep waiting the next hash.
|
||||
if block.Hash() == result.Hash() {
|
||||
return result, nil
|
||||
}
|
||||
case <-stop:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CalcDifficulty is the difficulty adjustment algorithm. It returns the difficulty
|
||||
// that a new block should have based on the previous blocks in the chain and the
|
||||
// current signer.
|
||||
func (sb *backend) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
|
||||
return defaultDifficulty
|
||||
}
|
||||
|
||||
// update timestamp and signature of the block based on its number of transactions
|
||||
func (sb *backend) updateBlock(parent *types.Header, block *types.Block) (*types.Block, error) {
|
||||
header := block.Header()
|
||||
// sign the hash
|
||||
seal, err := sb.Sign(sigHash(header).Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writeSeal(header, seal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return block.WithSeal(header), nil
|
||||
}
|
||||
|
||||
// APIs returns the RPC APIs this consensus engine provides.
|
||||
func (sb *backend) APIs(chain consensus.ChainReader) []rpc.API {
|
||||
return []rpc.API{{
|
||||
Namespace: "istanbul",
|
||||
Version: "1.0",
|
||||
Service: &API{chain: chain, istanbul: sb},
|
||||
Public: true,
|
||||
}}
|
||||
}
|
||||
|
||||
// Start implements consensus.Istanbul.Start
|
||||
func (sb *backend) Start(chain consensus.ChainReader, currentBlock func() *types.Block, hasBadBlock func(hash common.Hash) bool) error {
|
||||
sb.coreMu.Lock()
|
||||
defer sb.coreMu.Unlock()
|
||||
if sb.coreStarted {
|
||||
return istanbul.ErrStartedEngine
|
||||
}
|
||||
|
||||
// clear previous data
|
||||
sb.proposedBlockHash = common.Hash{}
|
||||
if sb.commitCh != nil {
|
||||
close(sb.commitCh)
|
||||
}
|
||||
sb.commitCh = make(chan *types.Block, 1)
|
||||
|
||||
sb.chain = chain
|
||||
sb.currentBlock = currentBlock
|
||||
sb.hasBadBlock = hasBadBlock
|
||||
|
||||
if err := sb.core.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sb.coreStarted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop implements consensus.Istanbul.Stop
|
||||
func (sb *backend) Stop() error {
|
||||
sb.coreMu.Lock()
|
||||
defer sb.coreMu.Unlock()
|
||||
if !sb.coreStarted {
|
||||
return istanbul.ErrStoppedEngine
|
||||
}
|
||||
if err := sb.core.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
sb.coreStarted = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// snapshot retrieves the authorization snapshot at a given point in time.
|
||||
func (sb *backend) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error) {
|
||||
// Search for a snapshot in memory or on disk for checkpoints
|
||||
var (
|
||||
headers []*types.Header
|
||||
snap *Snapshot
|
||||
)
|
||||
for snap == nil {
|
||||
// If an in-memory snapshot was found, use that
|
||||
if s, ok := sb.recents.Get(hash); ok {
|
||||
snap = s.(*Snapshot)
|
||||
break
|
||||
}
|
||||
// If an on-disk checkpoint snapshot can be found, use that
|
||||
if number%checkpointInterval == 0 {
|
||||
if s, err := loadSnapshot(sb.config.Epoch, sb.db, hash); err == nil {
|
||||
log.Trace("Loaded voting snapshot form disk", "number", number, "hash", hash)
|
||||
snap = s
|
||||
break
|
||||
}
|
||||
}
|
||||
// If we're at block zero, make a snapshot
|
||||
if number == 0 {
|
||||
genesis := chain.GetHeaderByNumber(0)
|
||||
if err := sb.VerifyHeader(chain, genesis, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
istanbulExtra, err := types.ExtractIstanbulExtra(genesis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snap = newSnapshot(sb.config.Epoch, 0, genesis.Hash(), validator.NewSet(istanbulExtra.Validators, sb.config.ProposerPolicy))
|
||||
if err := snap.store(sb.db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Trace("Stored genesis voting snapshot to disk")
|
||||
break
|
||||
}
|
||||
// No snapshot for this header, gather the header and move backward
|
||||
var header *types.Header
|
||||
if len(parents) > 0 {
|
||||
// If we have explicit parents, pick from there (enforced)
|
||||
header = parents[len(parents)-1]
|
||||
if header.Hash() != hash || header.Number.Uint64() != number {
|
||||
return nil, consensus.ErrUnknownAncestor
|
||||
}
|
||||
parents = parents[:len(parents)-1]
|
||||
} else {
|
||||
// No explicit parents (or no more left), reach out to the database
|
||||
header = chain.GetHeader(hash, number)
|
||||
if header == nil {
|
||||
return nil, consensus.ErrUnknownAncestor
|
||||
}
|
||||
}
|
||||
headers = append(headers, header)
|
||||
number, hash = number-1, header.ParentHash
|
||||
}
|
||||
// Previous snapshot found, apply any pending headers on top of it
|
||||
for i := 0; i < len(headers)/2; i++ {
|
||||
headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i]
|
||||
}
|
||||
snap, err := snap.apply(headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.recents.Add(snap.Hash, snap)
|
||||
|
||||
// If we've generated a new checkpoint snapshot, save to disk
|
||||
if snap.Number%checkpointInterval == 0 && len(headers) > 0 {
|
||||
if err = snap.store(sb.db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)
|
||||
}
|
||||
return snap, err
|
||||
}
|
||||
|
||||
// FIXME: Need to update this for Istanbul
|
||||
// sigHash returns the hash which is used as input for the Istanbul
|
||||
// signing. It is the hash of the entire header apart from the 65 byte signature
|
||||
// contained at the end of the extra data.
|
||||
//
|
||||
// Note, the method requires the extra data to be at least 65 bytes, otherwise it
|
||||
// panics. This is done to avoid accidentally using both forms (signature present
|
||||
// or not), which could be abused to produce different hashes for the same header.
|
||||
func sigHash(header *types.Header) (hash common.Hash) {
|
||||
hasher := sha3.NewKeccak256()
|
||||
|
||||
// Clean seal is required for calculating proposer seal.
|
||||
rlp.Encode(hasher, types.IstanbulFilteredHeader(header, false))
|
||||
hasher.Sum(hash[:0])
|
||||
return hash
|
||||
}
|
||||
|
||||
// ecrecover extracts the Ethereum account address from a signed header.
|
||||
func ecrecover(header *types.Header) (common.Address, error) {
|
||||
hash := header.Hash()
|
||||
if addr, ok := recentAddresses.Get(hash); ok {
|
||||
return addr.(common.Address), nil
|
||||
}
|
||||
|
||||
// Retrieve the signature from the header extra-data
|
||||
istanbulExtra, err := types.ExtractIstanbulExtra(header)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
addr, err := istanbul.GetSignatureAddress(sigHash(header).Bytes(), istanbulExtra.Seal)
|
||||
if err != nil {
|
||||
return addr, err
|
||||
}
|
||||
recentAddresses.Add(hash, addr)
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// prepareExtra returns a extra-data of the given header and validators
|
||||
func prepareExtra(header *types.Header, vals []common.Address) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// compensate the lack bytes if header.Extra is not enough IstanbulExtraVanity bytes.
|
||||
if len(header.Extra) < types.IstanbulExtraVanity {
|
||||
header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, types.IstanbulExtraVanity-len(header.Extra))...)
|
||||
}
|
||||
buf.Write(header.Extra[:types.IstanbulExtraVanity])
|
||||
|
||||
ist := &types.IstanbulExtra{
|
||||
Validators: vals,
|
||||
Seal: []byte{},
|
||||
CommittedSeal: [][]byte{},
|
||||
}
|
||||
|
||||
payload, err := rlp.EncodeToBytes(&ist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(buf.Bytes(), payload...), nil
|
||||
}
|
||||
|
||||
// writeSeal writes the extra-data field of the given header with the given seals.
|
||||
// suggest to rename to writeSeal.
|
||||
func writeSeal(h *types.Header, seal []byte) error {
|
||||
if len(seal)%types.IstanbulExtraSeal != 0 {
|
||||
return errInvalidSignature
|
||||
}
|
||||
|
||||
istanbulExtra, err := types.ExtractIstanbulExtra(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
istanbulExtra.Seal = seal
|
||||
payload, err := rlp.EncodeToBytes(&istanbulExtra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Extra = append(h.Extra[:types.IstanbulExtraVanity], payload...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeCommittedSeals writes the extra-data field of a block header with given committed seals.
|
||||
func writeCommittedSeals(h *types.Header, committedSeals [][]byte) error {
|
||||
if len(committedSeals) == 0 {
|
||||
return errInvalidCommittedSeals
|
||||
}
|
||||
|
||||
for _, seal := range committedSeals {
|
||||
if len(seal) != types.IstanbulExtraSeal {
|
||||
return errInvalidCommittedSeals
|
||||
}
|
||||
}
|
||||
|
||||
istanbulExtra, err := types.ExtractIstanbulExtra(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
istanbulExtra.CommittedSeal = make([][]byte, len(committedSeals))
|
||||
copy(istanbulExtra.CommittedSeal, committedSeals)
|
||||
|
||||
payload, err := rlp.EncodeToBytes(&istanbulExtra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Extra = append(h.Extra[:types.IstanbulExtraVanity], payload...)
|
||||
return nil
|
||||
}
|
||||
549
consensus/istanbul/backend/engine_test.go
Normal file
549
consensus/istanbul/backend/engine_test.go
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// in this test, we can set n to 1, and it means we can process Istanbul and commit a
|
||||
// block by one node. Otherwise, if n is larger than 1, we have to generate
|
||||
// other fake events to process Istanbul.
|
||||
func newBlockChain(n int) (*core.BlockChain, *backend) {
|
||||
genesis, nodeKeys := getGenesisAndKeys(n)
|
||||
memDB, _ := ethdb.NewMemDatabase()
|
||||
config := istanbul.DefaultConfig
|
||||
// Use the first key as private key
|
||||
b, _ := New(config, nodeKeys[0], memDB).(*backend)
|
||||
genesis.MustCommit(memDB)
|
||||
blockchain, err := core.NewBlockChain(memDB, nil, genesis.Config, b, vm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b.Start(blockchain, blockchain.CurrentBlock, blockchain.HasBadBlock)
|
||||
snap, err := b.snapshot(blockchain, 0, common.Hash{}, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if snap == nil {
|
||||
panic("failed to get snapshot")
|
||||
}
|
||||
proposerAddr := snap.ValSet.GetProposer().Address()
|
||||
|
||||
// find proposer key
|
||||
for _, key := range nodeKeys {
|
||||
addr := crypto.PubkeyToAddress(key.PublicKey)
|
||||
if addr.String() == proposerAddr.String() {
|
||||
b.privateKey = key
|
||||
b.address = addr
|
||||
}
|
||||
}
|
||||
|
||||
return blockchain, b
|
||||
}
|
||||
|
||||
func getGenesisAndKeys(n int) (*core.Genesis, []*ecdsa.PrivateKey) {
|
||||
// Setup validators
|
||||
var nodeKeys = make([]*ecdsa.PrivateKey, n)
|
||||
var addrs = make([]common.Address, n)
|
||||
for i := 0; i < n; i++ {
|
||||
nodeKeys[i], _ = crypto.GenerateKey()
|
||||
addrs[i] = crypto.PubkeyToAddress(nodeKeys[i].PublicKey)
|
||||
}
|
||||
|
||||
// generate genesis block
|
||||
genesis := core.DefaultGenesisBlock()
|
||||
genesis.Config = params.TestChainConfig
|
||||
// force enable Istanbul engine
|
||||
genesis.Config.Istanbul = ¶ms.IstanbulConfig{}
|
||||
genesis.Config.Ethash = nil
|
||||
genesis.Difficulty = defaultDifficulty
|
||||
genesis.Nonce = emptyNonce.Uint64()
|
||||
genesis.Mixhash = types.IstanbulDigest
|
||||
|
||||
appendValidators(genesis, addrs)
|
||||
return genesis, nodeKeys
|
||||
}
|
||||
|
||||
func appendValidators(genesis *core.Genesis, addrs []common.Address) {
|
||||
|
||||
if len(genesis.ExtraData) < types.IstanbulExtraVanity {
|
||||
genesis.ExtraData = append(genesis.ExtraData, bytes.Repeat([]byte{0x00}, types.IstanbulExtraVanity)...)
|
||||
}
|
||||
genesis.ExtraData = genesis.ExtraData[:types.IstanbulExtraVanity]
|
||||
|
||||
ist := &types.IstanbulExtra{
|
||||
Validators: addrs,
|
||||
Seal: []byte{},
|
||||
CommittedSeal: [][]byte{},
|
||||
}
|
||||
|
||||
istPayload, err := rlp.EncodeToBytes(&ist)
|
||||
if err != nil {
|
||||
panic("failed to encode istanbul extra")
|
||||
}
|
||||
genesis.ExtraData = append(genesis.ExtraData, istPayload...)
|
||||
}
|
||||
|
||||
func makeHeader(parent *types.Block, config *istanbul.Config) *types.Header {
|
||||
header := &types.Header{
|
||||
ParentHash: parent.Hash(),
|
||||
Number: parent.Number().Add(parent.Number(), common.Big1),
|
||||
GasLimit: core.CalcGasLimit(parent),
|
||||
GasUsed: 0,
|
||||
Extra: parent.Extra(),
|
||||
Time: new(big.Int).Add(parent.Time(), new(big.Int).SetUint64(config.BlockPeriod)),
|
||||
Difficulty: defaultDifficulty,
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
func makeBlock(chain *core.BlockChain, engine *backend, parent *types.Block) *types.Block {
|
||||
block := makeBlockWithoutSeal(chain, engine, parent)
|
||||
block, _ = engine.Seal(chain, block, nil)
|
||||
return block
|
||||
}
|
||||
|
||||
func makeBlockWithoutSeal(chain *core.BlockChain, engine *backend, parent *types.Block) *types.Block {
|
||||
header := makeHeader(parent, engine.config)
|
||||
engine.Prepare(chain, header)
|
||||
state, _ := chain.StateAt(parent.Root())
|
||||
block, _ := engine.Finalize(chain, header, state, nil, nil, nil)
|
||||
return block
|
||||
}
|
||||
|
||||
func TestPrepare(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
header := makeHeader(chain.Genesis(), engine.config)
|
||||
err := engine.Prepare(chain, header)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
header.ParentHash = common.StringToHash("1234567890")
|
||||
err = engine.Prepare(chain, header)
|
||||
if err != consensus.ErrUnknownAncestor {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, consensus.ErrUnknownAncestor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSealStopChannel(t *testing.T) {
|
||||
chain, engine := newBlockChain(4)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
stop := make(chan struct{}, 1)
|
||||
eventSub := engine.EventMux().Subscribe(istanbul.RequestEvent{})
|
||||
eventLoop := func() {
|
||||
ev := <-eventSub.Chan()
|
||||
_, ok := ev.Data.(istanbul.RequestEvent)
|
||||
if !ok {
|
||||
t.Errorf("unexpected event comes: %v", reflect.TypeOf(ev.Data))
|
||||
}
|
||||
stop <- struct{}{}
|
||||
eventSub.Unsubscribe()
|
||||
}
|
||||
go eventLoop()
|
||||
finalBlock, err := engine.Seal(chain, block, stop)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
if finalBlock != nil {
|
||||
t.Errorf("block mismatch: have %v, want nil", finalBlock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSealCommittedOtherHash(t *testing.T) {
|
||||
chain, engine := newBlockChain(4)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
otherBlock := makeBlockWithoutSeal(chain, engine, block)
|
||||
eventSub := engine.EventMux().Subscribe(istanbul.RequestEvent{})
|
||||
eventLoop := func() {
|
||||
ev := <-eventSub.Chan()
|
||||
_, ok := ev.Data.(istanbul.RequestEvent)
|
||||
if !ok {
|
||||
t.Errorf("unexpected event comes: %v", reflect.TypeOf(ev.Data))
|
||||
}
|
||||
engine.Commit(otherBlock, [][]byte{})
|
||||
eventSub.Unsubscribe()
|
||||
}
|
||||
go eventLoop()
|
||||
seal := func() {
|
||||
engine.Seal(chain, block, nil)
|
||||
t.Error("seal should not be completed")
|
||||
}
|
||||
go seal()
|
||||
|
||||
const timeoutDura = 2 * time.Second
|
||||
timeout := time.NewTimer(timeoutDura)
|
||||
<-timeout.C
|
||||
// wait 2 seconds to ensure we cannot get any blocks from Istanbul
|
||||
}
|
||||
|
||||
func TestSealCommitted(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
expectedBlock, _ := engine.updateBlock(engine.chain.GetHeader(block.ParentHash(), block.NumberU64()-1), block)
|
||||
|
||||
finalBlock, err := engine.Seal(chain, block, nil)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
if finalBlock.Hash() != expectedBlock.Hash() {
|
||||
t.Errorf("hash mismatch: have %v, want %v", finalBlock.Hash(), expectedBlock.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyHeader(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
|
||||
// errEmptyCommittedSeals case
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
block, _ = engine.updateBlock(chain.Genesis().Header(), block)
|
||||
err := engine.VerifyHeader(chain, block.Header(), false)
|
||||
if err != errEmptyCommittedSeals {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errEmptyCommittedSeals)
|
||||
}
|
||||
|
||||
// short extra data
|
||||
header := block.Header()
|
||||
header.Extra = []byte{}
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidExtraDataFormat {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidExtraDataFormat)
|
||||
}
|
||||
// incorrect extra format
|
||||
header.Extra = []byte("0000000000000000000000000000000012300000000000000000000000000000000000000000000000000000000000000000")
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidExtraDataFormat {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidExtraDataFormat)
|
||||
}
|
||||
|
||||
// non zero MixDigest
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
header.MixDigest = common.StringToHash("123456789")
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidMixDigest {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidMixDigest)
|
||||
}
|
||||
|
||||
// invalid uncles hash
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
header.UncleHash = common.StringToHash("123456789")
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidUncleHash {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidUncleHash)
|
||||
}
|
||||
|
||||
// invalid difficulty
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
header.Difficulty = big.NewInt(2)
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidDifficulty {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidDifficulty)
|
||||
}
|
||||
|
||||
// invalid timestamp
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
header.Time = new(big.Int).Add(chain.Genesis().Time(), new(big.Int).SetUint64(engine.config.BlockPeriod-1))
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidTimestamp {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidTimestamp)
|
||||
}
|
||||
|
||||
// future block
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
header.Time = new(big.Int).Add(big.NewInt(now().Unix()), new(big.Int).SetUint64(10))
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != consensus.ErrFutureBlock {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, consensus.ErrFutureBlock)
|
||||
}
|
||||
|
||||
// invalid nonce
|
||||
block = makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
header = block.Header()
|
||||
copy(header.Nonce[:], hexutil.MustDecode("0x111111111111"))
|
||||
header.Number = big.NewInt(int64(engine.config.Epoch))
|
||||
err = engine.VerifyHeader(chain, header, false)
|
||||
if err != errInvalidNonce {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidNonce)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySeal(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
genesis := chain.Genesis()
|
||||
// cannot verify genesis
|
||||
err := engine.VerifySeal(chain, genesis.Header())
|
||||
if err != errUnknownBlock {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errUnknownBlock)
|
||||
}
|
||||
|
||||
block := makeBlock(chain, engine, genesis)
|
||||
// change block content
|
||||
header := block.Header()
|
||||
header.Number = big.NewInt(4)
|
||||
block1 := block.WithSeal(header)
|
||||
err = engine.VerifySeal(chain, block1.Header())
|
||||
if err != errUnauthorized {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errUnauthorized)
|
||||
}
|
||||
|
||||
// unauthorized users but still can get correct signer address
|
||||
engine.privateKey, _ = crypto.GenerateKey()
|
||||
err = engine.VerifySeal(chain, block.Header())
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyHeaders(t *testing.T) {
|
||||
chain, engine := newBlockChain(1)
|
||||
genesis := chain.Genesis()
|
||||
|
||||
// success case
|
||||
headers := []*types.Header{}
|
||||
blocks := []*types.Block{}
|
||||
size := 100
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
var b *types.Block
|
||||
if i == 0 {
|
||||
b = makeBlockWithoutSeal(chain, engine, genesis)
|
||||
b, _ = engine.updateBlock(genesis.Header(), b)
|
||||
} else {
|
||||
b = makeBlockWithoutSeal(chain, engine, blocks[i-1])
|
||||
b, _ = engine.updateBlock(blocks[i-1].Header(), b)
|
||||
}
|
||||
blocks = append(blocks, b)
|
||||
headers = append(headers, blocks[i].Header())
|
||||
}
|
||||
now = func() time.Time {
|
||||
return time.Unix(headers[size-1].Time.Int64(), 0)
|
||||
}
|
||||
_, results := engine.VerifyHeaders(chain, headers, nil)
|
||||
const timeoutDura = 2 * time.Second
|
||||
timeout := time.NewTimer(timeoutDura)
|
||||
index := 0
|
||||
OUT1:
|
||||
for {
|
||||
select {
|
||||
case err := <-results:
|
||||
if err != nil {
|
||||
if err != errEmptyCommittedSeals && err != errInvalidCommittedSeals {
|
||||
t.Errorf("error mismatch: have %v, want errEmptyCommittedSeals|errInvalidCommittedSeals", err)
|
||||
break OUT1
|
||||
}
|
||||
}
|
||||
index++
|
||||
if index == size {
|
||||
break OUT1
|
||||
}
|
||||
case <-timeout.C:
|
||||
break OUT1
|
||||
}
|
||||
}
|
||||
// abort cases
|
||||
abort, results := engine.VerifyHeaders(chain, headers, nil)
|
||||
timeout = time.NewTimer(timeoutDura)
|
||||
index = 0
|
||||
OUT2:
|
||||
for {
|
||||
select {
|
||||
case err := <-results:
|
||||
if err != nil {
|
||||
if err != errEmptyCommittedSeals && err != errInvalidCommittedSeals {
|
||||
t.Errorf("error mismatch: have %v, want errEmptyCommittedSeals|errInvalidCommittedSeals", err)
|
||||
break OUT2
|
||||
}
|
||||
}
|
||||
index++
|
||||
if index == 5 {
|
||||
abort <- struct{}{}
|
||||
}
|
||||
if index >= size {
|
||||
t.Errorf("verifyheaders should be aborted")
|
||||
break OUT2
|
||||
}
|
||||
case <-timeout.C:
|
||||
break OUT2
|
||||
}
|
||||
}
|
||||
// error header cases
|
||||
headers[2].Number = big.NewInt(100)
|
||||
abort, results = engine.VerifyHeaders(chain, headers, nil)
|
||||
timeout = time.NewTimer(timeoutDura)
|
||||
index = 0
|
||||
errors := 0
|
||||
expectedErrors := 2
|
||||
OUT3:
|
||||
for {
|
||||
select {
|
||||
case err := <-results:
|
||||
if err != nil {
|
||||
if err != errEmptyCommittedSeals && err != errInvalidCommittedSeals {
|
||||
errors++
|
||||
}
|
||||
}
|
||||
index++
|
||||
if index == size {
|
||||
if errors != expectedErrors {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, expectedErrors)
|
||||
}
|
||||
break OUT3
|
||||
}
|
||||
case <-timeout.C:
|
||||
break OUT3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareExtra(t *testing.T) {
|
||||
validators := make([]common.Address, 4)
|
||||
validators[0] = common.BytesToAddress(hexutil.MustDecode("0x44add0ec310f115a0e603b2d7db9f067778eaf8a"))
|
||||
validators[1] = common.BytesToAddress(hexutil.MustDecode("0x294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212"))
|
||||
validators[2] = common.BytesToAddress(hexutil.MustDecode("0x6beaaed781d2d2ab6350f5c4566a2c6eaac407a6"))
|
||||
validators[3] = common.BytesToAddress(hexutil.MustDecode("0x8be76812f765c24641ec63dc2852b378aba2b440"))
|
||||
|
||||
vanity := make([]byte, types.IstanbulExtraVanity)
|
||||
expectedResult := append(vanity, hexutil.MustDecode("0xf858f8549444add0ec310f115a0e603b2d7db9f067778eaf8a94294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212946beaaed781d2d2ab6350f5c4566a2c6eaac407a6948be76812f765c24641ec63dc2852b378aba2b44080c0")...)
|
||||
|
||||
h := &types.Header{
|
||||
Extra: vanity,
|
||||
}
|
||||
|
||||
payload, err := prepareExtra(h, validators)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want: nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(payload, expectedResult) {
|
||||
t.Errorf("payload mismatch: have %v, want %v", payload, expectedResult)
|
||||
}
|
||||
|
||||
// append useless information to extra-data
|
||||
h.Extra = append(vanity, make([]byte, 15)...)
|
||||
|
||||
payload, err = prepareExtra(h, validators)
|
||||
if !reflect.DeepEqual(payload, expectedResult) {
|
||||
t.Errorf("payload mismatch: have %v, want %v", payload, expectedResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSeal(t *testing.T) {
|
||||
vanity := bytes.Repeat([]byte{0x00}, types.IstanbulExtraVanity)
|
||||
istRawData := hexutil.MustDecode("0xf858f8549444add0ec310f115a0e603b2d7db9f067778eaf8a94294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212946beaaed781d2d2ab6350f5c4566a2c6eaac407a6948be76812f765c24641ec63dc2852b378aba2b44080c0")
|
||||
expectedSeal := append([]byte{1, 2, 3}, bytes.Repeat([]byte{0x00}, types.IstanbulExtraSeal-3)...)
|
||||
expectedIstExtra := &types.IstanbulExtra{
|
||||
Validators: []common.Address{
|
||||
common.BytesToAddress(hexutil.MustDecode("0x44add0ec310f115a0e603b2d7db9f067778eaf8a")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x6beaaed781d2d2ab6350f5c4566a2c6eaac407a6")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x8be76812f765c24641ec63dc2852b378aba2b440")),
|
||||
},
|
||||
Seal: expectedSeal,
|
||||
CommittedSeal: [][]byte{},
|
||||
}
|
||||
var expectedErr error
|
||||
|
||||
h := &types.Header{
|
||||
Extra: append(vanity, istRawData...),
|
||||
}
|
||||
|
||||
// normal case
|
||||
err := writeSeal(h, expectedSeal)
|
||||
if err != expectedErr {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, expectedErr)
|
||||
}
|
||||
|
||||
// verify istanbul extra-data
|
||||
istExtra, err := types.ExtractIstanbulExtra(h)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(istExtra, expectedIstExtra) {
|
||||
t.Errorf("extra data mismatch: have %v, want %v", istExtra, expectedIstExtra)
|
||||
}
|
||||
|
||||
// invalid seal
|
||||
unexpectedSeal := append(expectedSeal, make([]byte, 1)...)
|
||||
err = writeSeal(h, unexpectedSeal)
|
||||
if err != errInvalidSignature {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCommittedSeals(t *testing.T) {
|
||||
vanity := bytes.Repeat([]byte{0x00}, types.IstanbulExtraVanity)
|
||||
istRawData := hexutil.MustDecode("0xf858f8549444add0ec310f115a0e603b2d7db9f067778eaf8a94294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212946beaaed781d2d2ab6350f5c4566a2c6eaac407a6948be76812f765c24641ec63dc2852b378aba2b44080c0")
|
||||
expectedCommittedSeal := append([]byte{1, 2, 3}, bytes.Repeat([]byte{0x00}, types.IstanbulExtraSeal-3)...)
|
||||
expectedIstExtra := &types.IstanbulExtra{
|
||||
Validators: []common.Address{
|
||||
common.BytesToAddress(hexutil.MustDecode("0x44add0ec310f115a0e603b2d7db9f067778eaf8a")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x294fc7e8f22b3bcdcf955dd7ff3ba2ed833f8212")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x6beaaed781d2d2ab6350f5c4566a2c6eaac407a6")),
|
||||
common.BytesToAddress(hexutil.MustDecode("0x8be76812f765c24641ec63dc2852b378aba2b440")),
|
||||
},
|
||||
Seal: []byte{},
|
||||
CommittedSeal: [][]byte{expectedCommittedSeal},
|
||||
}
|
||||
var expectedErr error
|
||||
|
||||
h := &types.Header{
|
||||
Extra: append(vanity, istRawData...),
|
||||
}
|
||||
|
||||
// normal case
|
||||
err := writeCommittedSeals(h, [][]byte{expectedCommittedSeal})
|
||||
if err != expectedErr {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, expectedErr)
|
||||
}
|
||||
|
||||
// verify istanbul extra-data
|
||||
istExtra, err := types.ExtractIstanbulExtra(h)
|
||||
if err != nil {
|
||||
t.Errorf("error mismatch: have %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(istExtra, expectedIstExtra) {
|
||||
t.Errorf("extra data mismatch: have %v, want %v", istExtra, expectedIstExtra)
|
||||
}
|
||||
|
||||
// invalid seal
|
||||
unexpectedCommittedSeal := append(expectedCommittedSeal, make([]byte, 1)...)
|
||||
err = writeCommittedSeals(h, [][]byte{unexpectedCommittedSeal})
|
||||
if err != errInvalidCommittedSeals {
|
||||
t.Errorf("error mismatch: have %v, want %v", err, errInvalidCommittedSeals)
|
||||
}
|
||||
}
|
||||
103
consensus/istanbul/backend/handler.go
Normal file
103
consensus/istanbul/backend/handler.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
const (
|
||||
istanbulMsg = 0x11
|
||||
)
|
||||
|
||||
var (
|
||||
// errDecodeFailed is returned when decode message fails
|
||||
errDecodeFailed = errors.New("fail to decode istanbul message")
|
||||
)
|
||||
|
||||
// Protocol implements consensus.Engine.Protocol
|
||||
func (sb *backend) Protocol() consensus.Protocol {
|
||||
return consensus.Protocol{
|
||||
Name: "istanbul",
|
||||
Versions: []uint{64},
|
||||
Lengths: []uint64{18},
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMsg implements consensus.Handler.HandleMsg
|
||||
func (sb *backend) HandleMsg(addr common.Address, msg p2p.Msg) (bool, error) {
|
||||
sb.coreMu.Lock()
|
||||
defer sb.coreMu.Unlock()
|
||||
|
||||
if msg.Code == istanbulMsg {
|
||||
if !sb.coreStarted {
|
||||
return true, istanbul.ErrStoppedEngine
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if err := msg.Decode(&data); err != nil {
|
||||
return true, errDecodeFailed
|
||||
}
|
||||
|
||||
hash := istanbul.RLPHash(data)
|
||||
|
||||
// Mark peer's message
|
||||
ms, ok := sb.recentMessages.Get(addr)
|
||||
var m *lru.ARCCache
|
||||
if ok {
|
||||
m, _ = ms.(*lru.ARCCache)
|
||||
} else {
|
||||
m, _ = lru.NewARC(inmemoryMessages)
|
||||
sb.recentMessages.Add(addr, m)
|
||||
}
|
||||
m.Add(hash, true)
|
||||
|
||||
// Mark self known message
|
||||
if _, ok := sb.knownMessages.Get(hash); ok {
|
||||
return true, nil
|
||||
}
|
||||
sb.knownMessages.Add(hash, true)
|
||||
|
||||
go sb.istanbulEventMux.Post(istanbul.MessageEvent{
|
||||
Payload: data,
|
||||
})
|
||||
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SetBroadcaster implements consensus.Handler.SetBroadcaster
|
||||
func (sb *backend) SetBroadcaster(broadcaster consensus.Broadcaster) {
|
||||
sb.broadcaster = broadcaster
|
||||
}
|
||||
|
||||
func (sb *backend) NewChainHead() error {
|
||||
sb.coreMu.RLock()
|
||||
defer sb.coreMu.RUnlock()
|
||||
if !sb.coreStarted {
|
||||
return istanbul.ErrStoppedEngine
|
||||
}
|
||||
go sb.istanbulEventMux.Post(istanbul.FinalCommittedEvent{})
|
||||
return nil
|
||||
}
|
||||
72
consensus/istanbul/backend/handler_test.go
Normal file
72
consensus/istanbul/backend/handler_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
func TestIstanbulMessage(t *testing.T) {
|
||||
_, backend := newBlockChain(1)
|
||||
|
||||
// generate one msg
|
||||
data := []byte("data1")
|
||||
hash := istanbul.RLPHash(data)
|
||||
msg := makeMsg(istanbulMsg, data)
|
||||
addr := common.StringToAddress("address")
|
||||
|
||||
// 1. this message should not be in cache
|
||||
// for peers
|
||||
if _, ok := backend.recentMessages.Get(addr); ok {
|
||||
t.Fatalf("the cache of messages for this peer should be nil")
|
||||
}
|
||||
|
||||
// for self
|
||||
if _, ok := backend.knownMessages.Get(hash); ok {
|
||||
t.Fatalf("the cache of messages should be nil")
|
||||
}
|
||||
|
||||
// 2. this message should be in cache after we handle it
|
||||
_, err := backend.HandleMsg(addr, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("handle message failed: %v", err)
|
||||
}
|
||||
// for peers
|
||||
if ms, ok := backend.recentMessages.Get(addr); ms == nil || !ok {
|
||||
t.Fatalf("the cache of messages for this peer cannot be nil")
|
||||
} else if m, ok := ms.(*lru.ARCCache); !ok {
|
||||
t.Fatalf("the cache of messages for this peer cannot be casted")
|
||||
} else if _, ok := m.Get(hash); !ok {
|
||||
t.Fatalf("the cache of messages for this peer cannot be found")
|
||||
}
|
||||
|
||||
// for self
|
||||
if _, ok := backend.knownMessages.Get(hash); !ok {
|
||||
t.Fatalf("the cache of messages cannot be found")
|
||||
}
|
||||
}
|
||||
|
||||
func makeMsg(msgcode uint64, data interface{}) p2p.Msg {
|
||||
size, r, _ := rlp.EncodeToReader(data)
|
||||
return p2p.Msg{Code: msgcode, Size: uint32(size), Payload: r}
|
||||
}
|
||||
321
consensus/istanbul/backend/snapshot.go
Normal file
321
consensus/istanbul/backend/snapshot.go
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
const (
|
||||
dbKeySnapshotPrefix = "istanbul-snapshot"
|
||||
)
|
||||
|
||||
// Vote represents a single vote that an authorized validator made to modify the
|
||||
// list of authorizations.
|
||||
type Vote struct {
|
||||
Validator common.Address `json:"validator"` // Authorized validator that cast this vote
|
||||
Block uint64 `json:"block"` // Block number the vote was cast in (expire old votes)
|
||||
Address common.Address `json:"address"` // Account being voted on to change its authorization
|
||||
Authorize bool `json:"authorize"` // Whether to authorize or deauthorize the voted account
|
||||
}
|
||||
|
||||
// Tally is a simple vote tally to keep the current score of votes. Votes that
|
||||
// go against the proposal aren't counted since it's equivalent to not voting.
|
||||
type Tally struct {
|
||||
Authorize bool `json:"authorize"` // Whether the vote it about authorizing or kicking someone
|
||||
Votes int `json:"votes"` // Number of votes until now wanting to pass the proposal
|
||||
}
|
||||
|
||||
// Snapshot is the state of the authorization voting at a given point in time.
|
||||
type Snapshot struct {
|
||||
Epoch uint64 // The number of blocks after which to checkpoint and reset the pending votes
|
||||
|
||||
Number uint64 // Block number where the snapshot was created
|
||||
Hash common.Hash // Block hash where the snapshot was created
|
||||
Votes []*Vote // List of votes cast in chronological order
|
||||
Tally map[common.Address]Tally // Current vote tally to avoid recalculating
|
||||
ValSet istanbul.ValidatorSet // Set of authorized validators at this moment
|
||||
}
|
||||
|
||||
// newSnapshot create a new snapshot with the specified startup parameters. This
|
||||
// method does not initialize the set of recent validators, so only ever use if for
|
||||
// the genesis block.
|
||||
func newSnapshot(epoch uint64, number uint64, hash common.Hash, valSet istanbul.ValidatorSet) *Snapshot {
|
||||
snap := &Snapshot{
|
||||
Epoch: epoch,
|
||||
Number: number,
|
||||
Hash: hash,
|
||||
ValSet: valSet,
|
||||
Tally: make(map[common.Address]Tally),
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
// loadSnapshot loads an existing snapshot from the database.
|
||||
func loadSnapshot(epoch uint64, db ethdb.Database, hash common.Hash) (*Snapshot, error) {
|
||||
blob, err := db.Get(append([]byte(dbKeySnapshotPrefix), hash[:]...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snap := new(Snapshot)
|
||||
if err := json.Unmarshal(blob, snap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snap.Epoch = epoch
|
||||
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// store inserts the snapshot into the database.
|
||||
func (s *Snapshot) store(db ethdb.Database) error {
|
||||
blob, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Put(append([]byte(dbKeySnapshotPrefix), s.Hash[:]...), blob)
|
||||
}
|
||||
|
||||
// copy creates a deep copy of the snapshot, though not the individual votes.
|
||||
func (s *Snapshot) copy() *Snapshot {
|
||||
cpy := &Snapshot{
|
||||
Epoch: s.Epoch,
|
||||
Number: s.Number,
|
||||
Hash: s.Hash,
|
||||
ValSet: s.ValSet.Copy(),
|
||||
Votes: make([]*Vote, len(s.Votes)),
|
||||
Tally: make(map[common.Address]Tally),
|
||||
}
|
||||
|
||||
for address, tally := range s.Tally {
|
||||
cpy.Tally[address] = tally
|
||||
}
|
||||
copy(cpy.Votes, s.Votes)
|
||||
|
||||
return cpy
|
||||
}
|
||||
|
||||
// checkVote return whether it's a valid vote
|
||||
func (s *Snapshot) checkVote(address common.Address, authorize bool) bool {
|
||||
_, validator := s.ValSet.GetByAddress(address)
|
||||
return (validator != nil && !authorize) || (validator == nil && authorize)
|
||||
}
|
||||
|
||||
// cast adds a new vote into the tally.
|
||||
func (s *Snapshot) cast(address common.Address, authorize bool) bool {
|
||||
// Ensure the vote is meaningful
|
||||
if !s.checkVote(address, authorize) {
|
||||
return false
|
||||
}
|
||||
// Cast the vote into an existing or new tally
|
||||
if old, ok := s.Tally[address]; ok {
|
||||
old.Votes++
|
||||
s.Tally[address] = old
|
||||
} else {
|
||||
s.Tally[address] = Tally{Authorize: authorize, Votes: 1}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// uncast removes a previously cast vote from the tally.
|
||||
func (s *Snapshot) uncast(address common.Address, authorize bool) bool {
|
||||
// If there's no tally, it's a dangling vote, just drop
|
||||
tally, ok := s.Tally[address]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Ensure we only revert counted votes
|
||||
if tally.Authorize != authorize {
|
||||
return false
|
||||
}
|
||||
// Otherwise revert the vote
|
||||
if tally.Votes > 1 {
|
||||
tally.Votes--
|
||||
s.Tally[address] = tally
|
||||
} else {
|
||||
delete(s.Tally, address)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// apply creates a new authorization snapshot by applying the given headers to
|
||||
// the original one.
|
||||
func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) {
|
||||
// Allow passing in no headers for cleaner code
|
||||
if len(headers) == 0 {
|
||||
return s, nil
|
||||
}
|
||||
// Sanity check that the headers can be applied
|
||||
for i := 0; i < len(headers)-1; i++ {
|
||||
if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 {
|
||||
return nil, errInvalidVotingChain
|
||||
}
|
||||
}
|
||||
if headers[0].Number.Uint64() != s.Number+1 {
|
||||
return nil, errInvalidVotingChain
|
||||
}
|
||||
// Iterate through the headers and create a new snapshot
|
||||
snap := s.copy()
|
||||
|
||||
for _, header := range headers {
|
||||
// Remove any votes on checkpoint blocks
|
||||
number := header.Number.Uint64()
|
||||
if number%s.Epoch == 0 {
|
||||
snap.Votes = nil
|
||||
snap.Tally = make(map[common.Address]Tally)
|
||||
}
|
||||
// Resolve the authorization key and check against validators
|
||||
validator, err := ecrecover(header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, v := snap.ValSet.GetByAddress(validator); v == nil {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
|
||||
// Header authorized, discard any previous votes from the validator
|
||||
for i, vote := range snap.Votes {
|
||||
if vote.Validator == validator && vote.Address == header.Coinbase {
|
||||
// Uncast the vote from the cached tally
|
||||
snap.uncast(vote.Address, vote.Authorize)
|
||||
|
||||
// Uncast the vote from the chronological list
|
||||
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
|
||||
break // only one vote allowed
|
||||
}
|
||||
}
|
||||
// Tally up the new vote from the validator
|
||||
var authorize bool
|
||||
switch {
|
||||
case bytes.Equal(header.Nonce[:], nonceAuthVote):
|
||||
authorize = true
|
||||
case bytes.Equal(header.Nonce[:], nonceDropVote):
|
||||
authorize = false
|
||||
default:
|
||||
return nil, errInvalidVote
|
||||
}
|
||||
if snap.cast(header.Coinbase, authorize) {
|
||||
snap.Votes = append(snap.Votes, &Vote{
|
||||
Validator: validator,
|
||||
Block: number,
|
||||
Address: header.Coinbase,
|
||||
Authorize: authorize,
|
||||
})
|
||||
}
|
||||
// If the vote passed, update the list of validators
|
||||
if tally := snap.Tally[header.Coinbase]; tally.Votes > snap.ValSet.Size()/2 {
|
||||
if tally.Authorize {
|
||||
snap.ValSet.AddValidator(header.Coinbase)
|
||||
} else {
|
||||
snap.ValSet.RemoveValidator(header.Coinbase)
|
||||
|
||||
// Discard any previous votes the deauthorized validator cast
|
||||
for i := 0; i < len(snap.Votes); i++ {
|
||||
if snap.Votes[i].Validator == header.Coinbase {
|
||||
// Uncast the vote from the cached tally
|
||||
snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize)
|
||||
|
||||
// Uncast the vote from the chronological list
|
||||
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
|
||||
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
// Discard any previous votes around the just changed account
|
||||
for i := 0; i < len(snap.Votes); i++ {
|
||||
if snap.Votes[i].Address == header.Coinbase {
|
||||
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
delete(snap.Tally, header.Coinbase)
|
||||
}
|
||||
}
|
||||
snap.Number += uint64(len(headers))
|
||||
snap.Hash = headers[len(headers)-1].Hash()
|
||||
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// validators retrieves the list of authorized validators in ascending order.
|
||||
func (s *Snapshot) validators() []common.Address {
|
||||
validators := make([]common.Address, 0, s.ValSet.Size())
|
||||
for _, validator := range s.ValSet.List() {
|
||||
validators = append(validators, validator.Address())
|
||||
}
|
||||
for i := 0; i < len(validators); i++ {
|
||||
for j := i + 1; j < len(validators); j++ {
|
||||
if bytes.Compare(validators[i][:], validators[j][:]) > 0 {
|
||||
validators[i], validators[j] = validators[j], validators[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return validators
|
||||
}
|
||||
|
||||
type snapshotJSON struct {
|
||||
Epoch uint64 `json:"epoch"`
|
||||
Number uint64 `json:"number"`
|
||||
Hash common.Hash `json:"hash"`
|
||||
Votes []*Vote `json:"votes"`
|
||||
Tally map[common.Address]Tally `json:"tally"`
|
||||
|
||||
// for validator set
|
||||
Validators []common.Address `json:"validators"`
|
||||
Policy istanbul.ProposerPolicy `json:"policy"`
|
||||
}
|
||||
|
||||
func (s *Snapshot) toJSONStruct() *snapshotJSON {
|
||||
return &snapshotJSON{
|
||||
Epoch: s.Epoch,
|
||||
Number: s.Number,
|
||||
Hash: s.Hash,
|
||||
Votes: s.Votes,
|
||||
Tally: s.Tally,
|
||||
Validators: s.validators(),
|
||||
Policy: s.ValSet.Policy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal from a json byte array
|
||||
func (s *Snapshot) UnmarshalJSON(b []byte) error {
|
||||
var j snapshotJSON
|
||||
if err := json.Unmarshal(b, &j); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Epoch = j.Epoch
|
||||
s.Number = j.Number
|
||||
s.Hash = j.Hash
|
||||
s.Votes = j.Votes
|
||||
s.Tally = j.Tally
|
||||
s.ValSet = validator.NewSet(j.Validators, j.Policy)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal to a json byte array
|
||||
func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||
j := s.toJSONStruct()
|
||||
return json.Marshal(j)
|
||||
}
|
||||
455
consensus/istanbul/backend/snapshot_test.go
Normal file
455
consensus/istanbul/backend/snapshot_test.go
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
type testerVote struct {
|
||||
validator string
|
||||
voted string
|
||||
auth bool
|
||||
}
|
||||
|
||||
// testerAccountPool is a pool to maintain currently active tester accounts,
|
||||
// mapped from textual names used in the tests below to actual Ethereum private
|
||||
// keys capable of signing transactions.
|
||||
type testerAccountPool struct {
|
||||
accounts map[string]*ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func newTesterAccountPool() *testerAccountPool {
|
||||
return &testerAccountPool{
|
||||
accounts: make(map[string]*ecdsa.PrivateKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *testerAccountPool) sign(header *types.Header, validator string) {
|
||||
// Ensure we have a persistent key for the validator
|
||||
if ap.accounts[validator] == nil {
|
||||
ap.accounts[validator], _ = crypto.GenerateKey()
|
||||
}
|
||||
// Sign the header and embed the signature in extra data
|
||||
hashData := crypto.Keccak256([]byte(sigHash(header).Bytes()))
|
||||
sig, _ := crypto.Sign(hashData, ap.accounts[validator])
|
||||
|
||||
writeSeal(header, sig)
|
||||
}
|
||||
|
||||
func (ap *testerAccountPool) address(account string) common.Address {
|
||||
// Ensure we have a persistent key for the account
|
||||
if ap.accounts[account] == nil {
|
||||
ap.accounts[account], _ = crypto.GenerateKey()
|
||||
}
|
||||
// Resolve and return the Ethereum address
|
||||
return crypto.PubkeyToAddress(ap.accounts[account].PublicKey)
|
||||
}
|
||||
|
||||
// Tests that voting is evaluated correctly for various simple and complex scenarios.
|
||||
func TestVoting(t *testing.T) {
|
||||
// Define the various voting scenarios to test
|
||||
tests := []struct {
|
||||
epoch uint64
|
||||
validators []string
|
||||
votes []testerVote
|
||||
results []string
|
||||
}{
|
||||
{
|
||||
// Single validator, no votes cast
|
||||
validators: []string{"A"},
|
||||
votes: []testerVote{{validator: "A"}},
|
||||
results: []string{"A"},
|
||||
}, {
|
||||
// Single validator, voting to add two others (only accept first, second needs 2 votes)
|
||||
validators: []string{"A"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "B", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Two validators, voting to add three others (only accept first two, third needs 3 votes already)
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
{validator: "B", voted: "C", auth: true},
|
||||
{validator: "A", voted: "D", auth: true},
|
||||
{validator: "B", voted: "D", auth: true},
|
||||
{validator: "C"},
|
||||
{validator: "A", voted: "E", auth: true},
|
||||
{validator: "B", voted: "E", auth: true},
|
||||
},
|
||||
results: []string{"A", "B", "C", "D"},
|
||||
}, {
|
||||
// Single validator, dropping itself (weird, but one less cornercase by explicitly allowing this)
|
||||
validators: []string{"A"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "A", auth: false},
|
||||
},
|
||||
results: []string{},
|
||||
}, {
|
||||
// Two validators, actually needing mutual consent to drop either of them (not fulfilled)
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Two validators, actually needing mutual consent to drop either of them (fulfilled)
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
{validator: "B", voted: "B", auth: false},
|
||||
},
|
||||
results: []string{"A"},
|
||||
}, {
|
||||
// Three validators, two of them deciding to drop the third
|
||||
validators: []string{"A", "B", "C"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Four validators, consensus of two not being enough to drop anyone
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
},
|
||||
results: []string{"A", "B", "C", "D"},
|
||||
}, {
|
||||
// Four validators, consensus of three already being enough to drop someone
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "D", auth: false},
|
||||
{validator: "B", voted: "D", auth: false},
|
||||
{validator: "C", voted: "D", auth: false},
|
||||
},
|
||||
results: []string{"A", "B", "C"},
|
||||
}, {
|
||||
// Authorizations are counted once per validator per target
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Authorizing multiple accounts concurrently is permitted
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "D", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "D", auth: true},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B", "C", "D"},
|
||||
}, {
|
||||
// Deauthorizations are counted once per validator per target
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Deauthorizing multiple accounts concurrently is permitted
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "C"},
|
||||
{validator: "A", voted: "D", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "C"},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "D", auth: false},
|
||||
{validator: "C", voted: "D", auth: false},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Votes from deauthorized validators are discarded immediately (deauth votes)
|
||||
validators: []string{"A", "B", "C"},
|
||||
votes: []testerVote{
|
||||
{validator: "C", voted: "B", auth: false},
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Votes from deauthorized validators are discarded immediately (auth votes)
|
||||
validators: []string{"A", "B", "C"},
|
||||
votes: []testerVote{
|
||||
{validator: "C", voted: "B", auth: false},
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
{validator: "A", voted: "B", auth: false},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Cascading changes are not allowed, only the the account being voted on may change
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "C"},
|
||||
{validator: "A", voted: "D", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
{validator: "C"},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "D", auth: false},
|
||||
{validator: "C", voted: "D", auth: false},
|
||||
},
|
||||
results: []string{"A", "B", "C"},
|
||||
}, {
|
||||
// Changes reaching consensus out of bounds (via a deauth) execute on touch
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "C"},
|
||||
{validator: "A", voted: "D", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
{validator: "C"},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "D", auth: false},
|
||||
{validator: "C", voted: "D", auth: false},
|
||||
{validator: "A"},
|
||||
{validator: "C", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
}, {
|
||||
// Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch
|
||||
validators: []string{"A", "B", "C", "D"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: false},
|
||||
{validator: "B"},
|
||||
{validator: "C"},
|
||||
{validator: "A", voted: "D", auth: false},
|
||||
{validator: "B", voted: "C", auth: false},
|
||||
{validator: "C"},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "D", auth: false},
|
||||
{validator: "C", voted: "D", auth: false},
|
||||
{validator: "A"},
|
||||
{validator: "B", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B", "C"},
|
||||
}, {
|
||||
// Ensure that pending votes don't survive authorization status changes. This
|
||||
// corner case can only appear if a validator is quickly added, remove and then
|
||||
// readded (or the inverse), while one of the original voters dropped. If a
|
||||
// past vote is left cached in the system somewhere, this will interfere with
|
||||
// the final validator outcome.
|
||||
validators: []string{"A", "B", "C", "D", "E"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "F", auth: true}, // Authorize F, 3 votes needed
|
||||
{validator: "B", voted: "F", auth: true},
|
||||
{validator: "C", voted: "F", auth: true},
|
||||
{validator: "D", voted: "F", auth: false}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged")
|
||||
{validator: "E", voted: "F", auth: false},
|
||||
{validator: "B", voted: "F", auth: false},
|
||||
{validator: "C", voted: "F", auth: false},
|
||||
{validator: "D", voted: "F", auth: true}, // Almost authorize F, 2/3 votes needed
|
||||
{validator: "E", voted: "F", auth: true},
|
||||
{validator: "B", voted: "A", auth: false}, // Deauthorize A, 3 votes needed
|
||||
{validator: "C", voted: "A", auth: false},
|
||||
{validator: "D", voted: "A", auth: false},
|
||||
{validator: "B", voted: "F", auth: true}, // Finish authorizing F, 3/3 votes needed
|
||||
},
|
||||
results: []string{"B", "C", "D", "E", "F"},
|
||||
}, {
|
||||
// Epoch transitions reset all votes to allow chain checkpointing
|
||||
epoch: 3,
|
||||
validators: []string{"A", "B"},
|
||||
votes: []testerVote{
|
||||
{validator: "A", voted: "C", auth: true},
|
||||
{validator: "B"},
|
||||
{validator: "A"}, // Checkpoint block, (don't vote here, it's validated outside of snapshots)
|
||||
{validator: "B", voted: "C", auth: true},
|
||||
},
|
||||
results: []string{"A", "B"},
|
||||
},
|
||||
}
|
||||
// Run through the scenarios and test them
|
||||
for i, tt := range tests {
|
||||
// Create the account pool and generate the initial set of validators
|
||||
accounts := newTesterAccountPool()
|
||||
|
||||
validators := make([]common.Address, len(tt.validators))
|
||||
for j, validator := range tt.validators {
|
||||
validators[j] = accounts.address(validator)
|
||||
}
|
||||
for j := 0; j < len(validators); j++ {
|
||||
for k := j + 1; k < len(validators); k++ {
|
||||
if bytes.Compare(validators[j][:], validators[k][:]) > 0 {
|
||||
validators[j], validators[k] = validators[k], validators[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create the genesis block with the initial set of validators
|
||||
genesis := &core.Genesis{
|
||||
Difficulty: defaultDifficulty,
|
||||
Mixhash: types.IstanbulDigest,
|
||||
}
|
||||
b := genesis.ToBlock(nil)
|
||||
extra, _ := prepareExtra(b.Header(), validators)
|
||||
genesis.ExtraData = extra
|
||||
// Create a pristine blockchain with the genesis injected
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
genesis.Commit(db)
|
||||
|
||||
config := istanbul.DefaultConfig
|
||||
if tt.epoch != 0 {
|
||||
config.Epoch = tt.epoch
|
||||
}
|
||||
engine := New(config, accounts.accounts[tt.validators[0]], db).(*backend)
|
||||
chain, err := core.NewBlockChain(db, nil, genesis.Config, engine, vm.Config{})
|
||||
|
||||
// Assemble a chain of headers from the cast votes
|
||||
headers := make([]*types.Header, len(tt.votes))
|
||||
for j, vote := range tt.votes {
|
||||
headers[j] = &types.Header{
|
||||
Number: big.NewInt(int64(j) + 1),
|
||||
Time: big.NewInt(int64(j) * int64(config.BlockPeriod)),
|
||||
Coinbase: accounts.address(vote.voted),
|
||||
Difficulty: defaultDifficulty,
|
||||
MixDigest: types.IstanbulDigest,
|
||||
}
|
||||
extra, _ := prepareExtra(headers[j], validators)
|
||||
headers[j].Extra = extra
|
||||
if j > 0 {
|
||||
headers[j].ParentHash = headers[j-1].Hash()
|
||||
}
|
||||
if vote.auth {
|
||||
copy(headers[j].Nonce[:], nonceAuthVote)
|
||||
}
|
||||
copy(headers[j].Extra, genesis.ExtraData)
|
||||
accounts.sign(headers[j], vote.validator)
|
||||
}
|
||||
// Pass all the headers through clique and ensure tallying succeeds
|
||||
head := headers[len(headers)-1]
|
||||
|
||||
snap, err := engine.snapshot(chain, head.Number.Uint64(), head.Hash(), headers)
|
||||
if err != nil {
|
||||
t.Errorf("test %d: failed to create voting snapshot: %v", i, err)
|
||||
continue
|
||||
}
|
||||
// Verify the final list of validators against the expected ones
|
||||
validators = make([]common.Address, len(tt.results))
|
||||
for j, validator := range tt.results {
|
||||
validators[j] = accounts.address(validator)
|
||||
}
|
||||
for j := 0; j < len(validators); j++ {
|
||||
for k := j + 1; k < len(validators); k++ {
|
||||
if bytes.Compare(validators[j][:], validators[k][:]) > 0 {
|
||||
validators[j], validators[k] = validators[k], validators[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
result := snap.validators()
|
||||
if len(result) != len(validators) {
|
||||
t.Errorf("test %d: validators mismatch: have %x, want %x", i, result, validators)
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(result); j++ {
|
||||
if !bytes.Equal(result[j][:], validators[j][:]) {
|
||||
t.Errorf("test %d, validator %d: validator mismatch: have %x, want %x", i, j, result[j], validators[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAndLoad(t *testing.T) {
|
||||
snap := &Snapshot{
|
||||
Epoch: 5,
|
||||
Number: 10,
|
||||
Hash: common.HexToHash("1234567890"),
|
||||
Votes: []*Vote{
|
||||
{
|
||||
Validator: common.StringToAddress("1234567891"),
|
||||
Block: 15,
|
||||
Address: common.StringToAddress("1234567892"),
|
||||
Authorize: false,
|
||||
},
|
||||
},
|
||||
Tally: map[common.Address]Tally{
|
||||
common.StringToAddress("1234567893"): {
|
||||
Authorize: false,
|
||||
Votes: 20,
|
||||
},
|
||||
},
|
||||
ValSet: validator.NewSet([]common.Address{
|
||||
common.StringToAddress("1234567894"),
|
||||
common.StringToAddress("1234567895"),
|
||||
}, istanbul.RoundRobin),
|
||||
}
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
err := snap.store(db)
|
||||
if err != nil {
|
||||
t.Errorf("store snapshot failed: %v", err)
|
||||
}
|
||||
|
||||
snap1, err := loadSnapshot(snap.Epoch, db, snap.Hash)
|
||||
if err != nil {
|
||||
t.Errorf("load snapshot failed: %v", err)
|
||||
}
|
||||
if snap.Epoch != snap1.Epoch {
|
||||
t.Errorf("epoch mismatch: have %v, want %v", snap1.Epoch, snap.Epoch)
|
||||
}
|
||||
if snap.Hash != snap1.Hash {
|
||||
t.Errorf("hash mismatch: have %v, want %v", snap1.Number, snap.Number)
|
||||
}
|
||||
if !reflect.DeepEqual(snap.Votes, snap.Votes) {
|
||||
t.Errorf("votes mismatch: have %v, want %v", snap1.Votes, snap.Votes)
|
||||
}
|
||||
if !reflect.DeepEqual(snap.Tally, snap.Tally) {
|
||||
t.Errorf("tally mismatch: have %v, want %v", snap1.Tally, snap.Tally)
|
||||
}
|
||||
if !reflect.DeepEqual(snap.ValSet, snap.ValSet) {
|
||||
t.Errorf("validator set mismatch: have %v, want %v", snap1.ValSet, snap.ValSet)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue