以太坊源码分析—挖矿与共识

前言

挖矿(mine)是指矿工节点互相竞争生成新区块以写入整个区块链得到奖励的过程.
共识(consensus)是指区块链各个节点对下一个区块的内容造成一致的过程
在以太坊中, miner包向外提供挖矿功能,consensus包对外提供共识引擎接口git

挖矿

miner包主要由miner.go worker.go agent.go 三个文件组成github

  • Miner 负责与外部交互和高层次的挖矿控制
  • worker 负责低层次的挖矿控制 管理下属全部Agent
  • Agent 负责实际的挖矿计算工做

三者之间的顶层联系以下图所示
worker_miner_agentgolang

下面先从这几个数据结构的定义和建立函数来了解下它们之间的联系

Miner

Miner的定义以下算法

type Miner struct{
    mux *event.TypeMux 
    worker *worker
    coinbase common.Address
    eth  Backend
    engine consensus.Engine
    .... 
}

各字段做用以下, 其中标有的字段表示与Miner包外部有联系数据库

  • mux 接收来自downloader模块的_StartEvent_ DoneEvent _FailedEvent_事件通知。在网络中,不可能只有一个矿工节点,当downloader开始从其余节点同步Block时,咱们就没有必要再继续挖矿了.
  • eth 经过该接口可查询后台TxPool BlockChain ethdb的数据.举例来讲,做为矿工,咱们在生成一个新的Block时须要从TxPool中取出pending Tx(待打包成块的交易),而后将它们中的一部分做为新的Block中的Transaction
  • engine 采用的共识引擎,目前以太坊公网采用的是ethash,测试网络采用clique.
  • worker 对应的worker,从这里看出Miner和worker是一一对应的
  • coinbase 本矿工的帐户地址,挖矿所得的收入将计入该帐户
  • mining 标识是否正在挖矿

miner.New()建立一个Miner,它主要完成Miner字段的初始化和如下功能api

  • 使用miner.newWorker()建立一个worker
  • 使用miner.newCpuAgent()建立Agent 并用Register方法注册给worker
  • 启动miner.update() 线程.该线程等待mux上的来自 downloader模块的事件通知用来控制挖矿开始或中止

worker

worker成员比较多,其中部分红员的意义以下缓存

  • mux engine eth coinbase 这几项都来自与miner, 其中mux相对于Miner里的稍微有点不一样, Miner里的mux是用来接收downloader的事件,而worker里用mux来向外部发布已经挖到新Block
  • txCh 从后台eth接收新的Tx的Channel
  • chainHeadCh 从后台eth接收新的Block的Channel
  • recv 从agents接收挖矿结果的Channel,注意,每一个管理的agent均可能将挖出的Block发到该Channel,也就是说,这个收方向Channel是一对多的
  • agents 管理的全部Agent组成的集合

miner.newWorker() 建立一个worker,它除了完成各个成员字段的初始化,还作了如下工做网络

  • 向后台eth注册txCh chainHeadCh chainSideCh通道用来接收对应数据
  • 启动worker.update() 线程.该线程等待上面几个外部Channel 并做出相应处理
  • 启动worker.wait()线程.该线程等待Agent挖出的新Block
  • 调用worker.commitNewWork() 尝试启动新的挖掘工做

Agent

Agent(定义在worker.go)是一个抽象interface ,只要实现了其如下接口就能够充当worker的下属agent数据结构

type Agent interface {
    Work()   chan <-*Work
    SetReturnCh (chan<-*Result)
    Stop()
    Start()
    GetHashRate() int64
}

在agent.go中定义了CpuAgent做为一种Agent的实现,其主要成员定义以下app

type CpuAgent struct {
      workCh      chan *Work
      stop        chan struct{}
      returnCh    chan<-*Result
      chain     consensus.ChainReader
      engine   consensus.Engine
}
  • workCh 接收来自worker下发的工做任务Work
  • returnChworker反馈工做任务的完成状况,实际上就是挖出的新Block
  • stop 使该CpuAgent中止工做的信号
  • chain 用于访问本地节点BlockChain数据的接口
  • engine 计算所采用的共识引擎

CpuAgent的建立函数中并无启动新的线程, Agent的工做线程是由Agent.Start()接口启动的
CpuAgent实现中,启动了CpuAgent.update()线程来监听workChstop信道

func (self *CpuAgent) Start(){
      if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1){
            return 
      }
      go self.update()
}

而Agent真正的挖矿工做是在收到工做任务'Work'后调用CpuAgent.mine()完成的

以上就是Miner worker Agent三者之间的联系,将它们画成一张图以下:

总结如下就是

  • Miner监听后台的数据
  • 须要挖矿时,worker发送给各个Agent工做任务Work, Agent挖出后反馈给worker

让咱们顺着一次实际的挖掘工做看看一个Block是如何被挖掘出来的以及挖掘出以后的过程
worker.commitNewWork()开始
这里写图片描述
1.parent Block是权威链上最新的Block
2.将标识矿工帐户的Coinbase填入Header,这里生成的Header只是个半成品
3.对于ehtash来讲,这里计算Block的Difficulty
4.工做任务Work 准确地说标识一次挖掘工做的上下文Context,在建立时,它包含了当前最新的各个帐户信息state和2中生成的Header,在这个上下中能够经过调用work.commitTransactions()执行这些交易,这就是俗称的打包过程
5.矿工老是选择Price高的交易优先执行,由于这能使其得到更高的收益率,因此对于交易的发起者来讲,若是指望本身的交易能尽快被全部人认可,他能够设置更高gasPrice以吸引矿工优先打包这笔交易
6.运行EVM执行这些交易
7.调用共识引擎的Finalize()接口
8.如此,一个Block的大部分原料都已经准备好了,下一步就是发送给Agent来将这个Block挖掘出来

Cpuagent收到Work后,调用mine()方法

func (self *CpuAgent) mine(work *Work, stop<-chan struct{}) {
        result, _  = self.engine.Seal(self.chain, work.Block, stop) 
        self.returnCh <- &Result{work,result}
}

能够看到其实是调用的共识接口的Engine.Seal接口,挖掘的细节在后面共识部分详述,这里先略过这部分且不考虑挖矿被Stop的情景,Block被挖掘出来以后将经过CpuAgent.returnCh反馈给workerworkerwait线程收到接口后将结果写入数据库,经过worker.mux向外发布NewMinedBlockEvent事件,这样以太坊的其余在该mux上订阅了该事件组件就能够收到这个事件

共识

共识部分包含由consensus对外提供共识引擎的接口定义,当前以太坊有两个实现,分别是公网使用的基于POW的ethash包和测试网络使用的基于POA的clique

根据前文的分析,在挖矿过程当中主要涉及Prepare() Finalize() Seal() 接口,三者的职责分别为
Prepare() 初始化新Block的Header
Finalize() 在执行完交易后,对Block进行修改(好比向矿工发放挖矿所得)
Seal() 实际的挖矿工做

ethash

ethash是基于POW(Proof-of-Work),即工做量证实,矿工消耗算力来求得一个nonce,使其知足难度要求HASH(Header) <= C / Diff,注意,这里的HASH是一个很复杂的函数,而nonce是Header的一个成员字段,一旦改变nonce,左边的结果将发生很大的变化。 C是一个很是大的常数,Diff是Block的难度,可由此可知,Diff越大,右式越小,要想找到知足不等式的nonce就愈加的困难,而矿工正是消耗本身的算力去不断尝试nonce,若是找到就意味着他挖出这个区块。
本文不打算详述具体的HASH函数,感兴趣的读者能够参考官方文档https://github.com/ethereum/w...

Prepare()

ethash的Prepare()计算新Block须要达到的难度(Diffculty),这部分理论可见https://www.jianshu.com/p/9e5...

Finalize()

ethash的Finalize()向矿工节点发放奖励,再Byzantium时期以前的区块,挖出的区块奖励是5 ETH
,以后的奖励3 ETH,这部分理论比较复杂,准备之后专门写一篇文章。

Seal()

下面来看看ethash具体是怎么实现Seal接口的

core/ethash/sealer.go
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop<-chan struct{})(*types.Block, error){
   ......
   abort := make(chan struct{})
   found:= make(chan *types.Blocks)
   threads:= runtime.NumCPU()
   for i := 0; i < threads; i++ {
        go func(id int, nonce uint64){
             ethash.mine(block,id,nonce,abort,found)
        }(i, uint64(ethash.rand.Int63()))
   }
   var result *type.Block
   select{
       case <- stop:
       ....
       case result<-found:
       close(abort)
    }
    return result, nil
}

能够看到,ethash启动了多个线程调用mine()函数,当有线程挖到Block时,会经过传入的found通道传出结果。

core/ethash/sealer.go
func (ethash *Ethash) mine(block *types.Block, id int, 
seed uint64, abort chan struct{}, found chan *types.Block) {
.....
search:
    for {
        select {
            case <-abort:    
            ......
            default:
            digest, result := hashimotoFull(dataset.dataset, hash, nonce)
            if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
                // Correct nonce found, create a new header with it
                header = types.CopyHeader(header)
                header.Nonce = types.EncodeNonce(nonce)
                // Seal and return a block (if still needed)
                select {
                    case found <- block.WithSeal(header):
                    ......
                    case <-abort:
                }
                break search
            }
            nonce++
         }
    }
......

能够看到,在主要for循环中,不断递增nonce的值,调用hashimotoFull()函数计算上面公式中的左边,而target则是公式的右边。当找到一个nonce使得左式<=右式时,挖矿结束,nonce填到header.Nonce

clique

以太网社区为开发者提供了基于POA(proof on Authortiy)的clique共识算法。与基于POS的ethash不一样的是,clique挖矿不消耗矿工的算力。在clique中,节点分为两类:

  • 通过认证(Authorized)的节点,在源码里称为signer,具备生成(签发)新区块的能力,对应网络里的矿工
  • 未通过认证的节点,对应网络里的普通节点

ethash中,矿工的帐户地址存放在Header的Coinbase字段,但在clique中,这个字段另有他用。那么如何知道一个Block的挖掘者呢?答案是,矿工用本身的私钥对Block进行签名(Signature),存放在Header的Extra字段,其余节点收到后,能够从这个字段提取出数字签名以及签发者(signer)的公钥,使用这个公钥能够计算出矿工(即signer)的帐户地址。
一个节点a的认证状态能够互相转换,每一个signer在签发Block时,能够附带一个提议(purposal),提议另外一个本地记录为非认证的节点b转变为认证节点,或者相反。网络中的其余节点c收到这个提议后,将其转化为一张选票(Vote),若是支持节点的选票超过了节点c本地记录的signer数量的一半,那么节点c就认可节点b是signer

clique包由api.go clique.go snapshot.go三个文件组成
其中api.go中是一些提供给用户的命令行操做,好比用户能够输入如下命令表示他支持b成为signer

clique.propose("帐户b的地址", true)

clique.gosnapshot.go中分别定义两个重要的数据结构CliqueSnapshot
Clique数据结构的主要成员定义以下

type  Clique struct {
    config *params.CliqueConfig
    recents      *lru.ARCCache
    signatures   *lrn.ARCCache
    proposals   map[common.Address]bool
    signer common.Address
    signFn  SignerFn
    ......
}
  • config 包含两个配置参数,其中Period设置模拟产生新Block的时间间隔,而Epoch表示每隔必定数量的Block就要把当前的投票结果清空并存入数据库,这么作是为了防止节点积压过多的投票信息,相似于单机游戏中的存档
  • recents 缓存最近访问过的Snapshot,查询的key为Block的Hash值,详见以后的Snapshot
  • signatures 缓存最近访问过的Block的signer,查询的key为Block的Hash值
  • proposals 本节点待附带的提议池,用户经过propose()命名提交的提议会存放在这里,当本节点做为矿工对一个Block进行签名时,会随机选择池中的一个提议附带出去
  • signer 矿工节点的帐户地址,意义上与ethash中的Coinbase相似
  • signFn 数字签名函数,它和signer都由Clique.Authorize()进行设置,后者在eth/backend.go中的StartMining()中被调用

Snapshot翻译过来是快照,它记录了区块链在特定的时刻(即特定的区块高度)本地记录的认证地址列表,举个栗子,Block#18731的Snapshot记录了网络中存在3个signer分别为abc,且a已经支持另外一个节点d成为signer(a投了d一张支持票),当Block#18732的挖掘者b也支持d时,Block#18732记录的signer就会增长d的地址

type Snapshot struct{
    sigcache  *lru.ARCCache
    Number    uint64
    Hash    Common.Hash
    Signers map[Common.Address] struct{}
    Recents  map[uint64]common.Address
    Votes    []*Vote
    Tally    map[common.Address]Tally
}
  • sigcache 缓存最近访问过的signer,key为Block的Hash值
  • Number 本Snapshot对应的Block的高度,在建立时肯定
  • Hash 本Snapshot对应的Block的Hash,在建立时肯定
  • Signers 本Snapshot对应时刻网络中认证过的节点地址(矿工),在建立时肯定
  • Recents 最近若干个Block的signer的集合,即挖出区块的矿工
  • Votes 由收到的有效proposal计入的选票集合,每张选票记录了投票人/被投票人/投票意见 这里的有效有两层意思

    • 投票人是有效的的,首先他是signer(在Snapshot.Signers中),而且他不能频繁投票(不在 Snapshot.Recents中)
    • 被投票人是有效的,被投票人的当前认证状态与选票中携带的意见不一样
  • Tally 投票结果map,key为被投票人地址,value为投票计数
Prepare()

Prepare()的实现分为两部分

func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header){
    header.Coinbase = common.Address{}
    header.Nonce = types.BlockNonce{}
    number := header.Number.Uint64()

    snap, err := c.snapshot(chain, num-1, header.ParentHash, nil)
    if number % c.config.Epoch {
        addresses := make ([]common.Address)
        for address, authorize := range c.proposals{
            addresses = append(addresses, address)
        }
        header.Coinbase = addresses[rand.Intn(len(addresses))]
        if c.proposals[header.Coinbase] {
            copy(header.Nonce[:], nonceAuthVote)
        }  else {
            copy(header.Nonce[:], nonceDropVote)
        }
    }
    ......

首先获取上一个Block的Snapshot,它有如下几个获取途径

  • Clique的缓存
  • 若是Block的高度刚好是在checkpoint 就可从数据库中读取
  • 由一个以前已有的Snapshot通过这之间的全部Header推算出来

接下来随机地将本地proposal池中的一个目标节点地址放到Coinbase (注意在ethash中,这个字段填写的是矿工地址) 因为Clique不须要消耗算力,也就不须要计算nonce,所以在Clique中,Header的Nonce的字段被用来表示对目标节点投票的意见

func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header){
   ......
   header.Difficulty = CalcDifficulty(snap, c.signer)
   header.Extra  = append(header.Extra, make([]byte, extraSeal))
   ......

接下来填充Header中的Difficulty字段,在Clique中这个字段只有 12 两个取值,取决与本节点是否inturn,这彻底是测试网络为了减小Block区块生成冲突的一个技巧,由于测试网络不存在真正的计算,那么如何肯定下一个Block由谁肯定呢?既然都同样,那就轮流坐庄,inturn的意思就是本身的回合,咱们知道,区块链在生成中很容易出现短暂的分叉(fork),其中难度最大的链为权威(canonocal)链,所以若是一个节点inturn,它就把难度设置为 2 ,不然设置为 1

前面提到过在Clique中,矿工的地址不是存放在Coinbase,而是将本身对区块的数字签名存放在Header的Extra字段,能够看到在Prepare()接口中为数字签名预留了Extra的后 65 bytes

Finalize()

cliqueFinalize()操做比较简单,就是计算了一下Header的Root Hash值

Seal()

Seal()接口相对ethash的实现来讲比较简单 (省略了一些检查)

func (c *Clique) Seal (chain consensus.ChainReader, block *type.Block, stop <-chan struct{})  (*types.Block, error) {
    header := block.Header()
    signer, signFn := c.signer, c.signFn
    snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
    delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now())
    ......
    select {
    case <- stop:
        return nil, nil
    case <-time.After(delay):
    }
    
    sighash, err := signFn(accounts.Account{Address:signer}, sigHash(header).Bytes())
    copy(header.Extra[len(header.Extra) - extraSeal:], sighash)
    return block.WithSeal(header), nil
}

总的来讲就是延迟了必定时间后对Block进行签名,而后将本身的签名存入header的Extra字段的后 65 bytes,为了减小冲突,对于不是inturn的节点还会多延时一下子,上面的代码我省略了这部分

总结

  1. 挖矿的框架由miner包提供,期间使用了consensus包完成新的Block中一些字段的填充,总的来讲挖矿分为打包交易挖掘两个阶段
  2. 以太坊目前实现了ethashclique两套共识接口实现,分别用于公网环境和测试网络环境,前者消耗算力,后者不消耗。而且,他们对于Header中的字段的一些意义也不尽相同。!
相关文章
相关标签/搜索