死磕以太坊源碼分析之挖礦流程分析
基本架構(gòu)
以太坊挖礦的主要流程是由miner包負(fù)責(zé)的,下面是基本的一個(gè)架構(gòu):

首先外部是通過(guò)miner對(duì)象進(jìn)行了操作,miner里面則是實(shí)用worker對(duì)象來(lái)實(shí)現(xiàn)挖礦的整體功能。miner決定著是否停止挖礦或者是否可以開(kāi)始挖礦,同時(shí)還可以設(shè)置礦工的地址來(lái)獲取獎(jiǎng)勵(lì)。
真正調(diào)度處理挖礦相關(guān)細(xì)節(jié)的則是在worker.go里面,我們先來(lái)看一張總體的圖。

上圖我們看到有四個(gè)循環(huán),分別通過(guò)幾個(gè)channel負(fù)責(zé)不同的事:
newWorkLoop
-
startCh:接收startCh信號(hào),開(kāi)始挖礦 -
chainHeadCh:表示接收到新區(qū)塊,需要終止當(dāng)前的挖礦工作,開(kāi)始新的挖礦。 -
timer.C:默認(rèn)每三秒檢查一次是否有新交易需要處理。如果有則需要重新開(kāi)始挖礦。以便將加高的交易優(yōu)先打包到區(qū)塊中。
在 newWorkLoop 中還有一個(gè)輔助信號(hào),resubmitAdjustCh 和 resubmitIntervalCh。運(yùn)行外部修改timer計(jì)時(shí)器的時(shí)鐘。resubmitAdjustCh是根據(jù)歷史情況重新計(jì)算一個(gè)合理的間隔時(shí)間。而resubmitIntervalCh則允許外部,實(shí)時(shí)通過(guò) Miner 實(shí)例方法 SetRecommitInterval 修改間隔時(shí)間。
mainLoop
-
newWorkCh:接收生成新的挖礦任務(wù)信號(hào) -
chainSideCh:接收區(qū)塊鏈中加入了一個(gè)新區(qū)塊作為當(dāng)前鏈頭的旁支的信號(hào) -
txsCh:接收交易池的Pending中新加入了交易事件的信號(hào)
TaskLoop則是提交新的挖礦任務(wù),而resultLoop則是成功出塊之后做的一些處理。
啟動(dòng)挖礦
挖礦的參數(shù)設(shè)置
geth挖礦的參數(shù)設(shè)置定義在 cmd/utils/flags.go 文件中
| 參數(shù) | 默認(rèn)值 | 用途 |
|---|---|---|
| –mine | false | 是否開(kāi)啟自動(dòng)挖礦 |
| –miner.threads | 0 | 挖礦時(shí)可用并行PoW計(jì)算的協(xié)程(輕量級(jí)線程)數(shù)。 兼容過(guò)時(shí)參數(shù) —minerthreads。 |
| –miner.notify | 空 | 挖出新塊時(shí)用于通知遠(yuǎn)程服務(wù)的任意數(shù)量的遠(yuǎn)程服務(wù)地址。 是用 ,分割的多個(gè)遠(yuǎn)程服務(wù)器地址。 如:”http://api.miner.com,http://api2.miner.com“
|
| –miner.noverify | false | 是否禁用區(qū)塊的PoW工作量校驗(yàn)。 |
| –miner.gasprice | 1000000000 wei | 礦工可接受的交易Gas價(jià)格, 低于此GasPrice的交易將被拒絕寫(xiě)入交易池和不會(huì)被礦工打包到區(qū)塊。 |
| –miner.gastarget | 8000000 gas | 動(dòng)態(tài)計(jì)算新區(qū)塊燃料上限(gaslimit)的下限值。 兼容過(guò)時(shí)參數(shù) —targetgaslimit。 |
| –miner.gaslimit | 8000000 gas | 動(dòng)態(tài)技術(shù)新區(qū)塊燃料上限的上限值。 |
| –miner.etherbase | 第一個(gè)賬戶(hù) | 用于接收挖礦獎(jiǎng)勵(lì)的賬戶(hù)地址, 默認(rèn)是本地錢(qián)包中的第一個(gè)賬戶(hù)地址。 |
| –miner.extradata | geth版本號(hào) | 允許礦工自定義寫(xiě)入?yún)^(qū)塊頭的額外數(shù)據(jù)。 |
| –miner.recommit | 3s | 重新開(kāi)始挖掘新區(qū)塊的時(shí)間間隔。 將自動(dòng)放棄進(jìn)行中的挖礦后,重新開(kāi)始一次新區(qū)塊挖礦。 |
常見(jiàn)的啟動(dòng)挖礦的方式
參數(shù)設(shè)置挖礦
dgeth --dev --mine
控制臺(tái)啟動(dòng)挖礦
miner.start(1)
rpc 啟動(dòng)挖礦
這是部署節(jié)點(diǎn)使用的方式,一般設(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
開(kāi)始源碼分析,進(jìn)入到miner.go的New函數(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)
}
}
一開(kāi)始我們初始化的canStart=1 , 如果Downloader模塊正在同步,則canStart=0,并且停止挖礦,如果Downloader模塊Done或者Failed,則canStart=1,且同時(shí)shouldStart=0,miner將啟動(dòng)。
miner.Start(miner.coinbase)
func (miner *Miner) Start(coinbase common.Address) {
...
miner.worker.start()
}
func (w *worker) start() {
...
w.startCh <- struct{}{}
}
接下來(lái)將會(huì)進(jìn)入到mainLoop中去處理startCh:
①:清除過(guò)舊的挖礦任務(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ù),進(jìn)入到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ù)共識(shí)引擎吃初始化header的共識(shí)字段
w.engine.Prepare(w.chain, header);
③:為當(dāng)前挖礦新任務(wù)創(chuàng)建環(huán)境
w.makeCurrent(parent, header)
④:添加叔塊
叔塊集分本地礦工打包區(qū)塊和其他挖礦打包的區(qū)塊。優(yōu)先選擇自己挖出的區(qū)塊。選擇時(shí),將先刪除太舊的區(qū)塊,只從最近的7(staleThreshold)個(gè)高度中選擇,最多選擇兩個(gè)叔塊放入新區(qū)塊中.在真正添加叔塊的同時(shí)會(huì)進(jìn)行校驗(yàn),包括如下:
- 叔塊存在報(bào)錯(cuò)
- 添加的uncle是父塊的兄弟報(bào)錯(cuò)
- 叔塊的父塊未知報(bào)錯(cuò)
commitUncles(w.localUncles)
commitUncles(w.remoteUncles)
⑤:如果noempty為false,則提交空塊,不填充交易進(jìn)入到區(qū)塊中,表示提前挖礦
if !noempty {
w.commit(uncles, nil, false, tstart)
}
⑥:填充交易到新區(qū)塊中
6.1 從交易池中獲取交易,并把交易分為本地交易和遠(yuǎn)程交易,本地交易優(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提交交易
- 首先校驗(yàn)有沒(méi)有可用的
Gas - 如果碰到以下情況要進(jìn)行交易執(zhí)行的中斷
- 新的頭塊事件到達(dá),中斷信號(hào)為 1 (整個(gè)任務(wù)會(huì)被丟棄)
-
worker開(kāi)啟或者重啟,中斷信號(hào)為 1 (整個(gè)任務(wù)會(huì)被丟棄) -
worker重新創(chuàng)建挖礦任務(wù)根據(jù)新的交易,中斷信號(hào)為 2 (任務(wù)還是會(huì)被送入到共識(shí)引擎)
6.3開(kāi)始執(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í)行出錯(cuò),直接回退上一個(gè)快照
if err != nil {
w.current.state.RevertToSnapshot(snap)
return nil, err
}
出錯(cuò)的原因大概有以下幾個(gè):
- 超出當(dāng)前塊的
gas limit -
Nonce太低 -
Nonce太高
執(zhí)行成功的話講交易和收據(jù)存入到w.current中。
⑦:執(zhí)行交易的狀態(tài)更改,并組裝成最終塊
w.commit(uncles, w.fullTaskHook, true, tstart)
執(zhí)行交易的狀態(tài)更改,并組裝成最終塊是由下面的共識(shí)引擎所完成的事情:
block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)
底層會(huì)調(diào)用 state.IntermediateRoot執(zhí)行狀態(tài)更改。組裝成最終塊意味著到這打包任務(wù)完成。接著就是要提交新的挖礦任務(wù)。
提交新的挖礦任務(wù)
①:獲取sealHash(挖礦前的區(qū)塊哈希),重復(fù)提交則跳過(guò)
sealHash := w.engine.SealHash(task.block.Header()) // 返回挖礦前的塊的哈希
if sealHash == prev {
continue
}
②:生成新的挖礦請(qǐng)求,結(jié)果返回到reultCh或者StopCh中
w.engine.Seal(w.chain, task.block, w.resultCh, stopCh);
挖礦的結(jié)果會(huì)返回到resultCh中或者stopCh中,resultCh有數(shù)據(jù)成功出塊,stopCh不為空,則中斷挖礦線程。
成功出塊
resultCh有區(qū)塊數(shù)據(jù),則成功挖出了塊,到最后的成功出塊我們還需要進(jìn)行相應(yīng)的驗(yàn)證判斷。
①:塊為空或者鏈上已經(jīng)有塊或者pendingTasks不存在相關(guān)的sealhash,跳過(guò)處理
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ù)庫(kù)
_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true) // 互斥
④:廣播區(qū)塊并宣布鏈插入事件
w.mux.Post(core.NewMinedBlockEvent{Block: block})
⑤:等待規(guī)范確認(rèn)本地挖出的塊
新區(qū)塊并非立即穩(wěn)定,暫時(shí)存入到未確認(rèn)區(qū)塊集中。
w.unconfirmed.Insert(block.NumberU64(), block.Hash())
總結(jié)&參考
整個(gè)挖礦流程還是比較的簡(jiǎn)單,通過(guò) 4 個(gè)Loop互相工作,從開(kāi)啟挖礦到生成新的挖礦任務(wù)到提交新的挖礦任務(wù)到最后的成功出塊,這里面的共識(shí)處理細(xì)節(jié)不會(huì)提到,接下來(lái)的文章會(huì)說(shuō)到。
https://github.com/blockchainGuide
https://learnblockchain.cn/books/geth/part2/mine/design.html