死磕以太坊源碼分析之挖礦流程分析html
代碼分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9node
以太坊挖礦的主要流程是由miner
包負責的,下面是基本的一個架構:git
首先外部是經過miner
對象進行了操做,miner
裏面則是實用worker
對象來實現挖礦的總體功能。miner決定着是否中止挖礦或者是否能夠開始挖礦,同時還能夠設置礦工的地址來獲取獎勵。github
真正調度處理挖礦相關細節的則是在worker.go裏面,咱們先來看一張整體的圖。web
上圖咱們看到有四個循環,分別經過幾個channel
負責不一樣的事:數據庫
startCh
:接收startCh
信號,開始挖礦chainHeadCh
:表示接收到新區塊,須要終止當前的挖礦工做,開始新的挖礦。timer.C
:默認每三秒檢查一次是否有新交易須要處理。若是有則須要從新開始挖礦。以便將加高的交易優先打包到區塊中。在 newWorkLoop
中還有一個輔助信號,resubmitAdjustCh
和 resubmitIntervalCh
。運行外部修改timer計時器的時鐘。resubmitAdjustCh
是根據歷史狀況從新計算一個合理的間隔時間。而resubmitIntervalCh
則容許外部,實時經過 Miner
實例方法 SetRecommitInterval
修改間隔時間。api
newWorkCh
:接收生成新的挖礦任務信號chainSideCh
:接收區塊鏈中加入了一個新區塊做爲當前鏈頭的旁支的信號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)
這是部署節點使用的方式,通常設置以下:
/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.go
的New
函數中:
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)個高度中選擇,最多選擇兩個叔塊放入新區塊中.在真正添加叔塊的同時會進行校驗,包括以下:
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
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://github.com/blockchainGuide
https://learnblockchain.cn/books/geth/part2/mine/design.html