consensus/istanbul: Istanbul consensus backend implementation

This commit is contained in:
mark.lin 2017-10-04 11:04:18 +08:00
parent efd8b5be6f
commit 72ae3ec221
9 changed files with 2886 additions and 0 deletions

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

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

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

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

View 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 = &params.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)
}
}

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

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

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

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