From 72ae3ec221f59c87fda6720d19ead15f8868d904 Mon Sep 17 00:00:00 2001 From: "mark.lin" Date: Wed, 4 Oct 2017 11:04:18 +0800 Subject: [PATCH] consensus/istanbul: Istanbul consensus backend implementation --- consensus/istanbul/backend/api.go | 118 ++++ consensus/istanbul/backend/backend.go | 312 +++++++++ consensus/istanbul/backend/backend_test.go | 239 +++++++ consensus/istanbul/backend/engine.go | 717 ++++++++++++++++++++ consensus/istanbul/backend/engine_test.go | 549 +++++++++++++++ consensus/istanbul/backend/handler.go | 103 +++ consensus/istanbul/backend/handler_test.go | 72 ++ consensus/istanbul/backend/snapshot.go | 321 +++++++++ consensus/istanbul/backend/snapshot_test.go | 455 +++++++++++++ 9 files changed, 2886 insertions(+) create mode 100644 consensus/istanbul/backend/api.go create mode 100644 consensus/istanbul/backend/backend.go create mode 100644 consensus/istanbul/backend/backend_test.go create mode 100644 consensus/istanbul/backend/engine.go create mode 100644 consensus/istanbul/backend/engine_test.go create mode 100644 consensus/istanbul/backend/handler.go create mode 100644 consensus/istanbul/backend/handler_test.go create mode 100644 consensus/istanbul/backend/snapshot.go create mode 100644 consensus/istanbul/backend/snapshot_test.go diff --git a/consensus/istanbul/backend/api.go b/consensus/istanbul/backend/api.go new file mode 100644 index 0000000000..ee2a3e9d94 --- /dev/null +++ b/consensus/istanbul/backend/api.go @@ -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 . + +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) +} diff --git a/consensus/istanbul/backend/backend.go b/consensus/istanbul/backend/backend.go new file mode 100644 index 0000000000..03542ee94d --- /dev/null +++ b/consensus/istanbul/backend/backend.go @@ -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 . + +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) +} diff --git a/consensus/istanbul/backend/backend_test.go b/consensus/istanbul/backend/backend_test.go new file mode 100644 index 0000000000..822c0f5ec9 --- /dev/null +++ b/consensus/istanbul/backend/backend_test.go @@ -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 . + +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 +} diff --git a/consensus/istanbul/backend/engine.go b/consensus/istanbul/backend/engine.go new file mode 100644 index 0000000000..a365abf561 --- /dev/null +++ b/consensus/istanbul/backend/engine.go @@ -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 . + +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 +} diff --git a/consensus/istanbul/backend/engine_test.go b/consensus/istanbul/backend/engine_test.go new file mode 100644 index 0000000000..da37e248f6 --- /dev/null +++ b/consensus/istanbul/backend/engine_test.go @@ -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 . + +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) + } +} diff --git a/consensus/istanbul/backend/handler.go b/consensus/istanbul/backend/handler.go new file mode 100644 index 0000000000..a338009451 --- /dev/null +++ b/consensus/istanbul/backend/handler.go @@ -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 . + +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 +} diff --git a/consensus/istanbul/backend/handler_test.go b/consensus/istanbul/backend/handler_test.go new file mode 100644 index 0000000000..690a586be8 --- /dev/null +++ b/consensus/istanbul/backend/handler_test.go @@ -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 . + +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} +} diff --git a/consensus/istanbul/backend/snapshot.go b/consensus/istanbul/backend/snapshot.go new file mode 100644 index 0000000000..e369147f81 --- /dev/null +++ b/consensus/istanbul/backend/snapshot.go @@ -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 . + +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) +} diff --git a/consensus/istanbul/backend/snapshot_test.go b/consensus/istanbul/backend/snapshot_test.go new file mode 100644 index 0000000000..8dbaf808d6 --- /dev/null +++ b/consensus/istanbul/backend/snapshot_test.go @@ -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 . + +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) + } +}