死磕以太坊源碼分析之挖礦流程分析

死磕以太坊源碼分析之挖礦流程分析

代碼分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9

基本架構(gòu)

以太坊挖礦的主要流程是由miner包負責的,下面是基本的一個架構(gòu):

image-20201212125409326

首先外部是通過miner對象進行了操作,miner里面則是實用worker對象來實現(xiàn)挖礦的整體功能。miner決定著是否停止挖礦或者是否可以開始挖礦,同時還可以設(shè)置礦工的地址來獲取獎勵。

真正調(diào)度處理挖礦相關(guān)細節(jié)的則是在worker.go里面,我們先來看一張總體的圖。

image-20201212201358073

上圖我們看到有四個循環(huán),分別通過幾個channel負責不同的事:

newWorkLoop

  1. startCh:接收startCh信號,開始挖礦
  2. chainHeadCh:表示接收到新區(qū)塊,需要終止當前的挖礦工作,開始新的挖礦。
  3. timer.C:默認每三秒檢查一次是否有新交易需要處理。如果有則需要重新開始挖礦。以便將加高的交易優(yōu)先打包到區(qū)塊中。

newWorkLoop 中還有一個輔助信號,resubmitAdjustChresubmitIntervalCh。運行外部修改timer計時器的時鐘。resubmitAdjustCh是根據(jù)歷史情況重新計算一個合理的間隔時間。而resubmitIntervalCh則允許外部,實時通過 Miner 實例方法 SetRecommitInterval 修改間隔時間。

mainLoop

  1. newWorkCh:接收生成新的挖礦任務(wù)信號
  2. chainSideCh:接收區(qū)塊鏈中加入了一個新區(qū)塊作為當前鏈頭的旁支的信號
  3. txsCh:接收交易池的Pending中新加入了交易事件的信號

TaskLoop則是提交新的挖礦任務(wù),而resultLoop則是成功出塊之后做的一些處理。


啟動挖礦

挖礦的參數(shù)設(shè)置

geth挖礦的參數(shù)設(shè)置定義在 cmd/utils/flags.go 文件中

參數(shù) 默認值 用途
–mine false 是否開啟自動挖礦
–miner.threads 0 挖礦時可用并行PoW計算的協(xié)程(輕量級線程)數(shù)。 兼容過時參數(shù) —minerthreads。
–miner.notify 挖出新塊時用于通知遠程服務(wù)的任意數(shù)量的遠程服務(wù)地址。 是用 ,分割的多個遠程服務(wù)器地址。 如:”http://api.miner.com,http://api2.miner.com“
–miner.noverify false 是否禁用區(qū)塊的PoW工作量校驗。
–miner.gasprice 1000000000 wei 礦工可接受的交易Gas價格, 低于此GasPrice的交易將被拒絕寫入交易池和不會被礦工打包到區(qū)塊。
–miner.gastarget 8000000 gas 動態(tài)計算新區(qū)塊燃料上限(gaslimit)的下限值。 兼容過時參數(shù) —targetgaslimit。
–miner.gaslimit 8000000 gas 動態(tài)技術(shù)新區(qū)塊燃料上限的上限值。
–miner.etherbase 第一個賬戶 用于接收挖礦獎勵的賬戶地址, 默認是本地錢包中的第一個賬戶地址。
–miner.extradata geth版本號 允許礦工自定義寫入?yún)^(qū)塊頭的額外數(shù)據(jù)。
–miner.recommit 3s 重新開始挖掘新區(qū)塊的時間間隔。 將自動放棄進行中的挖礦后,重新開始一次新區(qū)塊挖礦。

常見的啟動挖礦的方式

參數(shù)設(shè)置挖礦

dgeth --dev --mine

控制臺啟動挖礦

miner.start(1)

rpc 啟動挖礦

這是部署節(jié)點使用的方式,一般設(shè)置如下:

/geth --datadir "/data0" --nodekeyhex "27aa615f5fa5430845e4e99229def5f23e9525a20640cc49304f40f3b43824dc" --bootnodes $enodeid --mine --debug --metrics --syncmode="full" --gcmode=archive --istanbul.blockperiod 5 --gasprice 0 --port 30303 --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --nat any --allow-insecure-unlock


開始源碼分析,進入到miner.goNew函數(shù)中:

func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, isLocalBlock func(block *types.Block) bool) *Miner {
    miner := &Miner{
        ...
    }
    go miner.update()
    return miner
}
func (miner *Miner) update() {
  switch ev.Data.(type) {
            case downloader.StartEvent:
                atomic.StoreInt32(&miner.canStart, 0)
                if miner.Mining() {
                    miner.Stop()
                    atomic.StoreInt32(&miner.shouldStart, 1)
                    log.Info("Mining aborted due to sync")
                }
            case downloader.DoneEvent, downloader.FailedEvent:
                shouldStart := atomic.LoadInt32(&miner.shouldStart) == 1

                atomic.StoreInt32(&miner.canStart, 1)
                atomic.StoreInt32(&miner.shouldStart, 0)
                if shouldStart {
                    miner.Start(miner.coinbase)
                }
}

一開始我們初始化的canStart=1 , 如果Downloader模塊正在同步,則canStart=0,并且停止挖礦,如果Downloader模塊Done或者Failed,則canStart=1,且同時shouldStart=0,miner將啟動。

miner.Start(miner.coinbase)

func (miner *Miner) Start(coinbase common.Address) {
...
    miner.worker.start()
}
func (w *worker) start() {
...
    w.startCh <- struct{}{}
}

接下來將會進入到mainLoop中去處理startCh

①:清除過舊的挖礦任務(wù)

clearPending(w.chain.CurrentBlock().NumberU64())

②:提交新的挖礦任務(wù)

commit := func(noempty bool, s int32) {
...
        w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp}
...
    }

生成新的挖礦任務(wù)

根據(jù)newWorkCh生成新的挖礦任務(wù),進入到CommitNewWork中:

①:組裝header

header := &types.Header{ //組裝header
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1), //num+1
        GasLimit:   core.CalcGasLimit(parent, w.config.GasFloor, w.config.GasCeil),
        Extra:      w.extra,
        Time:       uint64(timestamp),
    }

②:根據(jù)共識引擎吃初始化header的共識字段

w.engine.Prepare(w.chain, header); 

③:為當前挖礦新任務(wù)創(chuàng)建環(huán)境

 w.makeCurrent(parent, header)

④:添加叔塊

叔塊集分本地礦工打包區(qū)塊和其他挖礦打包的區(qū)塊。優(yōu)先選擇自己挖出的區(qū)塊。選擇時,將先刪除太舊的區(qū)塊,只從最近的7(staleThreshold)個高度中選擇,最多選擇兩個叔塊放入新區(qū)塊中.在真正添加叔塊的同時會進行校驗,包括如下:

  • 叔塊存在報錯
  • 添加的uncle是父塊的兄弟報錯
  • 叔塊的父塊未知報錯
commitUncles(w.localUncles)
commitUncles(w.remoteUncles)

⑤:如果noempty為false,則提交空塊,不填充交易進入到區(qū)塊中,表示提前挖礦

if !noempty {
  w.commit(uncles, nil, false, tstart)
}

⑥:填充交易到新區(qū)塊中

6.1 從交易池中獲取交易,并把交易分為本地交易和遠程交易,本地交易優(yōu)先,先將本地交易提交,再將外部交易提交。

localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending
    for _, account := range w.eth.TxPool().Locals() {
        if txs := remoteTxs[account]; len(txs) > 0 {
            delete(remoteTxs, account)
            localTxs[account] = txs
        }
    }
if len(localTxs) > 0 {
   txs := types.NewTransactionsByPriceAndNonce(w.current.signer, localTxs)
   if w.commitTransactions(txs, w.coinbase, interrupt) {
      return
   }
}
if len(remoteTxs) > 0 {
   ...
}

6.2提交交易

  • 首先校驗有沒有可用的Gas
  • 如果碰到以下情況要進行交易執(zhí)行的中斷
    • 新的頭塊事件到達,中斷信號為 1 (整個任務(wù)會被丟棄)
    • worker 開啟或者重啟,中斷信號為 1 (整個任務(wù)會被丟棄)
    • worker重新創(chuàng)建挖礦任務(wù)根據(jù)新的交易,中斷信號為 2 (任務(wù)還是會被送入到共識引擎)

6.3開始執(zhí)行交易

logs, err := w.commitTransaction(tx, coinbase)

6.4執(zhí)行交易獲取收據(jù)

receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig())

如果執(zhí)行出錯,直接回退上一個快照

if err != nil {
        w.current.state.RevertToSnapshot(snap)
        return nil, err
    }

出錯的原因大概有以下幾個:

  • 超出當前塊的gas limit
  • Nonce 太低
  • Nonce 太高

執(zhí)行成功的話講交易和收據(jù)存入到w.current中。

⑦:執(zhí)行交易的狀態(tài)更改,并組裝成最終塊

w.commit(uncles, w.fullTaskHook, true, tstart)

執(zhí)行交易的狀態(tài)更改,并組裝成最終塊是由下面的共識引擎所完成的事情:

block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)

底層會調(diào)用 state.IntermediateRoot執(zhí)行狀態(tài)更改。組裝成最終塊意味著到這打包任務(wù)完成。接著就是要提交新的挖礦任務(wù)。


提交新的挖礦任務(wù)

①:獲取sealHash(挖礦前的區(qū)塊哈希),重復提交則跳過

sealHash := w.engine.SealHash(task.block.Header()) // 返回挖礦前的塊的哈希
            if sealHash == prev {
                continue
            }

②:生成新的挖礦請求,結(jié)果返回到reultCh或者StopCh

w.engine.Seal(w.chain, task.block, w.resultCh, stopCh);

挖礦的結(jié)果會返回到resultCh中或者stopCh中,resultCh有數(shù)據(jù)成功出塊,stopCh不為空,則中斷挖礦線程。


成功出塊

resultCh有區(qū)塊數(shù)據(jù),則成功挖出了塊,到最后的成功出塊我們還需要進行相應的驗證判斷。

①:塊為空或者鏈上已經(jīng)有塊或者pendingTasks不存在相關(guān)的sealhash,跳過處理

if block == nil {}
if w.chain.HasBlock(block.Hash(), block.NumberU64()) {}
task, exist := w.pendingTasks[sealhash] if !exist {}

②:更新receipts

for i, receipt := range task.receipts {
  receipt.BlockHash = hash
  ...
}

③:提交塊和狀態(tài)到數(shù)據(jù)庫

_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true) // 互斥

④:廣播區(qū)塊并宣布鏈插入事件

w.mux.Post(core.NewMinedBlockEvent{Block: block})

⑤:等待規(guī)范確認本地挖出的塊

新區(qū)塊并非立即穩(wěn)定,暫時存入到未確認區(qū)塊集中。

w.unconfirmed.Insert(block.NumberU64(), block.Hash())

總結(jié)&參考

整個挖礦流程還是比較的簡單,通過 4 個Loop互相工作,從開啟挖礦到生成新的挖礦任務(wù)到提交新的挖礦任務(wù)到最后的成功出塊,這里面的共識處理細節(jié)不會提到,接下來的文章會說到。

https://mindcarver.cn

https://github.com/blockchainGuide

https://learnblockchain.cn/books/geth/part2/mine/design.html

https://yangzhe.me/2019/02/25/ethereum-miner/#%E5%8A%A8%E6%80%81%E8%B0%83%E6%95%B4%E5%87%BA%E5%9D%97%E9%A2%91%E7%8E%8

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容