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

mindcarver發表於2020-12-12

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

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

基本架構

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

image-20201212125409326

首先外部是通過miner物件進行了操作,miner裡面則是實用worker物件來實現挖礦的整體功能。miner決定著是否停止挖礦或者是否可以開始挖礦,同時還可以設定礦工的地址來獲取獎勵。

真正排程處理挖礦相關細節的則是在worker.go裡面,我們先來看一張總體的圖。

image-20201212201358073

上圖我們看到有四個迴圈,分別通過幾個channel負責不同的事:

newWorkLoop

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

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

mainLoop

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

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


啟動挖礦

挖礦的引數設定

geth挖礦的引數設定定義在 cmd/utils/flags.go 檔案中

引數 預設值 用途
–mine false 是否開啟自動挖礦
–miner.threads 0 挖礦時可用並行PoW計算的協程(輕量級執行緒)數。 相容過時引數 —minerthreads。
–miner.notify 挖出新塊時用於通知遠端服務的任意數量的遠端服務地址。 是用 ,分割的多個遠端伺服器地址。 如:”http://api.miner.com,http://api2.miner.com“
–miner.noverify false 是否禁用區塊的PoW工作量校驗。
–miner.gasprice 1000000000 wei 礦工可接受的交易Gas價格, 低於此GasPrice的交易將被拒絕寫入交易池和不會被礦工打包到區塊。
–miner.gastarget 8000000 gas 動態計算新區塊燃料上限(gaslimit)的下限值。 相容過時引數 —targetgaslimit。
–miner.gaslimit 8000000 gas 動態技術新區塊燃料上限的上限值。
–miner.etherbase 第一個賬戶 用於接收挖礦獎勵的賬戶地址, 預設是本地錢包中的第一個賬戶地址。
–miner.extradata geth版本號 允許礦工自定義寫入區塊頭的額外資料。
–miner.recommit 3s 重新開始挖掘新區塊的時間間隔。 將自動放棄進行中的挖礦後,重新開始一次新區塊挖礦。

常見的啟動挖礦的方式

引數設定挖礦

dgeth --dev --mine

控制檯啟動挖礦

miner.start(1)

rpc 啟動挖礦

這是部署節點使用的方式,一般設定如下:

/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函式中:

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

①:清除過舊的挖礦任務

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

②:提交新的挖礦任務

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

生成新的挖礦任務

根據newWorkCh生成新的挖礦任務,進入到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),
	}

②:根據共識引擎吃初始化header的共識欄位

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

③:為當前挖礦新任務建立環境

 w.makeCurrent(parent, header)

④:新增叔塊

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

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

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

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

⑥:填充交易到新區塊中

6.1 從交易池中獲取交易,並把交易分為本地交易和遠端交易,本地交易優先,先將本地交易提交,再將外部交易提交。

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
  • 如果碰到以下情況要進行交易執行的中斷
    • 新的頭塊事件到達,中斷訊號為 1 (整個任務會被丟棄)
    • worker 開啟或者重啟,中斷訊號為 1 (整個任務會被丟棄)
    • worker重新建立挖礦任務根據新的交易,中斷訊號為 2 (任務還是會被送入到共識引擎)

6.3開始執行交易

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

6.4執行交易獲取收據

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())

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

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

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

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

執行成功的話講交易和收據存入到w.current中。

⑦:執行交易的狀態更改,並組裝成最終塊

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

執行交易的狀態更改,並組裝成最終塊是由下面的共識引擎所完成的事情:

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

底層會呼叫 state.IntermediateRoot執行狀態更改。組裝成最終塊意味著到這打包任務完成。接著就是要提交新的挖礦任務。


提交新的挖礦任務

①:獲取sealHash(挖礦前的區塊雜湊),重複提交則跳過

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

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

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

挖礦的結果會返回到resultCh中或者stopCh中,resultCh有資料成功出塊,stopCh不為空,則中斷挖礦執行緒。


成功出塊

resultCh有區塊資料,則成功挖出了塊,到最後的成功出塊我們還需要進行相應的驗證判斷。

①:塊為空或者鏈上已經有塊或者pendingTasks不存在相關的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
  ...
}

③:提交塊和狀態到資料庫

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

④:廣播區塊並宣佈鏈插入事件

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

⑤:等待規範確認本地挖出的塊

新區塊並非立即穩定,暫時存入到未確認區塊集中。

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

總結&參考

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

https://mindcarver.cn

https://github.com/blockchainGuide

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

https://yangzhe.me/2019/02/25/ethereum-miner/#動態調整出塊頻��%8

相關文章