当前位置: 首页 > 编程日记 > 正文

以太坊挖矿源码:clique算法

链客,专为开发者而生,有问必答!

此文章来自区块链技术社区,未经允许拒绝转载。
在这里插入图片描述
clique
以太坊的官方共识算法是ethash算法,这在前文已经有了详细的分析:

它是基于POW的共识机制的,矿工需要通过计算nonce值,会消耗大量算力来匹配target值。

如果在联盟链或者私链的方案里,继续使用ethash就会浪费算力,POW也没有存在的意义。所以以太坊有了另一种共识方案:基于POA的clique。

POA, Proof of Authority。权力证明,不同于POW的工作量证明,POA是能够直接确定几个节点具备出块的权力,这几个节点出的块会被全网其他节点验证为有效块。

建立私链

通过这篇文章的操作可以建立一个私有链,观察这个流程可以看到,通过puppeth工具建立创世块时,会提示你选择哪种共识方式,有ethash和clique两个选项,说到这里我们就明白了为什么文章中默认要选择clique。

源码分析
讲过了基本概念,下面我们深入以太坊源码来仔细分析clique算法的具体实现。

入口仍然选择seal方法,这里与前文分析ethash算法的入口是保持一致的,因为他们是Seal的不同实现。

// 我们的注释可以对比着来看,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")<-stopreturn nil, nil}}
}
// 通过以上校验,到了这里说明协议已经允许我们来签名这个区块,等待此工作完成
delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now())
if header.Difficulty.Cmp(diffNoTurn) == 0 {// 这不是我们的轮次来签名,delaywiggle := 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 = mixDigestif 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 boolswitch {case bytes.Equal(header.Nonce[:], nonceAuthVote):authorize = truecase bytes.Equal(header.Nonce[:], nonceDropVote):authorize = falsedefault: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,也是完全有能力的。大家在阅读本文时有任何疑问均可留言给我,我一定会及时回复。

相关文章:

JS中简单原型的使用

转载于:https://www.cnblogs.com/hwgok/p/6163335.html

vuex+vue-router拦截

干就完了 项目中经常遇到这样一个场景&#xff0c;用户信息或者进行增删改的一些模块&#xff0c;需要根据用户是否登录&#xff0c;进行路由拦截&#xff0c;直接上代码 在store文件夹下的store.js中存放一个默认登录状态 /** store.js* */ import Vue from vue import Vuex …

通关制单机器人_2020关务节|“数字供应链与智能通关”论坛——如何打造云上跨境贸易生态圈...

点击标题下「蓝色微信名」可快速关注 随着跨境贸易的飞速发展&#xff0c;其涉及的有商流、信息流、资金流与物流。其中&#xff0c;物流特别是跨境物流&#xff0c;又是其中较为重要的一个环节。如何解决跨境贸易的物流物流困难&#xff1f;让我们来听听&#xff0c;欧坚集团副…

区块链技术世界

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 2017年发展最火热的技术&#xff0c;我觉得一个人工智能AI&#xff0c;另一个当之无愧的是一个叫区块链东西。最典型的例子是&#xff0c;人类顶…

Python学习心得第一周-03练习2

#5. 求1-23-45 ... 99的所有数的和 res0 count1 while count <100:if count%2 0:res-countelse:rescountcount1 print(res) #6. 用户登陆&#xff08;三次机会重试&#xff09; count0 while count<3:nameinput(name:)passwordinput(password:)if nameztc and passwords…

与MySQL传统复制相比,GTID有哪些独特的复制姿势?

与MySQL传统复制相比&#xff0c;GTID有哪些独特的复制姿势? http://mp.weixin.qq.com/s/IF1Pld-wGW0q2NiBjMXwfg 陈华军&#xff0c;苏宁云商IT总部资深技术经理&#xff0c;从事数据库服务相关的开发和维护工作&#xff0c;之前曾长期从事富士通关系数据库的开发&#xff0c…

方法的运用_企业如何运用论坛做营销,千享科技分享技巧方法

随着互联网的普及&#xff0c;对企业的发展带来了很大的影响&#xff0c;传统的营销已经满足不了企业的发展&#xff0c;需要运用互联网来营销&#xff0c;企业也意识到了互联网营销的重要性&#xff0c;而做互联网营销可以分成几种形式进行&#xff0c;像百度知识营销、论坛营…

区块链开发入门

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 区块链这么火&#xff0c;可是你很快就会发现&#xff0c;想要入门区块链开发&#xff0c;尤其是想要从零开始学习区块链编程&#xff0c;根本都找…

linux怎么创建牡蛎_文件amp;目录小技巧 | Linux后门系列

0x01 Linux 目录技巧我们都知道 Windows 下文件和文件夹命名是有很多规则和限制的&#xff0c;但是可以通过一些程序来绕过限制&#xff0c;今天我们来看看 Linux 有哪些有趣的规则 参考 https://www.pathname.com/fhs/pub/fhs-2.3.pdf当然了&#xff0c;我这种人怎么可能按照官…

php简单算法之冒泡排序

<?php $arr [2,4,1,5,3,11,6,999,88,666,66,44,22,33,776];function getNewArr($arr){$count count($arr);//该层循环控制 需要冒泡的轮数for($i1;$i<$count;$i){//该层循环用来控制每轮 冒出一个数 需要比较的次数for($k0;$k<$count-$i;$k){if($arr[$k]>$arr[…

iOS单个应用程序的最大可用内存是多少?

iOS单个应用程序的最大可用内存是多少&#xff1f; StackOverflow上有人做了一些简单的测试&#xff0c;有限设备下迄今为止测到的结果&#xff1a; iPad1: 127MB/256MB/49% (大致crash临界值 / 总内存 / 占比)iPad2: 275MB/512MB/53%iPad3: 645MB/1024MB/62%iPad4: 585MB/102…

sql 存储过程和函数

最近在学习数据库&#xff0c;上课过程中总是在许多知识点有或多或少的问题&#xff0c;对于这些问题的产生&#xff0c;大概是由于我听课习惯所造成的吧&#xff0c;好啦&#xff0c;废话不多说&#xff0c;开始今天到主题吧。 首先介绍SQL的存储过程&#xff0c;先来给它定义…

怎樣制作线段动画_PPT动画还能这么做?我擦!动画源文件免费送你

擦除动画&#xff0c;可以说是基础得不能再基础PPT动画之一了&#xff0c;我们几乎可以在任何带有PPT动画效果的演示中找到它的踪影。简单的直线擦除效果可能大部分都会&#xff0c;那么把直线换成曲线呢&#xff1f;小小的变动都会让你措手不及。所以&#xff0c;你确定自己真…

Linux最大打开文件描述符数

1. 系统最大打开文件描述符数&#xff1a;/proc/sys/fs/file-max a. 查看 $ cat /proc/sys/fs/file-max 186405 2. 设置 a. 临时性 # echo 1000000 > /proc/sys/fs/file-max 2. 永久性&#xff1a;在/etc/sysctl.conf中设置 fs.file-max 1000000 2. 进程最大…

XMT.com超200万被区块链终端交易

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 狭义来讲&#xff0c;区块链是一种按照时间顺序将数据区块以顺序相连的方式组合成的一种链式数据结构&#xff0c; 并以密码学方式保证的不可篡改和…

初学LINQ语句

//有两个数组&#xff0c;客户和地址&#xff0c;他们之间通过公司名关联&#xff1a;var customers new[] { new {CustomerID1,FirstName"Kim",LastName"Abercrombie",CompanyName"Alpine Sky House"},new {CustomerID2,FirstName"Jeff&q…

android 开启一个定时线程_ANDROID开发中定时器的3种方法

在android中&#xff0c;经常用到的定时器主要有以下几种实现&#xff1a;一、采用Handler与线程的sleep(long )方法二、采用Handler的postDelayed(Runnable, long) 方法三、采用Handler与timer及TimerTask结合的方法。下面逐一介绍&#xff1a;一、采用Handle与线程的sleep(lo…

083 HBase的完全分布式的搭建与部署,以及多master

一&#xff1a;前提准备 1.设置时间同步 2.清空logs&#xff0c;datas 3.格式化集群 bin/hdfs namenode -format 4.重启集群 sbin/start-dfs.sh sbin/start-yarn.sh 5.删除zookeeper下的data&#xff0c;并新建zkData 6.在zkData下新建myid 7.分发&#xff0c;后&#xff0c;修…

区块链技术指北

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 回顾近现代几次工业革命&#xff0c;人类的发展随着技术的变革而突飞猛进。第一次工业革命革命&#xff0c;以工作机的诞生为开始&#xff0c;以蒸…

cmd查看所有数据库 db2_DB2数据库常用命令集

【IT168 技术】在DB2的开发过程中&#xff0c;贯穿整个开发过程还有很重要的一部分工作就是数据库的维护&#xff1b;对于维护一个庞大信息系统来说是非常必要的&#xff1b;留一份简易的维护手册&#xff0c;以备不时之需&#xff1b;以下收集到的部分维护命令&#xff0c;以飨…

[原创]SparkR针对mysql的数据读写操作实现

网上翻了两天没找到一份有用的文章&#xff0c;自己研究SparkR的官方api文档&#xff0c;总算找到了实现的接口 我是用R语言加载SparkR库的方式&#xff0c;当然也可以直接用SparkR控制台就不用自己加载SparkR的库了 #首先加载sparkR的库 Sys.setenv(TEST_HOME "/root/so…

使用vue2.0 vue-router vuex 模拟ios7操作

其实你也可以&#xff0c;甚至做得更好... 首先看一下效果&#xff1a;用vue2.0实现SPA&#xff1a;模拟ios7操作 与 通讯录实现 github地址是&#xff1a;https://github.com/QRL909109/ios7 如果您觉得可以&#xff0c;麻烦给一个star&#xff0c;支持我一下。 之前接触过Ang…

区块链技术是否会终结开源时代?

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 2017年11月18~19日&#xff0c;在上海交大召开的2017中国开源年会&#xff0c;在第二天我们组织了一个“闭门会议”。在这个闭门会议上&#xff0c…

Direct2D开发:Direct2D 和 GDI 互操作性概述

本主题说明如何结合使用 Direct2D 和 GDI&#xff08;可能为英文网页&#xff09;。有两种方法可以结合使用 Direct2D 和 GDI&#xff1a;您可以将 GDI 内容写入与 Direct2D GDI 兼容的呈现器目标&#xff0c;也可以将 Direct2D 内容写入 GDI 设备上下文 (DC) 0X01 将Direct2D内…

vmware虚拟机启动centOs黑屏

如图所示 &#xff0c; 我的VM 启动虚拟机之后就变成了上面的样子&#xff0c;一直不动&#xff0c;ping也ping不好&#xff0c;这个时候 &#xff1a; 1. 要么 内存不够了&#xff1b; 2. 要么 网络协议存在问题了&#xff1b; 本地windows环境在管理员的cmd命令行输入 &…

plc和pc串口通讯接线_让你搞懂PLC串口通讯和通讯接口,这东西估计没几个能说清楚~...

电力作业人员在使用PLC的时候会接触到很多的通讯协议以及通讯接口&#xff0c;最基本的PLC串口通讯和基本的通讯接口你都了解吗&#xff1f;1&#xff0c;什么是串口通讯&#xff1f;串口是计算机上一种非常通用设备通信的协议(不要与通用串行总线Universal Serial Bus或者USB混…

西班牙放大招,利用区块链技术防腐

链客&#xff0c;专为开发者而生&#xff0c;有问必答&#xff01; 此文章来自区块链技术社区&#xff0c;未经允许拒绝转载。 在过去十年来&#xff0c;西班牙爆发了一系列引人注目的腐败丑闻&#xff0c;其中以公共采购最甚。但据TI 2017年的腐败认知指数表明&#xff0c;西…

FreeBSD 8

FreeBSD 8.0的安装过程和7.2区别不大。先在FreeBSD官方网站上下载安装镜像&#xff0c;我一般都下载DVD的ISO&#xff0c;也有人爱好下最小的安装包&#xff0c;然后通过FTP或HTTP方式从网上下载各个程序包。这里就以DVD的ISO为例&#xff0c;下载DVD的ISO后&#xff0c;刻录到…

c潭州课堂25班:Ph201805201 MySQL第二课 (课堂笔记)

mysql> create table tb_2( -> id int, -> name varchar(10) not null -> ); 插入数据 insert into tb_2 value(1,xiaobai); 在非空时&#xff0c;NOT NULL 必须有值&#xff0c; 2&#xff0c;在已有的表中设置一个字段的非空约束 mysql> alter table tb_2 -…

vanpopup 高度_解决VantUI popup 弹窗不弹出或无蒙层的问题

背景####组件PopupTime.vue把vant官网的popup时间选择器抽成组件&#xff1a;popup1show: true 即弹窗显示:title"popupTitle.popupName"v-model"currentDate"type"datetime"cancel"onCancel" confirm"onConfirm" class&quo…