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

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

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

基本架構

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

image-20201212125409326

首先外部是經過miner對象進行了操做,miner裏面則是實用worker對象來實現挖礦的總體功能。miner決定着是否中止挖礦或者是否能夠開始挖礦,同時還能夠設置礦工的地址來獲取獎勵。github

真正調度處理挖礦相關細節的則是在worker.go裏面,咱們先來看一張整體的圖。web

image-20201212201358073

上圖咱們看到有四個循環,分別經過幾個channel負責不一樣的事:數據庫

newWorkLoop

  1. startCh:接收startCh信號,開始挖礦
  2. chainHeadCh:表示接收到新區塊,須要終止當前的挖礦工做,開始新的挖礦。
  3. timer.C:默認每三秒檢查一次是否有新交易須要處理。若是有則須要從新開始挖礦。以便將加高的交易優先打包到區塊中。

newWorkLoop 中還有一個輔助信號,resubmitAdjustChresubmitIntervalCh。運行外部修改timer計時器的時鐘。resubmitAdjustCh是根據歷史狀況從新計算一個合理的間隔時間。而resubmitIntervalCh則容許外部,實時經過 Miner 實例方法 SetRecommitInterval 修改間隔時間。api

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 --mineide

控制檯啓動挖礦

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

相關文章
相關標籤/搜索