- func (s *Ethereum) StartMining(local bool) error {4 V& u% J( c* A6 i$ t; N
- eb, err := s.Etherbase()//用户地址) T) l' I/ T' i8 P
- if err != nil {: E# I; E1 z# g5 }
- log.Error("Cannot start mining without etherbase", "err", err)1 \9 S0 F+ }# J3 V5 |- t' x
- return fmt.Errorf("etherbase missing: %v", err)
- }1 ~6 G- ?6 c/ r$ I& A2 @, N
- if clique, ok := s.engine.(*clique.Clique); ok {* ~) n J3 L; P: r; _
- //如果是clique共识算法
- wallet, err := s.accountManager.Find(accounts.Account{Address: eb}) // 根据用它胡地址获取wallet对象$ F# S' {3 F5 R3 ?" {# Q$ x
- if wallet == nil || err != nil {
- log.Error("Etherbase account unavailable locally", "err", err)2 p& A. w% l! r9 T. Q2 `( w0 ~! T2 W
- return fmt.Errorf("signer missing: %v", err)
- }$ b- |0 l& f7 [; u2 n! `; K
- clique.Authorize(eb, wallet.SignHash) // 注入签名者以及wallet对象获取签名方法! {3 B% n* R: [. y5 N1 \6 `
- }' @) B* X6 l5 @" V7 }% ?
- if local {
- // 如果本地CPU已开始挖矿,我们可以禁用引入的交易拒绝机制来加速同步时间。CPU挖矿在主网是荒诞的,所以没有人能碰到这个路径,然而一旦CPU挖矿同步标志完成以后,将保证私网工作也在一个独立矿工结点。
- atomic.StoreUint32(&s.protocolManager.acceptTxs, 1). O2 l; U6 ?. T* Z( N
- }
- go s.miner.Start(eb)
- return nil# V4 b5 x% N# q+ g* ~
- }
这个StartMining会在miner.start前调用,然后通过woker -> agent -> CPUAgent -> update -> seal 挖掘区块和组装(后面会写单独的文章来对挖矿过程做源码分析)。
Clique的代码块在go-ethereum/consensus/clique路径下。和ethash一样,在clique.go 中实现了consensus的接口, consensus 定义了下面这些接口:4 e- K9 \, ~0 G( b, f
type Engine interface {5 y0 ]2 Q/ ]( [1 X3 d0 M ^
Author(header *types.Header) (common.Address, error), C: Y" ^: W5 g6 Z2 P
VerifyHeader(chain ChainReader, header *types.Header, seal bool) error" ?/ T/ o! V# s8 V# K! [8 S# V: F
VerifyHeaders(chain ChainReader, headers []*types.Header, seals []bool) (chan
Engine.Seal()函数可对一个调用过 Finalize()的区块进行授权或封印,成功时返回的区块全部成员齐整,可视为一个正常区块,可被广播到整个网络中,也可以被插入区块链等。对于挖掘一个新区块来说,所有相关代码里 Engine.Seal()是其中最重要最复杂的一步,所以这里我们首先来看下Clique 结构体:/ m! H' N/ h+ a5 P& k }
- type Clique struct {. [. \9 u @4 Y. k
- config *params.CliqueConfig // 共识引擎配置参数% R% ?, [ R7 w, `* f7 b: R n
- db ethdb.Database // 数据库,用来存储和获取快照检查点( `$ X4 M% ~" B! P8 d# p
- recents *lru.ARCCache // 最近区块快照,加速快照重组+ n/ M& e+ t+ U
- signatures *lru.ARCCache // 最近区块签名,加速挖矿
- proposals map[common.Address]bool // 目前正在推送的提案
- signer common.Address // 签名者的以太坊地址/ g: O2 e! k* B% V) { s/ I @0 w
- signFn SignerFn // 授权哈希的签名方法$ y& B/ j& c0 b% k# x! k, j1 t
- lock sync.RWMutex // 用锁来保护签名字段
- }
顺便来看下CliqueConfig共识引擎的配置参数结构体:
- type CliqueConfig struct {
- Period uint64 `json:"period"` // 在区块之间执行的秒数(比如出块秒数15s): j6 j# X Z6 t
- Epoch uint64 `json:"epoch"` // Epoch长度,重置投票和检查点(比如Epoch长度是30000个block, 每次进入新的epoch,前面的投票都被清空, 重新开始记录)
- }
在上面的 StartMining中,通过Clique. Authorize来注入签名者和签名方法,先来看下Authorize:
- func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
- c.lock.Lock()% }3 L1 `2 ]/ _7 ^! C1 G. f6 e( x
- defer c.lock.Unlock()
- // 这个方法就是为clique共识注入一个签名者的私钥地址已经签名函数用来挖出新块3 s- P) d4 W# t
- c.signer = signer* ]& m3 X; _- d( p
- c.signFn = signFn
- }
再来看Clique的Seal()函数的具体实现:* ^- J0 C' z3 L
//通过本地签名认证创建已密封的区块8 {2 Q. P. C% C
func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, stop number-limit {6 ]) |6 `/ \; v
log.Info("Signed recently, must wait for others")# o$ s9 [9 I/ B1 A
Seal是共识引擎的入口之一,该函数通过clique.signer对区块签名 A: k7 O" p; y6 ]+ ^0 F
signer不在snapshot的signer中不允许签名 z$ V( ], z" G# D
signer不是本区块的签名者需要延时随机一段时候后再签名,是本区块的签名者则直接签名7 V& h4 G3 M5 n+ h* u# l* L
签名存放在Extra的extraSeal的65个字节中0 T8 I/ T2 u2 A3 ?
关于机会均等 为了使得出块的负载(或者说是机会)对于每个认证节点尽量均等,同时避免某些恶意节点持续出块,clique中规定每一个认证节点在连续SIGNER_LIMIT个区块中,最多只能签发一个区块,也就是说,每一轮中,最多只有SIGNER_COUNT - SIGNER_LIMIT个认证节点可以参与区块签发。 其中SIGNER_LIMIT = floor(SIGNER_COUNT / 2) + 1,SIGNER_COUNT表示认证节点的个数。 N' i% s! g* g- [+ H( N* I5 N
- //snap.Signers是所有的认证节点% n- X0 F$ U; e' z
- for seen, recent := range snap.Recents {
- if recent == signer {' d$ s- U+ M' A9 J& t! P- M6 c
- if limit := uint64(len(snap.Signers)/2 + 1); number number-limit {
- log.Info("Signed recently, must wait for others")
在保证好节点的个数大于坏节点的前提下,好节点最少的个数为SIGNER_LIMIT(大于50%),坏节点最多的个数为SIGNER_COUNT - SIGNER_LIMIT(小于50%)。一个节点在SIGNER_LIMIT这个时间窗口内最多只能签发一个区块,这就使得恶意节点在不超过50%的情况下,从理论上无法一直掌握区块的签发权。) d; {0 B. y- V0 X8 u1 S0 [
关于难度计算 为了让每个认证节点都有均等的机会去签发一个区块,每个节点在签发时都会判断本节点是不是本轮的inturn节点,若是inturn节点,则该节点产生的区块难度为2,否则为1。每一轮仅有一个节点为inturn节点。9 w. Z! d! W, z: R
diffInTurn = big.NewInt(2)
diffNoTurn = big.NewInt(1)
当inturn的结点离线时,其他结点会来竞争,难度值降为1。然而正常出块时,limit中的所有认证结点包括一个inturn和其他noturn的结点,clique是采用了给noturn加延迟时间的方式来支持inturn首先出块,避免noturn的结点无谓生成区块,上面的延时代码段已经有提现了。 判断是否为inturn的节点,将本地维护的认证节点按照字典序排序,若当前区块号除以认证节点个数的余数等于该节点的下标,则该节点为inturn节点。代码实现在 snapshot.go中:
// 通过给定的区块高度和签发者返回该签发者是否在轮次内2 k1 f2 s1 {; k5 M+ p2 t
- func (s *Snapshot) inturn(number uint64, signer common.Address) bool {
- signers, offset := s.signers(), 02 E# }" _ a S% M; q" }
- for offset
Seal()代码中有获取快照,然后从快照中来检查授权区块签名者的逻辑,那么我们继续来看下Snapshot,首先看下Snapshot的结构体:
// Snapshot对象是在给定时间点的一个认证投票的状态
- type Snapshot struct {0 d x' P H/ N1 ~5 g, ~/ I# @. M
- config *params.CliqueConfig // 共识引擎配置参数
- sigcache *lru.ARCCache // 签名缓存,最近的区块签名加速恢复。$ U Q* p n2 S9 _6 Y. p4 u( N4 ]
- Number uint64 `json:"number"` // 快照建立的区块号
- Hash common.Hash `json:"hash"` // 快照建立的区块哈希* G3 \; _5 ?! X: J3 l2 S6 o3 m
- Signers map[common.Address]struct{} `json:"signers"` // 当下认证签名者的列表% ], P' {/ H3 `! k( N; C# M1 k
- Recents map[uint64]common.Address `json:"recents"` // 最近担当过数字签名算法的signer 的地址: |) f6 _7 G) m3 |
- Votes []*Vote `json:"votes"` // 按时间顺序排列的投票名单。
- Tally map[common.Address]Tally `json:"tally"` // 当前的投票结果,避免重新计算。( Y5 A% M, X( w; n5 I
- }
快照Snapshot对象中存在投票的Votes和记票的Tally对象:
- // Vote代表了一个独立的投票,这个投票可以授权一个签名者,更改授权列表。
- type Vote struct {
- Signer common.Address `json:"signer"` // 已授权的签名者(通过投票) R. h9 t* z6 W- \& F% X- z. O" Z
- Block uint64 `json:"block"` // 投票区块号
- Address common.Address `json:"address"` // 被投票的账户,修改它的授权' Z9 D. V/ ?, u( d
- Authorize bool `json:"authorize"` // 对一个被投票账户是否授权或解授权
- }
- // Tally是一个简单的用来保存当前投票分数的计分器
- type Tally struct {
- Authorize bool `json:"authorize"` // 授权true或移除false: O; C* U1 A/ a' C" f; v4 S
- Votes int `json:"votes"` // 该提案已获票数
- }
Snapshot是一个快照,不仅是一个缓存,而且存储了最近签名者的map loadSnapshot用来从数据库中加载一个已存在的快照:7 Y2 I' p3 e- j* m
- func loadSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, db ethdb.Database, hash common.Hash) (*Snapshot, error) {
- //使用Database接口的Get方法通过Key来查询缓存内容; O. H V) x) J' C3 m
- blob, err := db.Get(append([]byte("clique-"), hash[:]...))
- if err != nil {! D' c D7 x5 {% w" Y6 r
- return nil, err
- }8 A1 Z# q1 ^ ?2 h
- snap := new(Snapshot)6 K+ v/ O. U s' `* R5 z
- if err := json.Unmarshal(blob, snap); err != nil {
- return nil, err
- }* Y9 l! `0 \$ i6 A
- snap.config = config
- snap.sigcache = sigcache$ s7 L* {7 j6 [# }
- return snap, nil
- }
newSnapshot函数用于创建快照,这个方法没有初始化最近的签名者集合,所以只使用创世块:
- func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot { H3 I. u* i5 T5 E: ?/ L
- //组装一个Snapshot对象. K! V8 W: O! }
- snap := &Snapshot{
- config: config,4 r3 \) z1 G$ ]( [" h
- sigcache: sigcache,
- Number: number,- Y8 W$ o4 K6 H1 a* T
- Hash: hash,
- Signers: make(map[common.Address]struct{}),0 U- F6 D5 Z: v1 y& W" w
- Recents: make(map[uint64]common.Address),
- Tally: make(map[common.Address]Tally),% [* X8 J* l. N$ a7 G- z
- }
- for _, signer := range signers {
- snap.Signers[signer] = struct{}{}
- }
- return snap: Q5 k% ]6 p% q* c
- }
继续看下snapshot函数的具体实现:
- // 快照会在给定的时间点检索授权快照; A5 g9 Z6 z" E* p4 }
- func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error) {4 g1 Y' M. m. C& S8 ?( b- l9 X2 a
- // 在内存或者磁盘上查找一个快照来检查检查点checkpoints9 U) d, U6 d' y
- var (5 `# C% m1 O# `1 ]+ r
- headers []*types.Header //区块头
- snap *Snapshot //快照对象
- )
- for snap == nil {
- // 如果在内存中找到快照时,快照对象从内存中取 i0 p; p8 [% V0 U
- if s, ok := c.recents.Get(hash); ok {
- snap = s.(*Snapshot)
- break, X1 A4 k0 U9 K6 ~* f& S6 e5 D
- }# |0 H2 l* K' c, t6 S6 t
- // 如果在磁盘检查点找到快照时
- if number%checkpointInterval == 0 { //checkpointInterval = 1024 表示投票快照保存到数据库的区块的区块号
- if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil {
- log.Trace("Loaded voting snapshot form disk", "number", number, "hash", hash), ]8 F: L& p* u+ `" p: }
- snap = s
- break4 o: W: B q* c$ n( a% y
- }" g B I" N; \" Z5 [$ H: d/ j
- }
- // 如果在创世块,则新建一个快照- Y/ d: Q) G, V% M" w
- if number == 0 {
- genesis := chain.GetHeaderByNumber(0)
- if err := c.VerifyHeader(chain, genesis, false); err != nil {" q% i* |( |+ d: i0 D. K; K
- return nil, err5 G: y7 F/ C6 e; ?
- }
- signers := make([]common.Address, (len(genesis.Extra)-extraVanity-extraSeal)/common.AddressLength)2 J; J% Q+ t3 S- v2 V
- for i := 0; i 0 {
- // 如果我们有明确的父,从那里挑选(强制执行)3 b0 T8 m7 w1 ?
- header = parents[len(parents)-1]
- if header.Hash() != hash || header.Number.Uint64() != number {
- return nil, consensus.ErrUnknownAncestor: a- g0 t' f% F. W7 V- a0 a( K) h y
- }$ E& c/ Z) [- g) l j, _
- parents = parents[:len(parents)-1]
- } else {1 x! c% ^# z: F2 ^6 s
- // 没有明确的父(或者没有更多的父)转到数据库获取( V! o) ]+ C% @
- header = chain.GetHeader(hash, number), s0 f3 S: ?- P7 M- h: R
- if header == nil {5 Y. ^9 \( j% \1 |4 v
- return nil, consensus.ErrUnknownAncestor
- }
- }
- headers = append(headers, header); L( j( E2 Y, X
- number, hash = number-1, header.ParentHash8 C1 R1 ^$ |3 \3 F. m; s
- }
- // 找到了之前的快照,将所有的pedding块头放在它上面
- for i := 0; i 0 {2 v) q3 ]$ k' U% `% B
- if err = snap.store(c.db); err != nil {
- return nil, err
- }' `3 j' e6 o8 V0 ~& S
- log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)
- }
- return snap, err
- }
在snapshot中,snap.apply通过区块头来创建一个新的快照,这个apply中主要做什么操作?
- //apply将给定的区块头应用于原始头来创建新的授权快照。
- func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) {5 l. ~+ g4 }4 h4 F& u
- //可以传空区块头
- if len(headers) == 0 {# s# W9 C2 ^6 M
- return s, nil
- }
- //完整性检查区块头可用性, e. G" d) t. `1 g/ d
- for i := 0; i = limit {
- delete(snap.Recents, number-limit)" }/ n3 y8 i6 T
- }( d i0 Y3 d$ M
- // 从区块头中解密出来签名者地址5 b7 N0 q- }7 J/ y2 D: F# c8 o
- signer, err := ecrecover(header, s.sigcache)
- if err != nil {2 t' ^, Y4 l) x; `
- return nil, err
- }' p! q! R6 G8 a& W4 Z( u
- if _, ok := snap.Signers[signer]; !ok {
- return nil, errUnauthorized
- }2 L% [$ W* u2 d1 s! m
- for _, recent := range snap.Recents {8 a7 F3 \2 z1 q1 q/ _8 q8 S
- if recent == signer {
- return nil, errUnauthorized0 G( I/ Q t; y* w" a$ I; }" m
- }
- }
- snap.Recents[number] = signer9 `$ Q; Y: u2 H: {5 I/ j
- // 区块头认证,不管该签名者之前的任何投票
- for i, vote := range snap.Votes {
- if vote.Signer == signer && vote.Address == header.Coinbase {
- // 从缓存计数器中移除该投票
- snap.uncast(vote.Address, vote.Authorize)
- // 从按时间排序的列表中移除投票
- snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)$ k4 g8 A5 o c) W1 g+ o l+ s' P
- break // 只允许一票
- }
- }* [: E* D& R; k
- // 从签名者中计数新的投票
- var authorize bool1 i6 f. r4 ^8 ?" l) `. ~
- switch {
- case bytes.Equal(header.Nonce[:], nonceAuthVote):* M9 U) M: w, y2 U
- authorize = true
- case bytes.Equal(header.Nonce[:], nonceDropVote):
- authorize = false8 i3 x" o8 A, C# Z0 W
- default:2 U8 Q7 M+ Q# ]4 C
- return nil, errInvalidVote9 h' a2 W/ B5 M" b1 O. `9 m
- }
- if snap.cast(header.Coinbase, authorize) {8 n. ~; n [2 m% D+ {# |( C
- snap.Votes = append(snap.Votes, &Vote{+ O' l" r( N O/ G
- Signer: signer,+ T' D" [8 s: v
- Block: number,
- Address: header.Coinbase,3 e) y9 F2 g2 W0 N! Q8 \1 n! b% W
- Authorize: authorize,
- }); R; F/ F( Z6 d
- }$ i: X: h" n3 C5 I
- // 判断票数是否超过一半的投票者,如果投票通过,更新签名者列表
- if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 {
- if tally.Authorize {
- snap.Signers[header.Coinbase] = struct{}{}+ ?+ Z3 b( N1 J' m( d4 V
- } else {
- delete(snap.Signers, header.Coinbase)4 S8 p) ~( C; Q
- // 签名者列表缩减,删除最近剩余的缓存5 M5 N2 b" m) R# u4 X3 N
- if limit := uint64(len(snap.Signers)/2 + 1); number >= limit {0 \' u! V7 G9 r y8 [5 a* w
- delete(snap.Recents, number-limit)/ f3 x& F; b: t# l
- }
- for i := 0; i
Snapshot.apply()方法的主要部分是迭代处理每个header对象,首先从数字签名中恢复出签名所用公钥,转化为common.Address类型,作为signer地址。数字签名(signagure)长度65 bytes,存放在Header.Extra[]的末尾。如果signer地址是尚未认证的,则直接退出本次迭代;如果是已认证的,则投票+1。所以一个父区块可添加一张记名投票,signer作为投票方地址,Header.Coinbase作为被投票地址,投票内容authorized可由Header.Nonce取值确定。更新投票统计信息。如果被投票地址的总投票次数达到已认证地址个数的一半,则通过之。该被投票地址的认证状态立即被更改,根据是何种更改,相应的更新缓存数据,并删除过时的投票信息。在所有Header对象都被处理完后,Snapshot内部的Number,Hash值会被更新,表明当前Snapshot快照结构已经更新到哪个区块了。
区块验证的过程是普通节点在收到一个新区块时,会从区块头的extraData字段中取出认证节点的签名,利用标准的spec256k1椭圆曲线进行反解公钥信息,并且从公钥中截取出签发节点的地址,若该节点是认证节点,且该节点本轮拥有签名的权限,则认为该区块为合法区块。verifySeal是被SubmitWork(miner/remote_agent.go) 来调用,SubmitWork函数尝试注入一个pow解决方案(共识引擎)到远程代理,返回这个解决方案是否被接受。(不能同时是一个坏的pow也不能有其他任何错误,例如没有工作被pending)解决方案有效时,返回到矿工并且通知接受结果。$ C9 I1 m" ^& o' ^( m. W$ w
- // 检查包头中包含的签名是否满足共识协议要求。该方法接受一个可选的父头的列表,这些父头还不是本地区块链的一部分,用于生成快照
- func (c *Clique) verifySeal(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {
- // 不支持校检创世块
- number := header.Number.Uint64()& G& c- o$ O `4 r$ d
- if number == 0 {+ o: W! X3 f( K9 I1 A
- return errUnknownBlock0 I5 k& F, L& f* C( Q
- }" F% ^8 |& W. s$ q
- // 检索出所需的区块对象来校检去开头和将其缓存! u2 [5 G: R2 H% H6 ^
- snap, err := c.snapshot(chain, number-1, header.ParentHash, parents)# I6 V( B& G$ p% }0 `. P
- if err != nil {* E v' K1 P4 L1 g# I
- return err
- }
- //解析授权密钥并检查签署者,ecrecover方法从区块头中反解出Extra字段中签名字符串来获取签名者地址& `# L8 i/ f9 s9 M1 h( _
- signer, err := ecrecover(header, c.signatures)3 v, y5 T" q! a& N+ D6 p5 i
- if err != nil {% w$ M$ r0 _2 O4 V- ^
- return err1 Z2 t( s4 `" r. e5 {, B* d" u5 A
- }) ` u' H8 u! k0 {3 r. \9 H
- if _, ok := snap.Signers[signer]; !ok {
- return errUnauthorized
- }
- for seen, recent := range snap.Recents {
- if recent == signer {" L- T) r* t, `" u) ?
- // 签署者是最近的,只有当前块没有移出时才会失败,参见seal中的机会均等! L/ {: [! `1 g
- if limit := uint64(len(snap.Signers)/2 + 1); seen > number-limit {
- return errUnauthorized+ ~0 A" E2 m; b' J* r* h2 K) P
- }
- }
- }8 ?: F/ K& w2 `9 `( e q, U
- // 设置区块难度,参见上面的区块难度部分
- inturn := snap.inturn(header.Number.Uint64(), signer)6 @ h* g8 K. Q' k4 [
- if inturn && header.Difficulty.Cmp(diffInTurn) != 0 {1 U+ Y* \+ T: g. w( Z/ }# O% t
- return errInvalidDifficulty& l* P1 k; i3 w5 @/ m# n" j& v
- }
- if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 {
- return errInvalidDifficulty+ m8 ^! l. ?) e6 E [ Z1 X+ N
- }
- return nil2 l2 c7 \5 [9 V$ j, x
- }
前面已经分析了Clique的认证节点的出块和校检的过程,那么如何来区分一个节点是认证节点还是一个普通节点?以及一个授权者列表是如何产生并如何全网同步的?
Clique通过投票机制来确认一个认证节点,投票的范围在委员会中,委员会就是所有节点矿工集合,普通节点没有区块生成权利。矿工的投票流程如下:
委员会节点通过RPC调用Propose,对某节点状态变更,从普通节点变成认证阶段,或者相反,写入到Clique.purposal集合中8 c# K- G+ t: |: F! U' A
- // Propose注入一个新的授权提案,可以授权一个签名者或者移除一个。8 y( ~. t# {0 ^* G% E6 l
- func (api *API) Propose(address common.Address, auth bool) { x3 M" z$ {" E4 s$ \
- api.clique.lock.Lock()# I. |' z! e* h0 S( n8 ?4 Y
- defer api.clique.lock.Unlock()) {- _ O* n4 ]1 ?- T6 I
- api.clique.proposals[address] = auth// true:授权,false:移除
- }
本地认证节点在一次区块打包的过程中,从purposal池中随机挑选一条还未被应用的purposal,并将信息填入区块头,将区块广播给其他节点;" ?; s+ F% [. h/ @7 p
- //Clique.Prepare- j# l/ o' I( N6 N* B! q; H4 s/ E2 h
- // 抓取所有有意义投票的提案: r# O9 v. K, r2 ^( d
- addresses := make([]common.Address, 0, len(c.proposals))1 h- p3 y9 G: T% w T
- for address, authorize := range c.proposals {: d j+ s6 K" E$ U7 j) j
- if snap.validVote(address, authorize) {" D# @+ H5 ?; w, w) O4 t6 `" {
- addresses = append(addresses, address)$ b8 J! @/ p- i+ \' j" B
- }4 q: A. R, D' d# |! z+ q
- }
- // If there's pending proposals, cast a vote on them
- if len(addresses) > 0 {
- header.Coinbase = addresses[rand.Intn(len(addresses))] //随机挑选一条投票节点的地址赋值给区块头的Coinbase字段。. g8 _) J2 f7 r
- // 通过提案内容来组装区块头的随机数字段。
- if c.proposals[header.Coinbase] {* B7 B+ W' V, i/ U/ I* E; p
- copy(header.Nonce[:], nonceAuthVote)
- } else {" |3 I) ~( L4 n! s) D
- copy(header.Nonce[:], nonceDropVote)
- }
- }
在挖矿开始以后,会在miner.start()中提交一个commitNewWork,其中调用上面Prepare
- if err := self.engine.Prepare(self.chain, header); err != nil {( z* N0 A9 }. i7 r
- log.Error("Failed to prepare header for mining", "err", err)
- return8 Z2 `7 Z; m1 n% [; }: u7 P
- }
其他节点在接收到区块后,取出其中的信息,封装成一个vote进行存储,并将投票结果应用到本地,若关于目标节点的状态更改获得的一致投票超过1/2,则更改目标节点的状态:若为新增认证节点,将目标节点的地址添加到本地的认证节点的列表中;若为删除认证节点,将目标节点的地址从本地的认证节点列表中删除。具体实现可以查看上面的Snapshot.apply()方法
q- o0 x* X7 e% E- E8 N
& L: J. t+ ^3 I- E
以太坊中除了基于运算能力的POW(Ethash)外,还有基于权利证明的POA共识机制,Clique是以太坊的POA共识算法的实现,这里主要对POA的Clique相关源码做一个解读分析。