链客,专为开发者而生,有问必答!html
此文章来自区块链技术社区,未经容许拒绝转载。算法
clique
以太坊的官方共识算法是ethash算法,这在前文已经有了详细的分析:数据库
它是基于POW的共识机制的,矿工须要经过计算nonce值,会消耗大量算力来匹配target值。json
若是在联盟链或者私链的方案里,继续使用ethash就会浪费算力,POW也没有存在的意义。因此以太坊有了另外一种共识方案:基于POA的clique。api
POA, Proof of Authority。权力证实,不一样于POW的工做量证实,POA是可以直接肯定几个节点具有出块的权力,这几个节点出的块会被全网其余节点验证为有效块。数组
创建私链缓存
经过这篇文章的操做能够创建一个私有链,观察这个流程能够看到,经过puppeth工具创建创世块时,会提示你选择哪一种共识方式,有ethash和clique两个选项,说到这里咱们就明白了为何文章中默认要选择clique。安全
源码分析
讲过了基本概念,下面咱们深刻以太坊源码来仔细分析clique算法的具体实现。并发
入口仍然选择seal方法,这里与前文分析ethash算法的入口是保持一致的,由于他们是Seal的不一样实现。app
// 咱们的注释能够对比着来看,clique的seal函数的目的是:尝试经过本地签名认证(权力签名与认证,找到有权力的结点)来建立一个已密封的区块。
func (c Clique) Seal(chain consensus.ChainReader, block types.Block, stop <-chan struct{}) (*types.Block, error) {
header := block.Header() number := header.Number.Uint64() if number == 0 {// 不容许密封创世块 return nil, errUnknownBlock } // 跳转到下方Clique对象的分析。不支持0-period的链,同时拒绝密封空块,没有奖励可是可以旋转密封 if c.config.Period == 0 && len(block.Transactions()) == 0 { return nil, errWaitTransactions } // 在整个密封区块的过程当中,不要持有signer签名者字段。 c.lock.RLock() // 上锁获取config中的签名者和签名方法。 signer, signFn := c.signer, c.signFn c.lock.RUnlock() snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)// snapshot函数见下方分析 // 校验处理:若是咱们未经受权去签名了一个区块 if err != nil { return nil, err } if _, authorized := snap.Signers[signer]; !authorized { return nil, errUnauthorized } // 若是咱们是【最近签名者】的一员,则等待下一个区块,// 见下方[底层机制三](http://www.cnblogs.com/Evsward/p/clique.html#%E4%B8%89%E8%AE%A4%E8%AF%81%E7%BB%93%E7%82%B9%E7%9A%84%E5%87%BA%E5%9D%97%E6%9C%BA%E4%BC%9A%E5%9D%87%E7%AD%89) for seen, recent := range snap.Recents { if recent == signer { // Signer当前签名者在【最近签名者】中,若是当前区块没有剔除他的话只能继续等待。 if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit { log.Info("Signed recently, must wait for others") <-stop return nil, nil } } } // 经过以上校验,到了这里说明协议已经容许咱们来签名这个区块,等待此工做完成 delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now()) if header.Difficulty.Cmp(diffNoTurn) == 0 { // 这不是咱们的轮次来签名,delay wiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTime // wiggleTime = 500 * time.Millisecond // 随机推延,从而容许并发签名(针对每一个签名者) delay += time.Duration(rand.Int63n(int64(wiggle))) log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle)) } log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay)) select { case <-stop: return nil, nil case <-time.After(delay): } // 核心工做:开始签名 sighash, err := signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())// signFn函数见下方 if err != nil { return nil, err } copy(header.Extra[len(header.Extra)-extraSeal:], sighash)//将签名结果替换区块头的Extra字段(专门支持记录额外信息的) return block.WithSeal(header), nil //经过区块头从新组装一个区块
}
Clique对象的分析
// Clique是POA共识引擎,计划在Ropsten攻击之后,用来支持以太坊私测试链testnet(也能够本身搭建联盟链或者私有链)
type Clique struct {
config *params.CliqueConfig // 共识引擎配置参数,见下方CliqueConfig源码介绍 db ethdb.Database // 数据库,用来存储以及获取快照检查点 recents *lru.ARCCache // 最近区块的快照,用来加速快照重组 signatures *lru.ARCCache // 最近区块的签名,用来加速挖矿 proposals map[common.Address]bool // 目前咱们正在推进的提案清单,存的是地址和布尔值的键值对映射 signer common.Address // 签名者的以太坊地址 signFn SignerFn // 签名方法,用来受权哈希 lock sync.RWMutex // 锁,保护签名字段
}
CliqueConfig源码分析
// CliqueConfig是POA挖矿的共识引擎的配置字段。
type CliqueConfig struct {
Period uint64 `json:"period"` // 在区块之间执行的秒数(能够理解为距离上一块出块后的流逝时间秒数) Epoch uint64 `json:"epoch"` // Epoch['iːpɒk]长度,重置投票和检查点
}
snapshot函数分析
// snapshot函数可经过给定点获取认证快照
func (c Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []types.Header) (*Snapshot, error) {
// 在内存或磁盘上搜索一个快照以检查检查点。 var ( headers []*types.Header// 区块头 snap *Snapshot// 快照对象,见下方 ) for snap == nil { // 若是找到一个内存里的快照,使用如下方案: if s, ok := c.recents.Get(hash); ok { snap = s.(*Snapshot) break } // 若是一个在磁盘检查点的快照被找到,使用如下方案: if number%checkpointInterval == 0 {// checkpointInterval = 1024 // 区块号,在数据库中保存投票快照的区块。 if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil {// loadSnapshot函数见下方 log.Trace("Loaded voting snapshot form disk", "number", number, "hash", hash) snap = s break } } // 若是咱们在创世块,则作一个快照 if number == 0 { genesis := chain.GetHeaderByNumber(0) if err := c.VerifyHeader(chain, genesis, false); err != nil { return nil, err } signers := make([]common.Address, (len(genesis.Extra)-extraVanity-extraSeal)/common.AddressLength) for i := 0; i < len(signers); i++ { copy(signers[i][:], genesis.Extra[extraVanity+i*common.AddressLength:]) } snap = newSnapshot(c.config, c.signatures, 0, genesis.Hash(), signers)// 建立一个新的快照的函数,见下方 if err := snap.store(c.db); err != nil { return nil, err } log.Trace("Stored genesis voting snapshot to disk") break } // 没有针对这个区块头的快照,则收集区块头并向后移动 var header *types.Header if len(parents) > 0 { // 若是咱们有明确的父类,从这里强制挑拣出来。 header = parents[len(parents)-1] if header.Hash() != hash || header.Number.Uint64() != number { return nil, consensus.ErrUnknownAncestor } parents = parents[:len(parents)-1] } else { // 若是没有明确父类(或者没有更多的),则转到数据库 header = chain.GetHeader(hash, number) if header == nil { return nil, consensus.ErrUnknownAncestor } } headers = append(headers, header) number, hash = number-1, header.ParentHash } // 找到了先前的快照,那么将全部pending的区块头都放在它的上面。 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)//经过区块头生成一个新的snapshot对象 if err != nil { return nil, err } c.recents.Add(snap.Hash, snap)//将当前快照区块的hash存到recents中。 // 若是咱们生成了一个新的检查点快照,保存到磁盘上。 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
}
Snapshot对象源码分析:
// Snapshot对象是在给定点的一个认证投票的状态
type Snapshot struct {
config *params.CliqueConfig // 配置参数 sigcache *lru.ARCCache // 签名缓存,最近的区块签名加速恢复。 Number uint64 `json:"number"` // 快照创建的区块号 Hash common.Hash `json:"hash"` // 快照创建的区块哈希 Signers map[common.Address]struct{} `json:"signers"` // 当下认证签名者的集合 Recents map[uint64]common.Address `json:"recents"` // 最近签名区块地址的集合 Votes []*Vote `json:"votes"` // 按时间顺序排列的投票名单。 Tally map[common.Address]Tally `json:"tally"` // 当前的投票结果,避免从新计算。
}
loadSnapshot函数源码分析:
// loadSnapshot函数用来从数据库中加载一个现存的快照,参数列表中不少都是Snapshot对象的关键字段属性。
func loadSnapshot(config params.CliqueConfig, sigcache lru.ARCCache, db ethdb.Database, hash common.Hash) (*Snapshot, error) {
blob, err := db.Get(append([]byte("clique-"), hash[:]...))// ethdb使用的是leveldb,对外开放接口Dababase见下方 if err != nil { return nil, err } snap := new(Snapshot) if err := json.Unmarshal(blob, snap); err != nil { return nil, err } snap.config = config snap.sigcache = sigcache return snap, nil
}
ethdb数据库对外开放接口:
// Database接口包裹了全部的数据库相关操做,全部的方法都是线程安全的。
type Database interface {
Putter Get(key []byte) ([]byte, error)//经过某key获取值 Has(key []byte) (bool, error)//某key是否包含有效值 Delete(key []byte) error Close() NewBatch() Batch
}
newSnapshot函数源码:
// newSnapshot函数建立了一个新的快照,经过给出的特定的启动参数。这个方法没有初始化最近签名者的集合,因此只有使用创世块。
func newSnapshot(config params.CliqueConfig, sigcache lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot {
snap := &Snapshot{// 就是组装一个Snapshot对象,安装相应参数 config: config, sigcache: sigcache, Number: number, Hash: hash, Signers: make(map[common.Address]struct{}), Recents: make(map[uint64]common.Address), Tally: make(map[common.Address]Tally), } for _, signer := range signers { snap.Signers[signer] = struct{}{} } return snap
}
signFn函数:
// SignerFn是一个签名者的回调函数,用来请求一个可以被后台帐户签名生成的哈希
type SignerFn func(accounts.Account, []byte) ([]byte, error)
clique常量配置:
blockPeriod = uint64(15) // clique规定,两个区块的生成时间至少间隔15秒,timestamp类型。
Clique底层机制
在进入共识引擎以前,当前结点已经生成了一个完整的区块,包括区块头和密封的交易列表,而后进入seal函数,经过ethash或者clique算法引擎来操做出块确权。本文重点讲述了针对clique算法的源码分析,clique算法基于POA共识,是在结点中找出有权力的几个“超级结点”,只有这些结点能够生成合法区块,其余结点的出块都会直接丢弃。
一:clique是如何肯定签名者以及签名方法的?
我在clique文件中搜索,发现有一个方法作了这个工做:
// Authorize函数注入共识引擎clique一个私钥地址(签名者)以及签名方法signFn,用来挖矿新块
func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
c.lock.Lock() defer c.lock.Unlock() c.signer = signer c.signFn = signFn
}
那么继续搜索,该函数是在什么时候被调用的,找到了位于/eth/backend.go中的函数StartMining:
func (s *Ethereum) StartMining(local bool) error {
eb, err := s.Etherbase()// 用户地址 if err != nil { log.Error("Cannot start mining without etherbase", "err", err)//未找到以太帐户地址,报错 return fmt.Errorf("etherbase missing: %v", err) } // 若是是clique共识算法,则走if分支,若是是ethash则跳过if。 if clique, ok := s.engine.(*clique.Clique); ok {// Comma-ok断言语法见下方分析。 wallet, err := s.accountManager.Find(accounts.Account{Address: eb})// 经过用户地址得到wallet对象 if wallet == nil || err != nil { log.Error("Etherbase account unavailable locally", "err", err) return fmt.Errorf("signer missing: %v", err) } clique.Authorize(eb, wallet.SignHash)//在这里!注入了签名者以及经过wallet对象获取到签名方法 } if local { // 若是本地CPU挖矿已启动,咱们能够禁止注入机制以加速同步时间。 // CPU挖矿在主网是荒诞的,因此没有人能碰到这个路径,然而一旦CPU挖矿同步标志完成之后,将保证私网工做也在一个独立矿工结点。 atomic.StoreUint32(&s.protocolManager.acceptTxs, 1) } go s.miner.Start(eb)//并发启动挖矿工做 return nil
}
最终,经过miner.Start(eb),调用到work -> agent -> CPUAgent -> update -> seal,回到最上方咱们的入口。
这里要补充一点,挖矿机制是从miner.start()做为入口开始分析的,而上面的StartMining函数是在miner.start()以前的。这样就把整个这一条线串起来了。
Go语法补充:Comma-ok断言
if clique, ok := s.engine.(*clique.Clique); ok {
这段语句很使人迷惑,通过搜查,以上语法被称做Comma-ok断言。
value, ok = element.(T)
value是element变量的值,ok是布尔类型用来表达断言结果,element是接口变量,T是断言类型。
套入以上代码段,翻译过来即:
若是s.engine是Clique类型,则ok为true,同时clique就等于s.engine。
二:Snapshot起到的做用是什么?
Snapshot对象在Seal方法中是经过调用snapshot构造函数来获取到的。而snapshot构造函数内部有较长的函数体,包括newSnapshot方法以及loadSnapshot方法的处理。从这个分析来看,咱们也能够知道Snapshot是快照,也是缓存的一种机制,同时它也不只仅是缓存,由于它存储了最近签名者的map集合。
Snapshot能够从内存(即程序中的变量)或是磁盘上(即经过数据库leveldb)获取或者存储,实际上这就是二级缓存的概念了。
三:认证结点的出块机会均等
首先将上文Seal方法的源码遗留代码段展现以下。
for seen, recent := range snap.Recents {
if recent == signer { if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit { log.Info("Signed recently, must wait for others") <-stop return nil, nil } }
}
其中
if recent == signer {
若是当前结点最近签名过,则跳过,为保证机会均等,避免某个认证结点能够连续出块,从而做恶。
if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {
实际上到了这里就已经在决定出块权了。咱们依次来看,
snap.Signers是全部的认证结点。
limit的值是全部认证结点的数量的一半加1,也就是说能够保证limit>50%好的认证结点个数(安全性考虑:掌握大于50%的控制权)。结合上面的机会均等,clique要求认证结点在每轮limit个区块中只能生成一个区块。
number是当前区块号
seen是 “for seen, recent := range snap.Recents {” 中Recents的index,从0开始,最大值为Recents的总数-1。
接着,咱们来分析控制程序停止的条件表达式:
number < limit || seen > number-limit
number < limit, 若是区块高度小于limit
seen > number - limit,缓存中最近签发者序号已经超过了区块高度与limit之差。number-limit是最多的坏节点,索引seen大于坏节点也要中断(TODO: number区块高度与认证结点的关系)
在这两种状况下,会中断程序,中止签名以及出块操做。
四:出块难度
// inturn函数经过给定的区块高度和签发者返回该签发者是否在轮次内
func (s *Snapshot) inturn(number uint64, signer common.Address) bool {
// 方法体的内容就是区块高度与认证签发者集合长度的余数是否等于该签发者的下标值 signers, offset := s.signers(), 0 for offset < len(signers) && signers[offset] != signer { offset++ } return (number % uint64(len(signers))) == uint64(offset)
}
一句话,clique要求签发者必须按照其在snapshot中的认证签发者集合按照字典排序的顺序出块。
符合以上条件的话,难度为2,不然为1。
diffInTurn = big.NewInt(2) // 签名在轮次内的区块难度为2。
diffNoTurn = big.NewInt(1) // 签名未在轮次内的区块难度为1。
clique的出块难度比较容易理解,这是在POW中大书特书的部分但在clique中却十分简单,当inturn的结点离线时,其余结点会来竞争,难度值降为1。然而正常出块时,limit中的全部认证结点包括一个inturn和其余noturn的结点,clique是采用了给noturn加延迟时间的方式来支持inturn首先出块,避免noturn的结点无谓生成区块。这部分代码在下面再贴一次。
wiggle := time.Duration(len(snap.Signers)/2+1) wiggleTime // wiggleTime = 500 time.Millisecond // 随机推延,从而容许并发签名(针对每一个签名者)
delay += time.Duration(rand.Int63n(int64(wiggle)))
clique承认难度值最高的链为主链,因此彻底inturn结点出的块组成的链会是最理想的主链。
五:区块校验
// 一样位于clique文件中的verifySeal函数,顾名思义是结点用来校验别的结点广播过来的区块信息的。
func (c Clique) verifySeal(chain consensus.ChainReader, header types.Header, parents []*types.Header) error {
// 创世块的话不校验 number := header.Number.Uint64() if number == 0 { return errUnknownBlock } // 取到所需snapshot对象,用来校验区块头而且将其缓存。 snap, err := c.snapshot(chain, number-1, header.ParentHash, parents) if err != nil { return err } // 处理受权秘钥,检查是否违背认证签名者集合 signer, err := ecrecover(header, c.signatures)// 从区块头中解密出Extra字段,找到签名字符串,得到签名者地址信息。能够跳转到下面ecrecover函数的源码分析。 if err != nil { return err } if _, ok := snap.Signers[signer]; !ok { return errUnauthorized } // 与Seal相同的处理,机会均等 for seen, recent := range snap.Recents { if recent == signer { if limit := uint64(len(snap.Signers)/2 + 1); seen > number-limit { return errUnauthorized } } } // 区分是否inturn,设置区块困难度,上面也介绍过了。 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
}
ecrecover函数的源码分析:
// ecrecover函数从一个签名的区块头中解压出以太坊帐户地址
func ecrecover(header types.Header, sigcache lru.ARCCache) (common.Address, error) {
// 若是签名已经被缓存,返回它。 hash := header.Hash() if address, known := sigcache.Get(hash); known { return address.(common.Address), nil } // 从区块头的Extra字段取得签名内容。 if len(header.Extra) < extraSeal { return common.Address{}, errMissingSignature } signature := header.Extra[len(header.Extra)-extraSeal:] // 经过密码学技术从签名内容中解密出公钥和以太坊地址。 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:])//将公钥利用keccak256解密赋值给signer。 sigcache.Add(hash, signer)//加入缓存 return signer, nil
}
crypto包的Ecrecover函数:
func Ecrecover(hash, sig []byte) ([]byte, error) {
return secp256k1.RecoverPubkey(hash, sig)
}
Ecrecover函数是使用secp256k1来解密公钥。
下面咱们从VerifySeal函数反推,找出调用该函数的位置在miner/remote_agent.go,
// SubmitWork函数尝试注入一个pow解决方案(共识引擎)到远程代理,返回这个解决方案是否被接受。(不能同时是一个坏的pow也不能有其余任何错误,例如没有工做被pending
func (a *RemoteAgent) SubmitWork(nonce types.BlockNonce, mixDigest, hash common.Hash) bool {
a.mu.Lock() defer a.mu.Unlock() // 保证被提交的工做不是空 work := a.work[hash] if work == nil { log.Info("Work submitted but none pending", "hash", hash) return false } // 保证引擎是真实有效的。 result := work.Block.Header() result.Nonce = nonce result.MixDigest = mixDigest if err := a.engine.VerifySeal(a.chain, result); err != nil {//在这里,VerifySeal方法被调用。 log.Warn("Invalid proof-of-work submitted", "hash", hash, "err", err) return false } block := work.Block.WithSeal(result) // 解决方案看上去是有效的,返回到矿工而且通知接受结果。 a.returnCh <- &Result{work, block} delete(a.work, hash) return true
}
这个SubmitWork位于挖矿的pkg中,主要工做是对work的校验,包括work自己是否为空,work中的区块头以及区块头中包含的字段的有效性,而后是对区块头的VerifySeal(该函数的功能在上面已经介绍到了,主要是对区块签名者的认证,区块难度值的确认)
继续反推找到SubmitWork函数被调用的位置:
// SubmitWork函数可以被外部矿工用来提交他们的POW。
func (api *PublicMinerAPI) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) bool {
return api.agent.SubmitWork(nonce, digest, solution)
}
总结
区块的校验是外部结点自动执行PublicMinerAPI的SubmitWork方法,从而层层调用,经过检查区块头内的签名内容,经过secp256k1方法恢复公钥,而后利用Keccak256将公钥加密为一个以太地址做为签名地址,得到签名地址之后,去本地认证结点缓存中检查,看该签名地址是否符合要求。最终只要经过层层校验,就不会报出errUnauthorized的错误。
注意:签名者地址common.Address在Seal时被签名signature存在区块头的Extra字段中,而后在VerifySeal中被从区块头中取出签名signature。该签名的解密方式比较复杂:要先经过secp256k1恢复一个公钥,而后利用这个公钥和Keccak256加密出签名者地址common.Address。
common.Address自己就是结点公钥的Keccak256加密结果。请参照common/types.go:
// Hex函数返回了一个十六禁止的字符串,表明了以太坊地址。
func (a Address) Hex() string {
unchecksummed := hex.EncodeToString(a[:]) sha := sha3.NewKeccak256()//这里就不展开了,能够看出是经过Keccak256方法将未检查的明文Address加密为一个标准以太坊地址 sha.Write([]byte(unchecksummed)) hash := sha.Sum(nil) result := []byte(unchecksummed) for i := 0; i < len(result); i++ { hashByte := hash[i/2] if i%2 == 0 { hashByte = hashByte >> 4 } else { hashByte &= 0xf } if result[i] > '9' && hashByte > 7 { result[i] -= 32 } } return "0x" + string(result)
}
六: 基于投票的认证结点的运行机制
上面咱们分析了clique的认证结点的出块,校验等细节,那么这里引出终极问题:如何确认一个普通结点是不是认证结点呢?
答:clique是基于投票机制来确认认证结点的。
先来看投票实体类,存在于snapshot源码中。
// Vote表明了一个独立的投票,这个投票能够受权一个签名者,更改受权列表。
type Vote struct {
Signer common.Address `json:"signer"` // 已受权的签名者(经过投票) Block uint64 `json:"block"` // 投票区块号 Address common.Address `json:"address"` // 被投票的帐户,修改它的受权 Authorize bool `json:"authorize"` // 对一个被投票帐户是否受权或解受权
}
这个Vote是存在于Snapshot的属性字段中,因此投票机制离不开Snapshot,咱们在这里再次将Snapshot实体源码从新分析一遍,上面注释过的内容我再也不复述,而是直接关注在投票机制相关字段内容上。
type Snapshot struct {
config *params.CliqueConfig sigcache *lru.ARCCache Number uint64 `json:"number"` Hash common.Hash `json:"hash"` Signers map[common.Address]struct{} `json:"signers"` // 认证节点集合 Recents map[uint64]common.Address `json:"recents"` Votes []*Vote `json:"votes"` // 上面的Vote对象数组 Tally map[common.Address]Tally `json:"tally"` // 也是一个自定义类型,见下方
}
Tally结构体:
// Tally是一个简单的用来保存当前投票分数的计分器
type Tally struct {
Authorize bool `json:"authorize"` // 受权true或移除false Votes int `json:"votes"` // 该提案已获票数
}
另外Clique实体中还有个有争议的字段proposals,当时并无分析清楚,何谓提案?
proposal是能够经过rpc申请加入或移除一个认证节点,结构为待操做地址(节点地址)和状态(加入或移除)
投票中某些概念的肯定
投票的范围是在委员会,委员会的意思就是全部矿工。
概念介绍:checkpoint,checkpointInterval = 1024 ,每过1024个区块,则保存snapshot到数据库
概念介绍:Epoch,与ethash同样,一个Epoch是三万个区块
投票流程
首先委员会某个成员(即节点矿工)经过rpc调用consensus/clique/api.go中的propose方法
// Propose注入一个新的受权提案,能够受权一个签名者或者移除一个。
func (api *API) Propose(address common.Address, auth bool) {
api.clique.lock.Lock() defer api.clique.lock.Unlock() api.clique.proposals[address] = auth// true:受权,false:移除
}
上面rpc提交过来的propose会写入Clique.proposals集合中。
在挖矿开始之后,会在miner.start()中提交一个commitNewWork,其中涉及到准备区块头Prepare的方法,咱们进入到clique的实现,其中涉及到对上面的Clique.proposals的处理:
// 若是存在pending的proposals,则投票
if len(addresses) > 0 {
header.Coinbase = addresses[rand.Intn(len(addresses))]//将投票节点的地址赋值给区块头的Coinbase字段。 // 下面是经过提案内容来组装区块头的随机数字段。 if c.proposals[header.Coinbase] { copy(header.Nonce[:], nonceAuthVote) } else { copy(header.Nonce[:], nonceDropVote) }
}
// nonceAuthVote和nonceDropVote常量的声明与初始化
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") // 受权签名者的必要随机数
nonceDropVote = hexutil.MustDecode("0x0000000000000000") // 移除签名者的必要随机数
整个区块组装好之后(其余的内容再也不复述),会被广播到外部结点校验,若是没有问题该块被成功出了,则区块头中的这个提案也会被记录在主链上。
区块在生成时,会建立Snapshot,在snapshot构造函数中,会涉及到对proposal的处理apply方法。
// apply经过接受一个给定区块头建立了一个新的受权
func (s Snapshot) apply(headers []types.Header) (*Snapshot, error) {
if len(headers) == 0 { return s, nil } for i := 0; i < len(headers)-1; i++ { if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 { return nil, errInvalidVotingChain } } if headers[0].Number.Uint64() != s.Number+1 { return nil, errInvalidVotingChain } snap := s.copy() // 投票的处理核心代码 for _, header := range headers { // Remove any votes on checkpoint blocks number := header.Number.Uint64() // 若是区块高度正好在Epoch结束,则清空投票和计分器 if number%s.config.Epoch == 0 { snap.Votes = nil snap.Tally = make(map[common.Address]Tally) } if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { delete(snap.Recents, number-limit) } // 从区块头中解密出来签名者地址 signer, err := ecrecover(header, s.sigcache) if err != nil { return nil, err } if _, ok := snap.Signers[signer]; !ok { return nil, errUnauthorized } for _, recent := range snap.Recents { if recent == signer { return nil, errUnauthorized } } snap.Recents[number] = signer // 区块头认证,无论该签名者以前的任何投票 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:]...) break // only one vote allowed } } // 从签名者中计数新的投票 var authorize bool switch { case bytes.Equal(header.Nonce[:], nonceAuthVote): authorize = true case bytes.Equal(header.Nonce[:], nonceDropVote): authorize = false default: return nil, errInvalidVote } if snap.cast(header.Coinbase, authorize) { snap.Votes = append(snap.Votes, &Vote{ Signer: signer, Block: number, Address: header.Coinbase, Authorize: authorize, }) } // 判断票数是否超过一半的投票者,若是投票经过,更新签名者列表 if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 { if tally.Authorize { snap.Signers[header.Coinbase] = struct{}{} } else { delete(snap.Signers, header.Coinbase) if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { delete(snap.Recents, number-limit) } for i := 0; i < len(snap.Votes); i++ { if snap.Votes[i].Signer == header.Coinbase { snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize) snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) i-- } } } // 无论以前的任何投票,直接改变帐户 for i := 0; i < len(snap.Votes); i++ { if snap.Votes[i].Address == header.Coinbase { snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) i-- } } delete(snap.Tally, header.Coinbase) } } snap.Number += uint64(len(headers)) snap.Hash = headers[len(headers)-1].Hash() return snap, nil
}
关键控制的代码是tally.Votes > len(snap.Signers)/2,意思是计分器中的票数大于一半的签名者,就表示该投票经过,下面就是要更改snapshot中的认证签名者列表缓存,同时要同步给其余节点,并删除该投票相关信息。
总结
本觉得clique比较简单,没必要调查这么长,然而POA的共识算法仍是比较有难度的,它和POW是基于彻底不一样的两种场景的实现方式,出块方式也彻底不一样。下面我尝试用简短的语言来总结Clique的共识机制。
clique共识是基于委员会选举认证节点来确认出块权力的方式实现的。投票方式经过rpc请求propose,snapshot二级缓存机制,唱票,执行投票结果。认证节点出块机会均等,困难度经过轮次(是否按照缓存认证顺序出块)肯定,区块头Extra存储签名,keccak256加密以太地址,secp256k1解密签名为公钥,经过认证结点出块的逻辑能够反推区块校验。
到目前为止,咱们对POA共识机制,以及以太坊clique的实现有了深入的理解与认识,相信若是让咱们去实现一套POA,也是彻底有能力的。你们在阅读本文时有任何疑问都可留言给我,我必定会及时回复。