以太坊源碼分析—挖礦與共識

前言

挖礦(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中的字段的一些意義也不盡相同。!
相關文章
相關標籤/搜索