diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 0ff72e55c6..12199d7501 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -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 { diff --git a/consensus/clique/snapshot_test.go b/consensus/clique/snapshot_test.go index 17719884f0..7d14430d4d 100644 --- a/consensus/clique/snapshot_test.go +++ b/consensus/clique/snapshot_test.go @@ -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,21 +54,52 @@ 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 { - ap.accounts[signer], _ = crypto.GenerateKey() + 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 { - ap.accounts[account], _ = crypto.GenerateKey() + 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) @@ -75,16 +111,129 @@ type testerChainReader struct { db ethdb.Database } -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) GetBlock(common.Hash, uint64) *types.Block { panic("not supported") } -func (r *testerChainReader) GetHeaderByHash(common.Hash) *types.Header { panic("not supported") } +func (r *testerChainReader) Config() *params.ChainConfig { return params.AllCliqueProtocolChanges } +func (r *testerChainReader) CurrentHeader() *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(¶ms.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. diff --git a/core/chain_makers.go b/core/chain_makers.go index de0fc6be9e..0c52fd1536 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -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