diff options
author | Péter Szilágyi <peterke@gmail.com> | 2018-09-10 22:12:39 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2018-09-11 21:40:00 +0800 |
commit | 4bb25042eb957662155079e06bb2efbdd866eb99 (patch) | |
tree | 87e6833f5c30b0bf2cec878f4689f689572c058b /consensus | |
parent | 10bac3664702f31228eafedecf31e9bf115f6c29 (diff) | |
download | dexon-4bb25042eb957662155079e06bb2efbdd866eb99.tar.gz dexon-4bb25042eb957662155079e06bb2efbdd866eb99.tar.zst dexon-4bb25042eb957662155079e06bb2efbdd866eb99.zip |
consensus/clique, core: chain maker clique + error tests
Diffstat (limited to 'consensus')
-rw-r--r-- | consensus/clique/clique.go | 49 | ||||
-rw-r--r-- | consensus/clique/snapshot.go | 16 | ||||
-rw-r--r-- | consensus/clique/snapshot_test.go | 190 |
3 files changed, 184 insertions, 71 deletions
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 0ff72e55c..2b69141c1 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -93,27 +93,33 @@ var ( // errMissingSignature is returned if a block's extra-data section doesn't seem // to contain a 65 byte secp256k1 signature. - errMissingSignature = errors.New("extra-data 65 byte suffix signature missing") + errMissingSignature = errors.New("extra-data 65 byte signature suffix missing") // errExtraSigners is returned if non-checkpoint block contain signer data in // their extra-data fields. errExtraSigners = errors.New("non-checkpoint block contains extra signer list") // errInvalidCheckpointSigners is returned if a checkpoint block contains an - // invalid list of signers (i.e. non divisible by 20 bytes, or not the correct - // ones). + // invalid list of signers (i.e. non divisible by 20 bytes). errInvalidCheckpointSigners = errors.New("invalid signer list on checkpoint block") + // errMismatchingCheckpointSigners is returned if a checkpoint block contains a + // list of signers different than the one the local node calculated. + errMismatchingCheckpointSigners = errors.New("mismatching signer list on checkpoint block") + // errInvalidMixDigest is returned if a block's mix digest is non-zero. errInvalidMixDigest = errors.New("non-zero mix digest") // errInvalidUncleHash is returned if a block contains an non-empty uncle list. errInvalidUncleHash = errors.New("non empty uncle hash") - // errInvalidDifficulty is returned if the difficulty of a block is not either - // of 1 or 2, or if the value does not match the turn of the signer. + // errInvalidDifficulty is returned if the difficulty of a block neither 1 or 2. errInvalidDifficulty = errors.New("invalid difficulty") + // errWrongDifficulty is returned if the difficulty of a block doesn't match the + // turn of the signer. + errWrongDifficulty = errors.New("wrong difficulty") + // 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") @@ -122,8 +128,12 @@ var ( // be modified via out-of-range or non-contiguous headers. errInvalidVotingChain = errors.New("invalid voting chain") - // errUnauthorized is returned if a header is signed by a non-authorized entity. - errUnauthorized = errors.New("unauthorized") + // errUnauthorizedSigner is returned if a header is signed by a non-authorized entity. + errUnauthorizedSigner = errors.New("unauthorized signer") + + // errRecentlySigned is returned if a header is signed by an authorized entity + // that already signed a header recently, thus is temporarily not allowed to. + errRecentlySigned = errors.New("recently signed") // errWaitTransactions is returned if an empty block is attempted to be sealed // on an instant chain (0 second period). It's important to refuse these as the @@ -205,6 +215,9 @@ type Clique struct { signer common.Address // Ethereum address of the signing key signFn SignerFn // Signer function to authorize hashes with lock sync.RWMutex // Protects the signer fields + + // The fields below are for testing only + fakeDiff bool // Skip difficulty verifications } // New creates a Clique proof-of-authority consensus engine with the initial @@ -359,7 +372,7 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainReader, header *type } extraSuffix := len(header.Extra) - extraSeal if !bytes.Equal(header.Extra[extraVanity:extraSuffix], signers) { - return errInvalidCheckpointSigners + return errMismatchingCheckpointSigners } } // All basic checks passed, verify the seal and return @@ -481,23 +494,25 @@ func (c *Clique) verifySeal(chain consensus.ChainReader, header *types.Header, p return err } if _, ok := snap.Signers[signer]; !ok { - return errUnauthorized + return errUnauthorizedSigner } 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 errRecentlySigned } } } // 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 - } - if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 { - return errInvalidDifficulty + if !c.fakeDiff { + inturn := snap.inturn(header.Number.Uint64(), signer) + if inturn && header.Difficulty.Cmp(diffInTurn) != 0 { + return errWrongDifficulty + } + if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 { + return errWrongDifficulty + } } return nil } @@ -613,7 +628,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c return err } if _, authorized := snap.Signers[signer]; !authorized { - return errUnauthorized + return errUnauthorizedSigner } // If we're amongst the recent signers, wait for the next block for seen, recent := range snap.Recents { diff --git a/consensus/clique/snapshot.go b/consensus/clique/snapshot.go index 2333d6924..54d4555f3 100644 --- a/consensus/clique/snapshot.go +++ b/consensus/clique/snapshot.go @@ -57,12 +57,12 @@ type Snapshot struct { Tally map[common.Address]Tally `json:"tally"` // Current vote tally to avoid recalculating } -// signers implements the sort interface to allow sorting a list of addresses -type signers []common.Address +// signersAscending implements the sort interface to allow sorting a list of addresses +type signersAscending []common.Address -func (s signers) Len() int { return len(s) } -func (s signers) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 } -func (s signers) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s signersAscending) Len() int { return len(s) } +func (s signersAscending) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 } +func (s signersAscending) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // newSnapshot creates a new snapshot with the specified startup parameters. This // method does not initialize the set of recent signers, so only ever use if for @@ -214,11 +214,11 @@ func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { return nil, err } if _, ok := snap.Signers[signer]; !ok { - return nil, errUnauthorized + return nil, errUnauthorizedSigner } for _, recent := range snap.Recents { if recent == signer { - return nil, errUnauthorized + return nil, errRecentlySigned } } snap.Recents[number] = signer @@ -298,7 +298,7 @@ func (s *Snapshot) signers() []common.Address { for sig := range s.Signers { sigs = append(sigs, sig) } - sort.Sort(signers(sigs)) + sort.Sort(signersAscending(sigs)) return sigs } diff --git a/consensus/clique/snapshot_test.go b/consensus/clique/snapshot_test.go index 17719884f..71fe7ce8b 100644 --- a/consensus/clique/snapshot_test.go +++ b/consensus/clique/snapshot_test.go @@ -19,24 +19,18 @@ package clique import ( "bytes" "crypto/ecdsa" - "math/big" + "sort" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" "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" ) -type testerVote struct { - signer 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. @@ -50,17 +44,26 @@ func newTesterAccountPool() *testerAccountPool { } } -func (ap *testerAccountPool) sign(header *types.Header, signer string) { - // Ensure we have a persistent key for the signer - if ap.accounts[signer] == nil { - ap.accounts[signer], _ = crypto.GenerateKey() +// checkpoint creates a Clique checkpoint signer section from the provided list +// of authorized signers and embeds it into the provided header. +func (ap *testerAccountPool) checkpoint(header *types.Header, signers []string) { + auths := make([]common.Address, len(signers)) + for i, signer := range signers { + auths[i] = ap.address(signer) + } + sort.Sort(signersAscending(auths)) + for i, auth := range auths { + copy(header.Extra[extraVanity+i*common.AddressLength:], auth.Bytes()) } - // Sign the header and embed the signature in extra data - sig, _ := crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer]) - copy(header.Extra[len(header.Extra)-65:], sig) } +// address retrieves the Ethereum address of a tester account by label, creating +// a new account if no previous one exists yet. func (ap *testerAccountPool) address(account string) common.Address { + // Return the zero account for non-addresses + if account == "" { + return common.Address{} + } // Ensure we have a persistent key for the account if ap.accounts[account] == nil { ap.accounts[account], _ = crypto.GenerateKey() @@ -69,32 +72,38 @@ func (ap *testerAccountPool) address(account string) common.Address { return crypto.PubkeyToAddress(ap.accounts[account].PublicKey) } -// testerChainReader implements consensus.ChainReader to access the genesis -// block. All other methods and requests will panic. -type testerChainReader struct { - db ethdb.Database +// sign calculates a Clique digital signature for the given block and embeds it +// back into the header. +func (ap *testerAccountPool) sign(header *types.Header, signer string) { + // Ensure we have a persistent key for the signer + if ap.accounts[signer] == nil { + ap.accounts[signer], _ = crypto.GenerateKey() + } + // Sign the header and embed the signature in extra data + sig, _ := crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer]) + copy(header.Extra[len(header.Extra)-extraSeal:], sig) } -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) GetHeaderByNumber(number uint64) *types.Header { - if number == 0 { - return rawdb.ReadHeader(r.db, rawdb.ReadCanonicalHash(r.db, 0), 0) - } - return nil +// testerVote represents a single block signed by a parcitular account, where +// the account may or may not have cast a Clique vote. +type testerVote struct { + signer string + voted string + auth bool + checkpoint []string + newbatch bool } -// Tests that voting is evaluated correctly for various simple and complex scenarios. -func TestVoting(t *testing.T) { +// Tests that Clique signer voting is evaluated correctly for various simple and +// complex scenarios, as well as that a few special corner cases fail correctly. +func TestClique(t *testing.T) { // Define the various voting scenarios to test tests := []struct { epoch uint64 signers []string votes []testerVote results []string + failure error }{ { // Single signer, no votes cast @@ -322,10 +331,49 @@ func TestVoting(t *testing.T) { votes: []testerVote{ {signer: "A", voted: "C", auth: true}, {signer: "B"}, - {signer: "A"}, // Checkpoint block, (don't vote here, it's validated outside of snapshots) + {signer: "A", checkpoint: []string{"A", "B"}}, {signer: "B", voted: "C", auth: true}, }, results: []string{"A", "B"}, + }, { + // An unauthorized signer should not be able to sign blocks + signers: []string{"A"}, + votes: []testerVote{ + {signer: "B"}, + }, + failure: errUnauthorizedSigner, + }, { + // An authorized signer that signed recenty should not be able to sign again + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A"}, + {signer: "A"}, + }, + failure: errRecentlySigned, + }, { + // Recent signatures should not reset on checkpoint blocks imported in a batch + epoch: 3, + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "A"}, + {signer: "B"}, + {signer: "A", checkpoint: []string{"A", "B", "C"}}, + {signer: "A"}, + }, + failure: errRecentlySigned, + }, { + // Recent signatures should not reset on checkpoint blocks imported in a new + // batch (https://github.com/ethereum/go-ethereum/issues/17593). Whilst this + // seems overly specific and weird, it was a Rinkeby consensus split. + epoch: 3, + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "A"}, + {signer: "B"}, + {signer: "A", checkpoint: []string{"A", "B", "C"}}, + {signer: "A", newbatch: true}, + }, + failure: errRecentlySigned, }, } // Run through the scenarios and test them @@ -356,28 +404,78 @@ func TestVoting(t *testing.T) { genesis.Commit(db) // 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) * 15), - Coinbase: accounts.address(vote.voted), - Extra: make([]byte, extraVanity+extraSeal), + config := *params.TestChainConfig + config.Clique = ¶ms.CliqueConfig{ + Period: 1, + Epoch: tt.epoch, + } + engine := New(config.Clique, db) + engine.fakeDiff = true + + blocks, _ := core.GenerateChain(&config, genesis.ToBlock(db), engine, db, len(tt.votes), func(j int, gen *core.BlockGen) { + // Cast the vote contained in this block + gen.SetCoinbase(accounts.address(tt.votes[j].voted)) + if tt.votes[j].auth { + var nonce types.BlockNonce + copy(nonce[:], nonceAuthVote) + gen.SetNonce(nonce) } + }) + // Iterate through the blocks and seal them individually + for j, block := range blocks { + // Geth the header and prepare it for signing + header := block.Header() if j > 0 { - headers[j].ParentHash = headers[j-1].Hash() + header.ParentHash = blocks[j-1].Hash() } - if vote.auth { - copy(headers[j].Nonce[:], nonceAuthVote) + header.Extra = make([]byte, extraVanity+extraSeal) + if auths := tt.votes[j].checkpoint; auths != nil { + header.Extra = make([]byte, extraVanity+len(auths)*common.AddressLength+extraSeal) + accounts.checkpoint(header, auths) + } + header.Difficulty = diffInTurn // Ignored, we just need a valid number + + // Generate the signature, embed it into the header and the block + accounts.sign(header, tt.votes[j].signer) + blocks[j] = block.WithSeal(header) + } + // Split the blocks up into individual import batches (cornercase testing) + batches := [][]*types.Block{nil} + for j, block := range blocks { + if tt.votes[j].newbatch { + batches = append(batches, nil) } - accounts.sign(headers[j], vote.signer) + batches[len(batches)-1] = append(batches[len(batches)-1], block) } // Pass all the headers through clique and ensure tallying succeeds - head := headers[len(headers)-1] + chain, err := core.NewBlockChain(db, nil, &config, engine, vm.Config{}) + if err != nil { + t.Errorf("test %d: failed to create test chain: %v", i, err) + continue + } + failed := false + for j := 0; j < len(batches)-1; j++ { + if k, err := chain.InsertChain(batches[j]); err != nil { + t.Errorf("test %d: failed to import batch %d, block %d: %v", i, j, k, err) + failed = true + break + } + } + if failed { + continue + } + if _, err = chain.InsertChain(batches[len(batches)-1]); err != tt.failure { + t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure) + } + if tt.failure != nil { + continue + } + // No failure was produced or requested, generate the final voting snapshot + head := blocks[len(blocks)-1] - snap, err := New(¶ms.CliqueConfig{Epoch: tt.epoch}, db).snapshot(&testerChainReader{db: db}, head.Number.Uint64(), head.Hash(), headers) + snap, err := engine.snapshot(chain, head.NumberU64(), head.Hash(), nil) if err != nil { - t.Errorf("test %d: failed to create voting snapshot: %v", i, err) + t.Errorf("test %d: failed to retrieve voting snapshot: %v", i, err) continue } // Verify the final list of signers against the expected ones |