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


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

代碼分支: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM