死磕以太坊源碼分析之挖礦流程分析
基本架構
以太坊挖礦的主要流程是由miner
包負責的,下面是基本的一個架構:
首先外部是通過miner
對象進行了操作,miner
里面則是實用worker
對象來實現挖礦的整體功能。miner決定着是否停止挖礦或者是否可以開始挖礦,同時還可以設置礦工的地址來獲取獎勵。
真正調度處理挖礦相關細節的則是在worker.go里面,我們先來看一張總體的圖。
上圖我們看到有四個循環,分別通過幾個channel
負責不同的事:
newWorkLoop
startCh
:接收startCh
信號,開始挖礦chainHeadCh
:表示接收到新區塊,需要終止當前的挖礦工作,開始新的挖礦。timer.C
:默認每三秒檢查一次是否有新交易需要處理。如果有則需要重新開始挖礦。以便將加高的交易優先打包到區塊中。
在 newWorkLoop
中還有一個輔助信號,resubmitAdjustCh
和 resubmitIntervalCh
。運行外部修改timer計時器的時鍾。resubmitAdjustCh
是根據歷史情況重新計算一個合理的間隔時間。而resubmitIntervalCh
則允許外部,實時通過 Miner
實例方法 SetRecommitInterval
修改間隔時間。
mainLoop
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 --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.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)個高度中選擇,最多選擇兩個叔塊放入新區塊中.在真正添加叔塊的同時會進行校驗,包括如下:
- 叔塊存在報錯
- 添加的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://github.com/blockchainGuide
https://learnblockchain.cn/books/geth/part2/mine/design.html