clique: add testcase to demonstrate epoch error when accounting for recent signers

This commit is contained in:
Martin Holst Swende 2018-09-08 01:10:53 +02:00
parent 5918b88a8f
commit f1c8bdc760
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
3 changed files with 196 additions and 27 deletions

View file

@ -20,6 +20,7 @@ package clique
import (
"bytes"
"errors"
"fmt"
"math/big"
"math/rand"
"sync"
@ -166,19 +167,12 @@ func sigHash(header *types.Header) (hash common.Hash) {
return hash
}
// ecrecover extracts the Ethereum account address from a signed header.
func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error) {
// If the signature's already cached, return that
hash := header.Hash()
if address, known := sigcache.Get(hash); known {
return address.(common.Address), nil
}
func RecoverSealer(header *types.Header) (common.Address, error) {
// Retrieve the signature from the header extra-data
if len(header.Extra) < extraSeal {
return common.Address{}, errMissingSignature
}
signature := header.Extra[len(header.Extra)-extraSeal:]
// Recover the public key and the Ethereum address
pubkey, err := crypto.Ecrecover(sigHash(header).Bytes(), signature)
if err != nil {
@ -186,7 +180,20 @@ func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, er
}
var signer common.Address
copy(signer[:], crypto.Keccak256(pubkey[1:])[12:])
return signer, nil
}
// ecrecover extracts the Ethereum account address from a signed header.
func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error) {
// If the signature's already cached, return that
hash := header.Hash()
if address, known := sigcache.Get(hash); known {
return address.(common.Address), nil
}
signer, err := RecoverSealer(header)
if err != nil {
return common.Address{}, err
}
sigcache.Add(hash, signer)
return signer, nil
}
@ -341,7 +348,7 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainReader, header *type
parent = chain.GetHeader(header.ParentHash, number-1)
}
if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash {
return consensus.ErrUnknownAncestor
return fmt.Errorf("unknown ancestor [0x%x] for block %d", header.ParentHash, number)
}
if parent.Time.Uint64()+c.config.Period > header.Time.Uint64() {
return ErrInvalidTimestamp
@ -411,14 +418,14 @@ func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash commo
// 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
return nil, fmt.Errorf("unknown ancestor [0x%x] for block %d", header.ParentHash, number)
}
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
return nil, fmt.Errorf("unknown ancestor [0x%x] for block %d", hash, number)
}
}
headers = append(headers, header)
@ -481,23 +488,23 @@ func (c *Clique) verifySeal(chain consensus.ChainReader, header *types.Header, p
return err
}
if _, ok := snap.Signers[signer]; !ok {
return errUnauthorized
return fmt.Errorf("unauthorised[2]: signer 0x%x is not one of the signers", signer)
}
for seen, recent := range snap.Recents {
if recent == signer {
// Signer is among recents, only fail if the current block doesn't shift it out
if limit := uint64(len(snap.Signers)/2 + 1); seen > number-limit {
return errUnauthorized
return fmt.Errorf("unauthorised: signer 0x%x recently signed", signer)
}
}
}
// Ensure that the difficulty corresponds to the turn-ness of the signer
inturn := snap.inturn(header.Number.Uint64(), signer)
if inturn && header.Difficulty.Cmp(diffInTurn) != 0 {
return errInvalidDifficulty
return fmt.Errorf("Invalid difficulty [block %d], was %d expected %d (in-turn)", header.Number, header.Difficulty, diffInTurn)
}
if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 {
return errInvalidDifficulty
return fmt.Errorf("Invalid difficulty [block %d], was %d expected %d (out-of-turn)", header.Number, header.Difficulty, diffNoTurn)
}
return nil
}
@ -558,7 +565,7 @@ func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header) erro
// Ensure the timestamp has the correct delay
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil {
return consensus.ErrUnknownAncestor
return fmt.Errorf("unknown ancestor [0x%x] for block %d", header.ParentHash, number)
}
header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(c.config.Period))
if header.Time.Int64() < time.Now().Unix() {
@ -613,7 +620,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c
return err
}
if _, authorized := snap.Signers[signer]; !authorized {
return errUnauthorized
return fmt.Errorf("unauthorised[1]: signer 0x%x is not one of the signers", signer)
}
// If we're amongst the recent signers, wait for the next block
for seen, recent := range snap.Recents {

View file

@ -19,6 +19,9 @@ package clique
import (
"bytes"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/core/vm"
"io"
"math/big"
"testing"
@ -42,6 +45,8 @@ type testerVote struct {
// keys capable of signing transactions.
type testerAccountPool struct {
accounts map[string]*ecdsa.PrivateKey
// can be used to always yield the same accounts
fakePRNG io.Reader
}
func newTesterAccountPool() *testerAccountPool {
@ -49,22 +54,53 @@ func newTesterAccountPool() *testerAccountPool {
accounts: make(map[string]*ecdsa.PrivateKey),
}
}
func newDeterministicTesterAccountPool() *testerAccountPool {
return &testerAccountPool{
accounts: make(map[string]*ecdsa.PrivateKey),
fakePRNG: &fakeRand{},
}
}
func (ap *testerAccountPool) sign(header *types.Header, signer string) {
type fakeRand struct {
counter uint64
}
func (f *fakeRand) Read(p []byte) (n int, err error) {
for i := 0; i < len(p); i++ {
p[i] = byte(f.counter)
}
f.counter++
return len(p), nil
}
func (ap *testerAccountPool) getSignature(header *types.Header, signer string) ([]byte, error) {
// Ensure we have a persistent key for the signer
if ap.accounts[signer] == nil {
if ap.fakePRNG != nil {
ap.accounts[signer], _ = ecdsa.GenerateKey(crypto.S256(), ap.fakePRNG)
} else {
ap.accounts[signer], _ = crypto.GenerateKey()
}
}
// Sign the header and embed the signature in extra data
sig, _ := crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer])
return crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer])
}
func (ap *testerAccountPool) sign(header *types.Header, signer string) {
// Sign the header and embed the signature in extra data
sig, _ := ap.getSignature(header, signer)
copy(header.Extra[len(header.Extra)-65:], sig)
}
func (ap *testerAccountPool) address(account string) common.Address {
// Ensure we have a persistent key for the account
if ap.accounts[account] == nil {
if ap.fakePRNG != nil {
ap.accounts[account], _ = ecdsa.GenerateKey(crypto.S256(), ap.fakePRNG)
} else {
ap.accounts[account], _ = crypto.GenerateKey()
}
}
// Resolve and return the Ethereum address
return crypto.PubkeyToAddress(ap.accounts[account].PublicKey)
}
@ -77,14 +113,127 @@ type testerChainReader struct {
func (r *testerChainReader) Config() *params.ChainConfig { return params.AllCliqueProtocolChanges }
func (r *testerChainReader) CurrentHeader() *types.Header { panic("not supported") }
func (r *testerChainReader) GetHeader(common.Hash, uint64) *types.Header { panic("not supported") }
func (r *testerChainReader) GetHeader(hash common.Hash, number uint64) *types.Header {
return rawdb.ReadHeader(r.db, hash, number)
}
func (r *testerChainReader) GetBlock(common.Hash, uint64) *types.Block { panic("not supported") }
func (r *testerChainReader) GetHeaderByHash(common.Hash) *types.Header { panic("not supported") }
func (r *testerChainReader) GetHeaderByNumber(number uint64) *types.Header {
if number == 0 {
return rawdb.ReadHeader(r.db, rawdb.ReadCanonicalHash(r.db, 0), 0)
}
return nil
return rawdb.ReadHeader(r.db, rawdb.ReadCanonicalHash(r.db, number), number)
}
func TestCliqueReorgAtEpoch(t *testing.T) {
// Create the account pool and generate the initial set of signers
accounts := newDeterministicTesterAccountPool()
// 7 signers
signerlist := []string{"A", "B", "C", "D", "E", "F", "G"}
signers := make([]common.Address, len(signerlist))
for j, signer := range signerlist {
signers[j] = accounts.address(signer)
}
// Sort the signers
for j := 0; j < len(signers); j++ {
for k := j + 1; k < len(signers); k++ {
if bytes.Compare(signers[j][:], signers[k][:]) > 0 {
signers[j], signers[k] = signers[k], signers[j]
signerlist[j], signerlist[k] = signerlist[k], signerlist[j]
}
}
}
// Create the genesis block with the initial set of signers
genesis := &core.Genesis{
ExtraData: make([]byte, extraVanity+common.AddressLength*len(signers)+extraSeal),
}
for j, signer := range signers {
copy(genesis.ExtraData[extraVanity+j*common.AddressLength:], signer[:])
fmt.Printf("adding signer 0x%x\n", signer)
}
sealerListBytes := genesis.ExtraData
// Create a pristine blockchain with the genesis injected
db := ethdb.NewMemDatabase()
genesis.Commit(db)
engine := New(&params.CliqueConfig{Epoch: 5}, db)
// Initialize a fresh chain with only a genesis block
blockchain, _ := core.NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{})
snap, err := engine.snapshot(&testerChainReader{db: db}, blockchain.CurrentBlock().NumberU64(), blockchain.CurrentBlock().Hash(), nil)
for addy, _ := range snap.Signers {
fmt.Printf("signer: 0x%x\n", addy)
}
// Generate blocks, up to the checkpoint block
// This segment should with the last block being of higher difficulty: in-turn
blocks2, _ := core.GenerateChainSeal(params.TestChainConfig, blockchain.CurrentBlock(), engine, db, 6,
func(i int, b *core.BlockGen) {
b.SetDifficulty(diffNoTurn)
extraData := make([]byte, extraVanity+extraSeal)
b.SetExtra(extraData)
if i+1 == 5 {
// Checkpoint block
b.SetExtra(sealerListBytes)
b.SetDifficulty(diffInTurn)
}
},
func(b *types.Block) *types.Header {
// Seal it
hdr := b.Header()
if hdr.Number.Uint64() >= 5 {
k := 5 % len(signerlist)
sig, _ := accounts.getSignature(hdr, signerlist[k])
extradata := b.Extra()
copy(extradata[len(extradata)-65:], sig)
hdr.Extra = extradata
} else {
k := int(b.NumberU64()+5) % len(signerlist)
sig, _ := accounts.getSignature(hdr, signerlist[k])
extradata := b.Extra()
copy(extradata[len(extradata)-65:], sig)
hdr.Extra = extradata
}
return hdr
})
addBlocks := func(blocks []*types.Block) {
fmt.Printf("Importing %d blocks\n", len(blocks))
for _, block := range blocks {
author, _ := engine.Author(block.Header())
fmt.Printf(" Adding block %d: 0x%x, sealer: 0x%x, len(extra): %v, difficulty %d\n", block.Number(), block.Header().Hash(), author, len(block.Extra()), block.Difficulty())
}
if _, err := blockchain.InsertChain(blocks); err != nil {
t.Fatalf("failed to import blocks: %v", err)
}
}
showRecents := func() {
snap, err = engine.snapshot(&testerChainReader{db: db}, blockchain.CurrentBlock().NumberU64(), blockchain.CurrentBlock().Hash(), nil)
if err != nil {
t.Fatalf("failed to generate snapshot: %v", err)
}
fmt.Printf("Recents:\n")
for block, addy := range snap.Recents {
fmt.Printf("%v 0x%x\n", block, addy)
}
}
addBlocks(blocks2[0:4])
showRecents()
addBlocks(blocks2[4:5])
showRecents()
addBlocks(blocks2[5:6])
showRecents()
fmt.Printf("Current head: 0x%x\n", blockchain.CurrentBlock().Header().Hash())
for block, addy := range snap.Recents {
fmt.Printf("%v 0x%x\n", block, addy)
}
if blockchain.CurrentBlock().Number().Uint64() == 6 {
t.Error("Block should have been rejected")
}
}
// Tests that voting is evaluated correctly for various simple and complex scenarios.

View file

@ -128,6 +128,11 @@ func (b *BlockGen) AddUncle(h *types.Header) {
b.uncles = append(b.uncles, h)
}
// SetDifficulty sets difficulty of the block
func (b *BlockGen) SetDifficulty(d *big.Int) {
b.header.Difficulty = d
}
// PrevBlock returns a previously generated block by number. It panics if
// num is greater or equal to the number of the block being generated.
// For index -1, PrevBlock returns the parent block given to GenerateChain.
@ -165,6 +170,9 @@ func (b *BlockGen) OffsetTime(seconds int64) {
// values. Inserting them into BlockChain requires use of FakePow or
// a similar non-validating proof of work implementation.
func GenerateChain(config *params.ChainConfig, parent *types.Block, engine consensus.Engine, db ethdb.Database, n int, gen func(int, *BlockGen)) ([]*types.Block, []types.Receipts) {
return GenerateChainSeal(config, parent, engine, db, n, gen, nil)
}
func GenerateChainSeal(config *params.ChainConfig, parent *types.Block, engine consensus.Engine, db ethdb.Database, n int, gen func(int, *BlockGen), sealer func(block *types.Block) *types.Header) ([]*types.Block, []types.Receipts) {
if config == nil {
config = params.TestChainConfig
}
@ -205,6 +213,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
if err := statedb.Database().TrieDB().Commit(root, false); err != nil {
panic(fmt.Sprintf("trie write error: %v", err))
}
if sealer != nil {
header := sealer(block)
return types.NewBlock(header, b.txs, b.uncles, b.receipts), nil
}
return block, b.receipts
}
return nil, nil