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

2018-06-11  本文已影响0人  187J3X1
ethereum.jpeg

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

挖矿

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

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

worker_miner_agent.png

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

Miner

Miner的定义如下

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

各字段作用如下, 其中标有的字段表示与Miner包外部有联系

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

worker

worker成员比较多,其中部分成员的意义如下

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

Agent

Agent(定义在worker.go)是一个抽象interface ,只要实现了其以下接口就可以充当worker的下属agent

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

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

type CpuAgent struct {
      workCh      chan *Work
      stop        chan struct{}
      returnCh    chan<-*Result
      chain     consensus.ChainReader
      engine   consensus.Engine
}
func (self *CpuAgent) Start(){
      if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1){
            return 
      }
      go self.update()
}

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

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

总结以下就是

让我们顺着一次实际的挖掘工作看看一个Block是如何被挖掘出来的以及挖掘出之后的过程
worker.commitNewWork()开始

commitNewWork.png
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/wiki/blob/master/Dagger-Hashimoto.md

Prepare()

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

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中,节点分为两类:

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
    ......
}

Snapshot翻译过来是快照,它记录了区块链在特定的时刻(即特定的区块高度)本地记录的认证地址列表,举个栗子,Block#18731的Snapshot记录了网络中存在3个signer分别为a\b\c,且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
}
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,它有以下几个获取途径

接下来随机地将本地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中的字段的一些意义也不尽相同。
上一篇 下一篇

猜你喜欢

热点阅读