Go-ethereum 源码解析之 consensus/clique/clique.go
// Package clique implements the proof-of-authority consensus engine.
package clique
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
inmemorySignatures = 4096 // Number of recent block signatures to keep in memory
wiggleTime = 500 * time.Millisecond // Random delay (per signer) to allow concurrent signers
)
// Clique proof-of-authority protocol constants.
var (
epochLength = uint64(30000) // Default number of blocks after which to checkpoint and reset the pending votes
extraVanity = 32 // Fixed number of extra-data prefix bytes reserved for signer vanity
extraSeal = 65 // Fixed number of extra-data suffix bytes reserved for signer seal
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") // Magic nonce number to vote on adding a new signer
nonceDropVote = hexutil.MustDecode("0x0000000000000000") // Magic nonce number to vote on removing a signer.
uncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW.
diffInTurn = big.NewInt(2) // Block difficulty for in-turn signatures
diffNoTurn = big.NewInt(1) // Block difficulty for out-of-turn signatures
)
// Various error messages to mark blocks invalid. These should be private to
// prevent engine specific errors from being referenced in the remainder of the
// codebase, inherently breaking if the engine is swapped out. Please put common
// error types into the consensus package.
var (
// errUnknownBlock is returned when the list of signers is requested for a block
// that is not part of the local blockchain.
errUnknownBlock = errors.New("unknown block")
// errInvalidCheckpointBeneficiary is returned if a checkpoint/epoch transition
// block has a beneficiary set to non-zeroes.
errInvalidCheckpointBeneficiary = errors.New("beneficiary in checkpoint block non-zero")
// 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")
// errInvalidCheckpointVote is returned if a checkpoint/epoch transition block
// has a vote nonce set to non-zeroes.
errInvalidCheckpointVote = errors.New("vote nonce in checkpoint block non-zero")
// errMissingVanity is returned if a block's extra-data section is shorter than
// 32 bytes, which is required to store the signer vanity.
errMissingVanity = errors.New("extra-data 32 byte vanity prefix missing")
// 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")
// 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).
errInvalidCheckpointSigners = errors.New("invalid 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 = errors.New("invalid 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")
// 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")
// errUnauthorized is returned if a header is signed by a non-authorized entity.
errUnauthorized = errors.New("unauthorized")
// 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
// block reward is zero, so an empty block just bloats the chain... fast.
errWaitTransactions = errors.New("waiting for transactions")
)
// SignerFn is a signer callback function to request a hash to be signed by a
// backing account.
type SignerFn func(accounts.Account, []byte) ([]byte, error)
// sigHash returns the hash which is used as input for the proof-of-authority
// 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()
rlp.Encode(hasher, []interface{}{
header.ParentHash,
header.UncleHash,
header.Coinbase,
header.Root,
header.TxHash,
header.ReceiptHash,
header.Bloom,
header.Difficulty,
header.Number,
header.GasLimit,
header.GasUsed,
header.Time,
header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short
header.MixDigest,
header.Nonce,
})
hasher.Sum(hash[:0])
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
}
// 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 {
return common.Address{}, err
}
var signer common.Address
copy(signer[:], crypto.Keccak256(pubkey[1:])[12:])
sigcache.Add(hash, signer)
return signer, nil
}
// Clique is the proof-of-authority consensus engine proposed to support the
// Ethereum testnet following the Ropsten attacks.
type Clique struct {
config *params.CliqueConfig // Consensus engine configuration parameters
db ethdb.Database // Database to store and retrieve snapshot checkpoints
recents *lru.ARCCache // Snapshots for recent block to speed up reorgs
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
proposals map[common.Address]bool // Current list of proposals we are pushing
signer common.Address // Ethereum address of the signing key
signFn SignerFn // Signer function to authorize hashes with
lock sync.RWMutex // Protects the signer fields
}
// New creates a Clique proof-of-authority consensus engine with the initial
// signers set to the ones provided by the user.
func New(config *params.CliqueConfig, db ethdb.Database) *Clique {
// Set any missing consensus parameters to their defaults
conf := *config
if conf.Epoch == 0 {
conf.Epoch = epochLength
}
// Allocate the snapshot caches and create the engine
recents, _ := lru.NewARC(inmemorySnapshots)
signatures, _ := lru.NewARC(inmemorySignatures)
return &Clique{
config: &conf,
db: db,
recents: recents,
signatures: signatures,
proposals: make(map[common.Address]bool),
}
}
// Author implements consensus.Engine, returning the Ethereum address recovered
// from the signature in the header's extra-data section.
func (c *Clique) Author(header *types.Header) (common.Address, error) {
return ecrecover(header, c.signatures)
}
// VerifyHeader checks whether a header conforms to the consensus rules.
func (c *Clique) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error {
return c.verifyHeader(chain, header, nil)
}
// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers. 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 (c *Clique) 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 := c.verifyHeader(chain, header, headers[:i])
select {
case <-abort:
return
case results <- err:
}
}
}()
return abort, results
}
// 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 (c *Clique) verifyHeader(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
if header.Number == nil {
return errUnknownBlock
}
number := header.Number.Uint64()
// Don't waste time checking blocks from the future
if header.Time.Cmp(big.NewInt(time.Now().Unix())) > 0 {
return consensus.ErrFutureBlock
}
// Checkpoint blocks need to enforce zero beneficiary
checkpoint := (number % c.config.Epoch) == 0
if checkpoint && header.Coinbase != (common.Address{}) {
return errInvalidCheckpointBeneficiary
}
// Nonces must be 0x00..0 or 0xff..f, zeroes enforced on checkpoints
if !bytes.Equal(header.Nonce[:], nonceAuthVote) && !bytes.Equal(header.Nonce[:], nonceDropVote) {
return errInvalidVote
}
if checkpoint && !bytes.Equal(header.Nonce[:], nonceDropVote) {
return errInvalidCheckpointVote
}
// Check that the extra-data contains both the vanity and signature
if len(header.Extra) < extraVanity {
return errMissingVanity
}
if len(header.Extra) < extraVanity+extraSeal {
return errMissingSignature
}
// Ensure that the extra-data contains a signer list on checkpoint, but none otherwise
signersBytes := len(header.Extra) - extraVanity - extraSeal
if !checkpoint && signersBytes != 0 {
return errExtraSigners
}
if checkpoint && signersBytes%common.AddressLength != 0 {
return errInvalidCheckpointSigners
}
// Ensure that the mix digest is zero as we don't have fork protection currently
if header.MixDigest != (common.Hash{}) {
return errInvalidMixDigest
}
// Ensure that the block doesn't contain any uncles which are meaningless in PoA
if header.UncleHash != uncleHash {
return errInvalidUncleHash
}
// Ensure that the block's difficulty is meaningful (may not be correct at this point)
if number > 0 {
if header.Difficulty == nil || (header.Difficulty.Cmp(diffInTurn) != 0 && header.Difficulty.Cmp(diffNoTurn) != 0) {
return errInvalidDifficulty
}
}
// If all checks passed, validate any special fields for hard forks
if err := misc.VerifyForkHashes(chain.Config(), header, false); err != nil {
return err
}
// All basic checks passed, verify cascading fields
return c.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 (c *Clique) 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()+c.config.Period > header.Time.Uint64() {
return ErrInvalidTimestamp
}
// Retrieve the snapshot needed to verify this header and cache it
snap, err := c.snapshot(chain, number-1, header.ParentHash, parents)
if err != nil {
return err
}
// If the block is a checkpoint block, verify the signer list
if number%c.config.Epoch == 0 {
signers := make([]byte, len(snap.Signers)*common.AddressLength)
for i, signer := range snap.signers() {
copy(signers[i*common.AddressLength:], signer[:])
}
extraSuffix := len(header.Extra) - extraSeal
if !bytes.Equal(header.Extra[extraVanity:extraSuffix], signers) {
return errInvalidCheckpointSigners
}
}
// All basic checks passed, verify the seal and return
return c.verifySeal(chain, header, parents)
}
// snapshot retrieves the authorization snapshot at a given point in time.
func (c *Clique) 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 := c.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(c.config, c.signatures, c.db, hash); err == nil {
log.Trace("Loaded voting snapshot from disk", "number", number, "hash", hash)
snap = s
break
}
}
// If we're at an checkpoint block, make a snapshot if it's known
if number == 0 || (number%c.config.Epoch == 0 && chain.GetHeaderByNumber(number-1) == nil) {
checkpoint := chain.GetHeaderByNumber(number)
if checkpoint != nil {
hash := checkpoint.Hash()
signers := make([]common.Address, (len(checkpoint.Extra)-extraVanity-extraSeal)/common.AddressLength)
for i := 0; i < len(signers); i++ {
copy(signers[i][:], checkpoint.Extra[extraVanity+i*common.AddressLength:])
}
snap = newSnapshot(c.config, c.signatures, number, hash, signers)
if err := snap.store(c.db); err != nil {
return nil, err
}
log.Info("Stored checkpoint snapshot to disk", "number", number, "hash", hash)
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
}
c.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(c.db); err != nil {
return nil, err
}
log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)
}
return snap, err
}
// VerifyUncles implements consensus.Engine, always returning an error for any
// uncles as this consensus mechanism doesn't permit uncles.
func (c *Clique) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
if len(block.Uncles()) > 0 {
return errors.New("uncles not allowed")
}
return nil
}
// VerifySeal implements consensus.Engine, checking whether the signature contained
// in the header satisfies the consensus protocol requirements.
func (c *Clique) VerifySeal(chain consensus.ChainReader, header *types.Header) error {
return c.verifySeal(chain, header, nil)
}
// verifySeal checks whether the signature contained in the header satisfies the
// consensus protocol requirements. The method accepts an optional list of parent
// headers that aren't yet part of the local blockchain to generate the snapshots
// from.
func (c *Clique) verifySeal(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 := c.snapshot(chain, number-1, header.ParentHash, parents)
if err != nil {
return err
}
// Resolve the authorization key and check against signers
signer, err := ecrecover(header, c.signatures)
if err != nil {
return err
}
if _, ok := snap.Signers[signer]; !ok {
return errUnauthorized
}
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
}
}
}
// 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
}
return nil
}
// Prepare implements consensus.Engine, preparing all the consensus fields of the
// header for running the transactions on top.
func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header) error {
// If the block isn't a checkpoint, cast a random vote (good enough for now)
header.Coinbase = common.Address{}
header.Nonce = types.BlockNonce{}
number := header.Number.Uint64()
// Assemble the voting snapshot to check which votes make sense
snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
if err != nil {
return err
}
if number%c.config.Epoch != 0 {
c.lock.RLock()
// Gather all the proposals that make sense voting on
addresses := make([]common.Address, 0, len(c.proposals))
for address, authorize := range c.proposals {
if snap.validVote(address, authorize) {
addresses = append(addresses, address)
}
}
// If there's pending proposals, cast a vote on them
if len(addresses) > 0 {
header.Coinbase = addresses[rand.Intn(len(addresses))]
if c.proposals[header.Coinbase] {
copy(header.Nonce[:], nonceAuthVote)
} else {
copy(header.Nonce[:], nonceDropVote)
}
}
c.lock.RUnlock()
}
// Set the correct difficulty
header.Difficulty = CalcDifficulty(snap, c.signer)
// Ensure the extra data has all it's components
if len(header.Extra) < extraVanity {
header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, extraVanity-len(header.Extra))...)
}
header.Extra = header.Extra[:extraVanity]
if number%c.config.Epoch == 0 {
for _, signer := range snap.signers() {
header.Extra = append(header.Extra, signer[:]...)
}
}
header.Extra = append(header.Extra, make([]byte, extraSeal)...)
// Mix digest is reserved for now, set to empty
header.MixDigest = common.Hash{}
// Ensure the timestamp has the correct delay
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(c.config.Period))
if header.Time.Int64() < time.Now().Unix() {
header.Time = big.NewInt(time.Now().Unix())
}
return nil
}
// Finalize implements consensus.Engine, ensuring no uncles are set, nor block
// rewards given, and returns the final block.
func (c *Clique) 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 PoA, so the state remains as is and uncles are dropped
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
header.UncleHash = types.CalcUncleHash(nil)
// Assemble and return the final block for sealing
return types.NewBlock(header, txs, nil, receipts), nil
}
// Authorize injects a private key into the consensus engine to mint new blocks
// with.
func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
c.lock.Lock()
defer c.lock.Unlock()
c.signer = signer
c.signFn = signFn
}
// Seal implements consensus.Engine, attempting to create a sealed block using
// the local signing credentials.
func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
header := block.Header()
// Sealing the genesis block is not supported
number := header.Number.Uint64()
if number == 0 {
return errUnknownBlock
}
// For 0-period chains, refuse to seal empty blocks (no reward but would spin sealing)
if c.config.Period == 0 && len(block.Transactions()) == 0 {
return errWaitTransactions
}
// Don't hold the signer fields for the entire sealing procedure
c.lock.RLock()
signer, signFn := c.signer, c.signFn
c.lock.RUnlock()
// Bail out if we're unauthorized to sign a block
snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
if err != nil {
return err
}
if _, authorized := snap.Signers[signer]; !authorized {
return errUnauthorized
}
// If we're amongst the recent signers, wait for the next block
for seen, recent := range snap.Recents {
if recent == signer {
// Signer is among recents, only wait if the current block doesn't shift it out
if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {
log.Info("Signed recently, must wait for others")
return nil
}
}
}
// Sweet, the protocol permits us to sign the block, wait for our time
delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now()) // nolint: gosimple
if header.Difficulty.Cmp(diffNoTurn) == 0 {
// It's not our turn explicitly to sign, delay it a bit
wiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTime
delay += time.Duration(rand.Int63n(int64(wiggle)))
log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
}
// Sign all the things!
sighash, err := signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())
if err != nil {
return err
}
copy(header.Extra[len(header.Extra)-extraSeal:], sighash)
// Wait until sealing is terminated or delay timeout.
log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay))
go func() {
select {
case <-stop:
return
case <-time.After(delay):
}
select {
case results <- block.WithSeal(header):
default:
log.Warn("Sealing result is not read by miner", "sealhash", c.SealHash(header))
}
}()
return 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 (c *Clique) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
snap, err := c.snapshot(chain, parent.Number.Uint64(), parent.Hash(), nil)
if err != nil {
return nil
}
return CalcDifficulty(snap, c.signer)
}
// 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 CalcDifficulty(snap *Snapshot, signer common.Address) *big.Int {
if snap.inturn(snap.Number+1, signer) {
return new(big.Int).Set(diffInTurn)
}
return new(big.Int).Set(diffNoTurn)
}
// SealHash returns the hash of a block prior to it being sealed.
func (c *Clique) SealHash(header *types.Header) common.Hash {
return sigHash(header)
}
// Close implements consensus.Engine. It's a noop for clique as there is are no background threads.
func (c *Clique) Close() error {
return nil
}
// APIs implements consensus.Engine, returning the user facing RPC API to allow
// controlling the signer voting.
func (c *Clique) APIs(chain consensus.ChainReader) []rpc.API {
return []rpc.API{{
Namespace: "clique",
Version: "1.0",
Service: &API{chain: chain, clique: c},
Public: false,
}}
}
Appendix A. 总体批注
包 clique 实现了 proof-of-authority 共识引擎。
文件 consensus/clique/clique.go 主要实现了 Clique 共识引擎。同时,定义了一些 Clique 共识引擎特定的常量与错误消息,以及与签名相关的几个辅助函数。
对于 Clique,要特别注意区块或区块头是如何在其各方法中进行流转的,即对于一个原始区块,是如何最终确定共识协议相关的各字段的,并挖出带有签名的最终区块,等待网络中的其它节点确认。如:
- 方法 Prepare() 对于给定的区块头 header,逐一计算其 PoA 共识协议特定的字段,主要是 Coinbase、Nonce、Extra 三部分、Difficulty、Time 等,用于表示投票区块、检查点区块、区块签名等。
- 方法 Finalize() 根据 PoA 共识协议最后一次修正区块头中的字段 Root 和 UncleHash,这两个字段在方法 Prepare() 中没有被设置。同时,将区块头 header,事务列表 txs,收据列表 receipts 这些最重要的信息组装进区块中,并将组装后的区块进行签名,从而产生本地节点挖出来的新区块,等待网络中其它节点进行确认。
- 方法 Seal() 给方法 Finalize() 组装的区块添加签名,并将签名后的区块发送给结果通道 results,用于驱动本地节点挖出新区块后所应进行的后续流程。
- 方法 snapshot() 用于从缓存或数据库中加载某个区块编号对应的快照。快照有两种:一种检查点快照,另一种是投票快照。检查点快照用于确定授权签名者列表,并在下一个检查点快照之前对所有区块中的投票进行处理,从而更新下一个检查点快照。投票快照用于部分应用投票。
Appendix B. 启动的协程
1. 匿名协程
- 方法 VerifyHeaders() 中启动了一个独立的匿名协程,用于验证区块头列表并将验证结果发送到结果通道中,同时从退出通道监听中止消息以决定是否提前退出协程。
- 方法 Seal() 中启动了一个独立的匿名协程,用于在延迟特定的细微时间之后将带有签名的区块发送给结果通道,用于表明本地节点已经挖出了一个新区块。
Appendix C. 详细批注
1. const
checkpointInterval = 1024: 每隔多少个区块之后将投票快照保存到数据库。
inmemorySnapshots = 128: 内存中将保留的最近投票快照数量。即缓存多少个 clique.Snapshot。
inmemorySignatures = 4096: 内存中将保留的最近区块签名者的数量。即缓存多少个 common.Address。
wiggleTime = 500 * time.Millisecond: 每个签名者各自延迟随机数量的时间,以允许签名者可以并发签名。
2. var
定义了 Clique proof-of-authority 协议常量。
epochLength = uint64(30000): 每隔多少个区块设置一个检查点,并重置所有待处理的投票。
extraVanity = 32: types.Header 字段 Extra 的前 32 个字节。表示以太坊协议可以授受 32 个字节的特定信息,这一点上没有破坏 PoW 共识引擎?
extraSeal = 65: types.Header 字段 Extra 的最后 65 个字节。表示签名者对区块签名哈希值的签名信息。
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff"): 魔数,表示投票新增一个签名者。
nonceDropVote = hexutil.MustDecode("0x0000000000000000"): 魔数,表示投票解除一个签名者。types.Header 的字段 Nonce 默认为魔数 nonceDropVote。
uncleHash = types.CalcUncleHash(nil): 总是返回哈希值 Keccak256(RLP([])),因为叔区块在 PoW 共识协议之外没有任何意义。
diffInTurn = big.NewInt(2): 魔数,in-turn 签名时的区块难度值。即根据 snapshot.go 中对于 in-turn 签名的规则,在签名为 in-turn 时,type.Headers 的字段 Difficulty 应该为魔数 diffInTurn。默认为 diffNoTurn?
diffNoTurn = big.NewInt(1): 魔数,非 in-turn(或 out-of-turn)签名时的区块难度值。在签名为 out-of-turn 时,type.Headers 的字段 Difficulty 应该为魔数 diffNoTurn。
3. var
定义了各种错误消息以表示区块是无效的。这些错误消息应该是私有的,以防止在代码库的其余部分引用与特定共识引擎相关的错误消息,如果共识引擎被替换掉则会从本质上破坏代码库。请将常见的错误类型放入包 consensus 中,即文件 consensus/error.go 中。
- errUnknownBlock = errors.New("unknown block"): 当为不属于本地区块链的区块请求签名者列表时,将返回 errUnknownBlock。
- errInvalidCheckpointBeneficiary = errors.New("beneficiary in checkpoint block non-zero"): 如果处理检查点位置的区块的矿工地址不为 0x0,即 types.Header 的字段 Coinbase 不为 0x0,将返回 errInvalidCheckpointBeneficiary。
- errInvalidVote = errors.New("vote nonce not 0x00..0 or 0xff..f"): 当 types.Header 中的字段 Nonce 的值不是魔数 nonceAuthVote 和 nonceDropVote 中的一个时,将返回 errInvalidVote。
- errInvalidCheckpointVote = errors.New("vote nonce in checkpoint block non-zero"): 对处理检查点的区块头来说,如果 types.Header 中的字段 Nonce 不是 nonceDropVote 时,将返回 errInvalidCheckpointVote。
- errMissingVanity = errors.New("extra-data 32 byte vanity prefix missing"): 当 types.Header 中的字段 Extra 的字节数小于 extraVanity (32) 时,将返回 errMissingVanity。
- errMissingSignature = errors.New("extra-data 65 byte suffix signature missing"): 当 types.Header 中的字段 Extra 的字节数小于 extraVanity + extraSeal (32 + 65 = 97) 时,即无法包含 65 个字节的 secp256k1 签名信息。
- errExtraSigners = errors.New("non-checkpoint block contains extra signer list"): 对于处于非检查点位置的区块来说,如果其 types.Header 中的字段 Extra 中包含了签名信息,将返回 errExtraSigners。
- errInvalidCheckpointSigners = errors.New("invalid signer list on checkpoint block"): 对于处于检查点位置的区块来说,如果其 types.Header 字段中的 Extra 字段包含的签名者列表是无效的(如 types.Header.Extra[extraVanity: len(types.Header.Extra) - extraSeal] 的字节数不是 20 的倍数,或者这些字节与 clique.Snapshot 中的方法 signers() 返回的有序签名者列表不一致),将返回 errInvalidCheckpointSigners。
- 注意:根据 Clique.verifyCascadingFields() 返回 errInvalidCheckpointSigners 的代码可知,types.Header 的字段 Extra 中包含的授权签名者列表必须是升序存储的。这是 Clique 共识引擎的协议规则。
- errInvalidMixDigest = errors.New("non-zero mix digest"): 如果区块头 types.Header 的字段 MixDigest 不为 common.Hash{} 的话,将返回 errInvalidMixDigest。
- errInvalidUncleHash = errors.New("non empty uncle hash"): 如果区块头 types.Header 的字段 UncleHash 不为 uncleHash 时,将返回 errInvalidUncleHash。
- 在 PoA 共识协议里,叔区块列表为空。
- errInvalidDifficulty = errors.New("invalid difficulty"): 如果区块的难度,即 types.Header 的字段 Difficulty 既不是 diffInTurn 也不是 diffNoTurn 时,或者当签名者是 in-turn 签名但 Difficulty 不为 diffInTurn,或者当签名者是 no-turn 签名但 Difficulty 不为 diffNoTurn,将返回 errInvalidDifficulty。
- in-turn 就是签名者签名的区块正好是该签名者的轮次,否则为 no-turn 签名
- in-turn 和 no-turn 的具体解释可以参考 consensus/consensus.go
- ErrInvalidTimestamp = errors.New("invalid timestamp"): 如果区块被打包的时间戳,即 types.Header 的字段 Time,小于父区块的打包时间戳加上最小出块周期,将返回 ErrInvalidTimestamp。
- 即区块的出块频率不然过快。
- errInvalidVotingChain = errors.New("invalid voting chain"): 当授权签名者列表尝试修改不连续的区块头列表,或者超出范围的区块头列表时,将返回 errInvalidVotingChain。
- 用于确保区块从编号 0 开始,不断连续地被签名。确保区块链的完整性和连续性。
- errUnauthorized = errors.New("unauthorized"): 如果区块头由一个不在授权签名者列表中的账户进行签名,或者授权签名者在最近连续 K/2 + 1 个区块内签名超过 1 次时,将返回 errUnauthorized。
- 用于在签名者的签名不符合 PoA 共识协议时返回错误消息。
- 这个错误消息只在验证签名,或应用签名时,才会出现。而在验证区块头时,可不详细验证具体的签名信息。当然,这和具体的实现有关。目前实现中,检验签名信息是否符合 PoA 共识协议,主要在这些方法中:Clique.VerifySeal(), Clique.Seal(), Snapshot.apply()
- 方法 Clique.VerifySeal() 应该是本地节点验证网络中其它节点打包出的区块的签名信息是否有效。
- 方法 Clique.Seal() 是本地节点对最终将要打包的区块的区块头进行签名,签名之后区块 types.Block 相关的整体结构就被确认下来,不然再进行修改了。
- 方法 Snapshot.apply() 将根据各区块中包含的投票来实际修改授权签名者列表,并对各区块的签名信息进行一次检验。
- errWaitTransactions = errors.New("waiting for transactions"): 如果尝试在即时链(0 秒周期)上签名空块(即区块中不包含任何事务),将返回 errWaitTransactions。拒绝这些是非常重要的,因为区块奖励为零,所以一个空块只会使区块链快速膨胀。
4. type SignerFn func(accounts.Account, []byte) ([]byte, error)
SignerFn 是具体签名函数的抽象,最终描述一个具体的签名函数。签名函数返回账户对于哈希值的签名信息。这里的哈希为 types.Header 中去掉字段 Extra 最后 65 个字节的哈希值。
文件 go-ethereum/accounts/accounts.go 中包含了具体的签名函数 accounts.SignHash(),这个函数为 Clique 共识引擎所采用的具体的签名算法。
文件:go-ethereum/consensus/clique/clique.go
func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
c.lock.Lock()
defer c.lock.Unlock()
c.signer = signer
c.signFn = signFn
}
文件:go-ethereum/eth/backend.go
func (s *Ethereum) StartMining(threads int) error {
...
clique.Authorize(eb, wallet.SignHash)
...
}
5. func sigHash(header *types.Header) (hash common.Hash)
方法 sigHash() 返回给定区块头的哈希值,该哈希值用作权限证明签名的输入。除了包含在区块头 types.Header 中的字段 Extra 末尾的 65 字节签名之外,它是整个区块头的散列。
注意,该方法要求额外数据至少为 65 字节,否则会引起 panic。这样做是为了避免意外地使用两种形式(签名存在与否),这些形式可能被滥用以产生相同区块头的不同散列。
对于区块头存在两种比较重要的散列:
- 整个区块头的散列,也称作区块散列。
- 不包含区块头额外数据最后 65 字节的签名散列。说是签名散列,其实更应该说是给签名用的散列。
上述想要避免的应该是,虽然额外数据是否包含 65 字节对签名散列没有任何差别,但对应的区块散列却是完全不同的。
主要的实现细节如下:
- 通过方法 sha3.NewKeccak256() 获得哈希器 keccak256。
- 该哈希器实现了 hash.Hash 接口。
- 该哈希器继承了 io.Writer 接口。
- 通过方法 rlp.Encode() 将签名散列对应的区块头中的数据编码为 RLP 二进制流存入哈希器 keccak256 中。
- 调用哈希器的方法 Sum() 返回最终的签名散列。
6. func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error)
函数 ecrecover() 从已经签名的区块头中提取以太坊账户地址。
可以简单地把参数 sigcache 理解为 mapping[common.Hash]common.Address 结构。用于缓存账户地址对某个区块的签名,这里某个区块用其区块哈希值惟一标识。
主要的实现细节如下:
- 先从缓存 sigcache 中查找对应区块哈希的签名者,即以太坊账户地址。
- 从区块头额外数据中提取签名,失败时返回 errMissingSignature。
- 通过函数 crypto.Ecrecover() 从区块签名哈希和签名信息恢复出公钥。
- 通过函数 crypto.Keccak256() 对公钥进行二次散列,并将散列的前 20 个字节作为账户地址。
- 先缓存账户地址到参数 sigcache 中,再返回账户地址。
7. type Clique struct
Clique 是在发生 Ropsten 攻击之后被提议支持以太坊测试网络的权威证明共识引擎。
config *params.CliqueConfig: Clique 共识引擎特定的配置参数,主要是参数 epoch 和 period。
db ethdb.Database: 存储和查询检查点快照的数据库。底层数据库 LevelDB 与以太坊之间的接口层。
recents *lru.ARCCache: 最近区块的快照可以加快重组速度。内存中缓存的最近 inmemorySnapshots 个快照,可简单理解为 map[hash(clique.Snapshot)]clique.Snapshot。
signatures *lru.ARCCache: 最近的区块的签名,以加快挖矿。内存中缓存的最近 inmemorySignatures 个签名者,可简单理解为 map[types.Header.Hash()]common.Address
proposals map[common.Address]bool: 我们正在推动的当前提案列表。这是的提案是指投票者本身投出的是授权投票还是解除授权投票,而不是被投票者得到的是授权投票还是被授权投票。这也符合 map 结构,因为被投票者是可以获得多张投票的,而投票者则只能投出一张投票。当前提案列表应该是缓存的意思,而且是最新的,Clique 共识引擎可以直接拿来用于判定某个投票者的投票内容。
signer common.Address: 签名密钥的以太网地址。??? 是指本地节点的签名者吗?即本地节点的矿工?
signFn SignerFn: 签名者函数用于授权哈希。即矿工采用的具体签名函数,其中密钥为矿工地址,被签名信息为区块签名哈希。文件 go-ethereum/accounts/accounts.go 中包含了具体的签名函数 accounts.SignHash(),这个函数为 Clique 共识引擎所采用的具体的签名算法。
lock sync.RWMutex: 用于保护字段 signer 的锁。
(1) func New(config *params.CliqueConfig, db ethdb.Database) *Clique
构造函数 New() 创建子 Clique 权威证明共识引擎。
主要的实现细节:
- 设置配置参数中未设置的参数。
- 设置缓存字段 recents 的大小 inmemorySnapshots。
- 设置缓存字段 signatures 的大小 inmemorySignatures。
- 需要注意的是,字段 signer 和 signFn 在初始化时并未设置,而是由方法 Authorize() 明确设置。
(2) func (c *Clique) Author(header *types.Header) (common.Address, error)
方法 Author() 实现了接口 consensus.Engine,返回区块的签名者地址。
主要的实现细节:
- 通过函数 ecrecover() 返回从区块头额外数据部分中的签名中恢复的以太坊地址,即签名者地址。
(3) func (c *Clique) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error
方法 VerifyHeader() 检查在指定的区块链中区块头是否符合共识引擎。
主要的实现细节:
- 参数 seal 并没有被用到
- 具体实现转发给同名方法 VerifyHeader
(4) func (c Clique) VerifyHeaders(chain consensus.ChainReader, headers []types.Header, seals []bool) (chan<- struct{}, <-chan error)
方法 VerifyHeaders() 与方法 VerifyHeader() 类似,但验证了一批区块头。该方法返回退出通道以使调用者能够中止此方法中的操作,并返回结果通道以检索异步验证(顺序是输入切片的顺序)。
主要实现细节如下:
- 参数 seals 并没有得到使用。
- 创建了退出通道 abort,并将其作为返回结果返回给调用者。这是个消息接收通道,表示该通道的消息流是由外向内的,即只能由调用者发送消息给此方法。
- 创建了结果通道 results,并将其作为返回结构返回给调用者。这是个消息发送通道,表示该通道的消息流是由内向外的,邓只能由此方法发送消息给调用者。
- 启动一个独立的匿名协程
- 调用方法 verifyHeader() 依次验证区块头中的区块头
- 持续监听退出通道 abort,如果接收到退出消息,则中止后续的验证操作。
- 将方法 verifyHeader() 对于某个区块头的验证结果发送给结果通道 results。
- 立即返回退出通道 abort 和结果通道 results。
对于返回结果的两个通道需要特别说明:
chan<- struct{} 表明只用于接收消息的通道,且消息体为空的消息,往往只是用来驱动一个事件,如:开始操作、中止操作等。
<-chan error 表明只用于发送消息的通道。
方法 VerifyHeaders() 的两个调用者:
- BlockChain.insertChain
- HeaderChain.ValidateHeaderChain
(5) func (c *Clique) verifyHeader(chain consensus.ChainReader, header types.Header, parents []types.Header) error
方法 VerifyHeader() 检查在指定的区块链中区块头是否符合共识引擎。调用者可以选择传入一批父区块(升序),以避免从数据库中查找这些区块。这对于同时验证一批新区块头非常有用。
主要的实现细节:
- 获取区块编号 number,如果失败返回 errUnknownBlock
- 如果区块的时间大于当前时间,则返回 consensus.ErrFutureBlock
- 重点检查区块头 types.Header 中的各字段是否满足 PoA 共识协议,如果不满足则返回对应的错误消息。
- 对于处于检查点位置的区块,字段 Coinbase 应该为 common.Address{},否则返回 errInvalidCheckpointBeneficiary
- 字段 Nonce 是否为 nonceAuthVote 或 nonceDropVote,否则返回 errInvalidVote
- 对于处于检查点位置的区块,字段 Nonce 必须为 nonceDropVote,否则返回 errInvalidCheckpointVote
- 字段 Extra 的长度是否大于等于 extraVanity,否则返回 errMissingVanity
- 字段 Extra 的长度是否大于等于 extraVanity+extraSeal,否则返回 errMissingSignature
- 对于处于非检查点位置的区块,字段 Extra 中不应该包含授权签名者列表,即字段 Extra 的字节数为 extraVanity+extraSeal。否则返回 errExtraSigners
- 对于处于检查点位置的区块,字段 Extra 中包含的授权签名者列表必须不为空,且授权签名者列表占据的字节数为 common.AddressLength 的倍数。否则返回 errInvalidCheckpointSigners
- 字段 MixDigest 必须为 common.Hash{},因为当前并没有 fork 保护。否则返回 errInvalidMixDigest
- 字段 UncleHash 必须为 uncleHash,因为在 PoA 共识中叔区块没有意义。否则返回 errInvalidUncleHash
- 对于非创始区块,字段 Difficulty 必须是 diffInTurn 或 diffNoTurn,虽然在目前阶段可能并不正确。否则返回 errInvalidDifficulty。
- 如果上述所有的检查都通过,则检查硬分叉相关的特殊字段。返回函数 misc.VerifyForkHashes() 的错误结果。
- 如果上述所有的基础检查都通过,则检查级联字段。返回方法 verifyCascadingFields() 的错误结果。
(6) func (c *Clique) verifyCascadingFields(chain consensus.ChainReader, header types.Header, parents []types.Header) error
方法 verifyCascadingFields() 验证所有非独立的区块头字段,而不是依赖于一批先前的区块头。调用者可选地传递一批父区块(升序)以避免从数据库中查找这些父区块。这对于同时验证一批新区块头非常有用。
主要的实现细节:
- 获取区块编号 number,如果是创世区块则直接返回 nil。
- 获取父区块头 parent
- 如果参数 parents 不为空,则 parent = parents[len(parents)-1]
- 否则, parent = chain.GetHeader(header.ParentHash, number-1)
- 对父区块头进行检查,如果检查失败返回 consensus.ErrUnknownAncestor。检查的三个条件为:
- 父区块头 parent 不为空
- 父区块的编号为当前区块编号减 1
- 父区块 parnet 的哈希和当前区块存储的父区块哈希一致。
- 对父区块的出块时间和当前区块的出块时间进行判定,两者的间隔不能小于出块的周期 CliqueConfig.Period
- 检索验证此区块头所需的快照并对其进行缓存。通过调用 方法Clique.snapshot() 获取或构建快照 snap。需要注意的是,这里的快照 snap 是基于当前区块上一个区块,因为对于当前区块的签名等验证时,其授权签名者列表是基于上一个区块时的快照的。
- 对于处于检查点位置的区块,验证签名者列表。通过方法 snap.signers() 返回快照中的有序签名者列表,并拼接成字节序列,和区块头 types.Header 中的字段 Extra 中的签名者列表部分字节进行比较,如果不一致,则返回 errInvalidCheckpointSigners
- 如果上述所有的基本检查都通过了,则返回方法 Clique.verifySeal() 的验证结果。方法 Clique.verifySeal() 主要是验证 PoA 共识协议中关于签名规则的那一部分。
(7) func (c Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []types.Header) (*Snapshot, error)
方法 snapshot() 在给定时间点检索授权快照,如果内存缓存和数据库中都没有对应区块点的快照,则基于该区块点构建新的快照。
主要实现细节:
- 参数 number 为区块编号
- 参数 hash 为区块哈希
- 参数 parents 为缓存的父区块列表。需要注意的时 parents[len(parents)-1] 的区块编号和哈希分别是 number 和 hash。这是由调用者传递的参数决定的。
- 定义要传递给方法 Snapshot.apply() 的区块头列表 headers, headers []*types.Header
- 定义要返回的快照 snap,snap *Snapshot
- 如果 snap == nil,则循环执行下列步骤:
-
- 在内存缓存中检索对应区块点的快照。snap = c.recents.Get(hash)
-
- 在数据库中检索对应区块点的快照,这是因为每隔 checkpointInterval 个区块会把快照归档到数据库。snap = loadSnapshot(c.config, c.signatures, c.db, hash)
-
- 对于处于检查点的区块,并且该区块的父区块存在(表示该区块基本在区块链上),先从当前区块中收集签名者列表,再作为参数调用函数 newSnapshot() 创建一个新的快照 snap,并将快照 snap 归档到数据库。但是这一步保存的快照为检查点快照,里面没有任何投票信息。这一步会输出一个非常重要的日志信息,归档了检查点快照, log.Info("Stored checkpoint snapshot to disk", "number", number, "hash", hash)。
-
- 如果对于当前区块不存在快照,则首先在参数 parents 中查找父区块头 header,并对参数 hash 和 number 进行强制检查,检查失败返回 consensus.ErrUnknownAncestor。如果参数 parents 不存在,则到数据库去查找,如果没找到,也返回 consensus.ErrUnknownAncestor。
-
- 并父区块头加入 headers 中,之后调用 Snapshot.apply(headers) 方法,将这些对应区块列表还没有快照的创建对应的快照。
-
- 更新 number 和 hash,进行下一轮循环迭代。
-
- 找到上一个快照,在其上通过方法 apply(headers) 应用任何待处理的区块头。这里需要对前面计算出的 headers 做一个处理,因为前面的 headers 是根据区块编号倒序排列的,而方法 apply(headers) 需要 headers 中的区块编号为升序的。
- 将新创建的快照加入缓存 recents 中,c.recents.Add(snap.Hash, snap)
- 如果我们创建了新的检查点快照,并且正好也满足归档的频率 checkpointInterval,则把此快照存入数据库。这里需要注意,这一步存储的快照是记录投票信息的,因此得避免基于创世区块检查点的快照,因为创世区块检查点的快照没有任何投票记录。 这一步会生成非常重要的日志信息,归档了投票快照,log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)
??? 问题:这里面有两种类型的快照,一种是处于检查点位置但不包含任何投票记录的快照,一种是处理检查点位置且包含这一轮投票记录的快照。两者频率各为 c.config.Epoch 和 checkpointInterval,还是前者的频率为 c.config.Epoch,后者的频率为 checkpointInterval * c.config.Epoch,还是说每个区块都会有快照?
结果应该是:两者频率各为 c.config.Epoch 和 checkpointInterval。每隔间隔 c.config.Epoch 个区块创建新的快照,用于基于此检查点的快照验证授权签名者列表。记录检查点快照日志 log.Info("Stored checkpoint snapshot to disk", "number", number, "hash", hash)。然后每隔一定数量的区块应用一次当前区块到上一次检查点快照或上一次投票快照之间所有区块的投票,生成新的投票快照点。这个数量具体是多少?然后投票快照基于的区块正好间隔 checkpointInterval 个区块,则归档,并记录日志 log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)。
对于创世区块,它只有检查点快照,不会有投票记录快照,因为之前没有任何投票记录。
也就是说后续的区块验证只从上一次检查点的区块和快照开始,并不需要依次验证到创世区块,只需要验证到上一个检查点区块即可。然后会有间隔地在上一次检查点到当前某一个区块,将此之间的所有区块的投票结果实际上产生实际的作用,用来更新授权签名者列表。
??? 问题,现在不知道在上一次检查点到当前区块之间应该是间隔多少个区块,进行一次投票生效。每隔 K/2 + 1 个区块? K 是上一个检查点区块中的授权签名者列表的长度。肯定不是每个区块产生一个快照。而且,可以每 c.config.Epoch 个区块新建一个检查点区块并进行归档。在检查点区块之后,每隔多少个区块头产生一次投票生效点快照?而且如果投票生效点快照正好是 checkpointInterval 的倍数,会将投票生效点快照进行归档。
(8) func (c *Clique) VerifyUncles(chain consensus.ChainReader, block *types.Block) error
方法 VerifyUncles() 实现了接口 consensus.Engine,总是为任何叔区块返回错误,因为这种共识机制不允许有叔区块。
主要实现细节:
- 判断 block.Uncles 的长度是否大于 0.
(9) func (c *Clique) VerifySeal(chain consensus.ChainReader, header *types.Header) error
方法 VerifySeal() 实现了接口 consensus.Engine,检查包含在区块头中的签名是否符合 PoA 共识协议的需求。
主要的实现细节:
- 将具体实现转发给方法 Clique.verifySeal()
方法 VerifySeal() 主要是用于验证区块头中关于签名的那部分信息,区块头中的其它信息由 VerifyHeader() 方法族实现。
(10) func (c *Clique) verifySeal(chain consensus.ChainReader, header types.Header, parents []types.Header) error
方法 verifySeal() 检查包含在区块头中的签名是否符合 PoA 共识协议的需求。此方法接受可选的父区块头列表,这些父区块头列表还不是本地区块链的一部分,快照就是从这些父区块头列表中生成。
主要的实现细节:
- 获取区块编号 number,如果是创世区块,则返回 errUnknownBlock。因为不支持验证创世区块的签名,虽然创世区块中包含最初的授权签名者列表。
- 检索验证此区块头所需的快照 snap,并缓存快照。主要实现代码为 snap = c.snapshot(chain, number-1, header.ParentHash, parents)
- 恢复出签名者 signer,主要实现代码为 signer = ecrecover(header, c.signatures)
- 如果 snap.Signers[signer] 不存在,则返回 errUnauthorized。
- 检查 snap.Recents,如果 signer 在 snap.Recents 中的最近 K/2 + 1 个区块内进行过签名,则返回 errUnauthorized。
- 如果签名为 inturn,检查对应的难度是否为 diffInTurn,如果不是则返回 errInvalidDifficulty。
- 如果签名不为 inturn,检查对应的难度是否为 diffNoTurn,如果不是则返回 errInvalidDifficulty。
(11) func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header) error
方法 Prepare() 实现了接口 consensus.Engine,为在区块头上运行事务,准备其所需的共识字段。
主要实现细节如下:
- 如果非检查点区块,做一随机投票,或者说默认投票(目前为止没有问题)。代码为:
- header.Coinbase = common.Address{}
- header.Nonce = types.BlockNonce{}
- 获取区块编号 number
- 组装投票快照以检查哪些投票能够生效,即获取父区块那个位置的快照 snap
- 对于非检查点区块,尝试进行提议
- 加锁,代码为 c.lock.RLock()
- 从提议列表中随机获取一个提议,非有效提议已经被方法 snap.validVote() 进行了过滤。用该提议的账户地址填充 header.Coinbase。同时,根据该提议是对账户地址进行授权投票还是解除授权投票,分别填充 header.Nonce 为 nonceAuthVote 和 nonceDropVote。
- 解锁,代码为 c.lock.RUnlock()
- 设置正确的区块难度,代码为 header.Difficulty = CalcDifficulty(snap, c.signer)。具体的难度计算规则参考方法 CalcDifficulty()
- 确保字段 Extra 的三部分都得到正确填充
- header.Extra[:extraVanity] 这部分根据需要补充为 extraVanity 个字节
- 如果是检查点区块,则将升序的授权签名者列表(通过方法 snap.signers() 获得)添加到 header.Extra[extraVanity] 之后
- 在授权签名者列表信息之后补充 extraSeal 个字节的默认值,用于之后修正为签名。
- 字段 MixDigest 目前为保留的,设置为空。代码为 header.MixDigest = common.Hash{}
- 确保当前区块和父区块之间有正确的时间延迟,即设置正确的字段 header.Time。
- 先获取父区块头 parent,如果为 nil 则返回 consensus.ErrUnknownAncestor。代码为 parent := chain.GetHeader(header.ParentHash, number-1);
if parent == nil {
return consensus.ErrUnknownAncestor
} - 调整 header.Time 和父区块之间有正确的时间延迟。代码为 header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(c.config.Period));
if header.Time.Int64() < time.Now().Unix() {
header.Time = big.NewInt(time.Now().Unix())
}
- 先获取父区块头 parent,如果为 nil 则返回 consensus.ErrUnknownAncestor。代码为 parent := chain.GetHeader(header.ParentHash, number-1);
(12) func (c Clique) Finalize(chain consensus.ChainReader, header types.Header, state state.StateDB, txs []types.Transaction, uncles []types.Header, receipts []types.Receipt) (*types.Block, error)
方法 Finalize() 实现了接口 consensus.Engine,确保没有叔区块被设置,没有区块奖励被设置,并返回最终的区块。这个区块的三棵树(状态树、事务树、收据树)都已经被确定下来了,但是区块头中关于事务树、收据树的根哈希应该还没有被确定下来,特别是还没有进行签名,签名信息应该是在方法 Clique.Seal() 方法之后才被确定下来的。同时叔区块列表也被确定为空,且区块头中对应的叔区块哈希也被确定。
主要实现细节:
- 确定状态树的根哈希 header.Root,代码为 header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
- 确定叔区块列表的哈希header.UncleHash,代码为 header.UncleHash = types.CalcUncleHash(nil)
- 组装并返回最终可以被签名的区块,代码为 return types.NewBlock(header, txs, nil, receipts)
(13) func (c *Clique) Authorize(signer common.Address, signFn SignerFn)
方法 Authorize() 设置共识引擎的签名者和签名函数。
主要实现细节:
- 加锁,代码为 c.lock.Lock()
- 方法结束时解锁,代码为 defer c.lock.Unlock()
- 设置共识引擎的签名者,代码为 c.signer = signer
- 设置共识引擎的签名函数,代码为 c.signFn = signFn
(14) func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error
方法 Seal() 实现了接口 consensus.Engine,尝试使用本地签名标准创建一个被签名的区块。
对于参数中指定的待签名区块 block,尝试对其进行签名,如果签名成功则将签名后的区块发送到参数结果通道 results,结果通道 results 用于接收被签名过的区块,这个区块将作为本地最新挖出的区块被添加进待网络中其它节点确认的节点列表中。
在签名过程中,如果出现错误,将发送中止消息给参数退出通道 stop,用于中止调用者的操作。参数退出通道 stop 是调用者指定的退出通道,用于接收被调用者的出错消息,从而中止后续操作。
主要实现如下:
- 获取区块头 header 用于后续的签名。代码为 header := block.Header()
- 获取区块编号 number,如果是创世区块则返回 errUnknownBlock,这是因为不支持对创始区块进行签名。
- 对于 0-period 的区块链,拒绝签名空区块(没有奖励,但会自旋签名),返回 errWaitTransactions。即如果 c.config.Period == 0,则区块中的事务不能为空。
- 不要在整个签名过程中持有签名者字段。代码为 c.lock.RLock(); signer, signFn := c.signer, c.signFn; c.lock.RUnlock()
- 检索到上一个区块时的快照,代码为 snap = c.snapshot(chain, number-1, header.ParentHash, nil)
- 检查当前共识引擎的签名者 signer 是否为有效的签名者,即检查 snap.Signers[signer] 是否存在,如果不存在则返回 errUnauthorized
- 如果签名者 signer 在 snap.Recents 的最近 K/2 + 1 个区块中签名过,则直接退出此次签名过程,将签名机会留给其它签名者(即网络中的其它矿工)。其中,K 为授权签名者列表 Snapshot.Signers 的长度。这一步会输出重要的日志信息 log.Info("Signed recently, must wait for others")
- 到此,共识协议允许我们签署区块,随机一个只属于我们的延迟签名时间 delay,这样是为了使得网络中各个签名者实际的签名时间有略微的差别。delay 的具体计算请参考代码。
- 使用签名者 singer 作为 Key,对区块签名哈希进行签名,并获得签名 sighash,代码为 sighash = signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())
- 将签名 sighash 替换区块头字段 Extra 中的最后 65 个字节,代码为 copy(header.Extra[len(header.Extra)-extraSeal:], sighash)
- 启动一个独立的匿名协程,等待签名完成或者延迟超时。这一步会输出重要的日志信息 log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay))
- 在退出通道 stop 和 Time 通道进行持续监听
- 如果从退出通道 stop 接收到中止消息,则直接中止协程。
- 如果在 dalay 之后从 Time 通道接收到消息,则继续后续的流程
- 用签名后的区块头替换原区块,并将带有签名的区块发送给结果通道 results,这个区块将作为本地最新挖出的区块被添加进待网络中其它节点确认的节点列表中。。见代码 results <- block.WithSeal(header):
- 在退出通道 stop 和 Time 通道进行持续监听
(15) func (c *Clique) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int
方法 CalcDifficulty() 是难度调整算法。它根据链中的先前区块和当前签名者返回新区块应具有的难度。
主要实现如下:
- 检索快照 snap,代码为 snap= c.snapshot(chain, parent.Number.Uint64(), parent.Hash(), nil)
- 将具体的计算转发给函数 CalcDifficulty(),代码为 return CalcDifficulty(snap, c.signer)
(16) func (c *Clique) SealHash(header *types.Header) common.Hash
方法 SealHash() 返回区块在被签名之前的哈希值,即返回区块签名哈希。
主要实现:
- 将具体的计算转发给函数 sigHash,代码为 sigHash(header)
(17) func (c *Clique) Close() error
方法 Close() 实现了接口 consensus.Engine。由于没有后端线程,这对 Clique 共识引擎来说是一个 noop 操作。
主要实现:
- return nil
(18) func (c *Clique) APIs(chain consensus.ChainReader) []rpc.API
方法 APIs() 实现了接口 consensus.Engine,给用户返回 RPC API 以允许其控制签名者投票。
如:
- 对账户 address 的授权投票 clique.propose(address, true)
- 对账户 address 的解除授权投票 clique.propose(address, false)
其中,address 为账户地址。
8. func CalcDifficulty(snap *Snapshot, signer common.Address) *big.Int
函数 CalcDifficulty() 是难度调整算法。它根据链中的先前区块和当前签名者返回新区块应具有的难度。
主要实现:
- 如果签名者 signer 在快照中的签名为 inturn,则难度为 diffInTurn,否则难度为 diffNoTurn。
Reference
- https://github.com/ethereum/go-ethereum/blob/master/consensus/clique/clique.go
Contributor
- Windstamp, https://github.com/windstamp