死磕以太坊原始碼分析之挖礦流程分析
阿新 • • 發佈:2020-12-12
> 死磕以太坊原始碼分析之挖礦流程分析
>
> 程式碼分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9
## 基本架構
以太坊挖礦的主要流程是由`miner`包負責的,下面是基本的一個架構:
![image-20201212125409326](https://tva1.sinaimg.cn/large/0081Kckwgy1glkzcyd6xhj31am0u0qe0.jpg)
首先外部是通過`miner`物件進行了操作,`miner`裡面則是實用`worker`物件來實現挖礦的整體功能。miner決定著是否停止挖礦或者是否可以開始挖礦,同時還可以設定礦工的地址來獲取獎勵。
真正排程處理挖礦相關細節的則是在worker.go裡面,我們先來看一張總體的圖。
![image-20201212201358073](https://tva1.sinaimg.cn/large/0081Kckwgy1gllc2fgu1gj31cw0rwdlc.jpg)
上圖我們看到有四個迴圈,分別通過幾個`channel`負責不同的事:
### newWorkLoop
1. `startCh`:接收`startCh`訊號,開始挖礦
2. `chainHeadCh`:表示接收到新區塊,需要終止當前的挖礦工作,開始新的挖礦。
3. `timer.C`:預設每三秒檢查一次是否有新交易需要處理。如果有則需要重新開始挖礦。以便將加高的交易優先打包到區塊中。
在 `newWorkLoop` 中還有一個輔助訊號,`resubmitAdjustCh` 和 `resubmitIntervalCh`。執行外部修改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“](http://api.miner.com%2Chttp//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`函式中:
```GO
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
}
```
```GO
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將啟動。
```go
miner.Start(miner.coinbase)
```
```go
func (miner *Miner) Start(coinbase common.Address) {
...
miner.worker.start()
}
```
```go
func (w *worker) start() {
...
w.startCh <- struct{}{}
}
```
接下來將會進入到`mainLoop`中去處理`startCh`:
①:清除過舊的挖礦任務
```go
clearPending(w.chain.CurrentBlock().NumberU64())
```
②:提交新的挖礦任務
```go
commit := func(noempty bool, s int32) {
...
w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp}
...
}
```
## 生成新的挖礦任務
根據`newWorkCh`生成新的挖礦任務,進入到`CommitNewWork`中:
①:組裝`header`
```go
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的共識欄位
```GO
w.engine.Prepare(w.chain, header);
```
③:為當前挖礦新任務建立環境
```go
w.makeCurrent(parent, header)
```
④:新增叔塊
叔塊集分本地礦工打包區塊和其他挖礦打包的區塊。優先選擇自己挖出的區塊。選擇時,將先刪除太舊的區塊,只從最近的7(staleThreshold)個高度中選擇,最多**選擇兩個叔塊**放入新區塊中.在真正新增叔塊的同時會進行校驗,包括如下:
- 叔塊存在報錯
- 新增的uncle是父塊的兄弟報錯
- 叔塊的父塊未知報錯
```GO
commitUncles(w.localUncles)
commitUncles(w.remoteUncles)
```
⑤:如果noempty為false,則提交空塊,不填充交易進入到區塊中,表示提前挖礦
```go
if !noempty {
w.commit(uncles, nil, false, tstart)
}
```
⑥:填充交易到新區塊中
6.1 從交易池中獲取交易,並把交易分為本地交易和遠端交易,本地交易優先,先將本地交易提交,再將外部交易提交。
```GO
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
}
}
```
```go
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開始執行交易
```GO
logs, err := w.commitTransaction(tx, coinbase)
```
6.4執行交易獲取收據
```go
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())
```
如果執行出錯,直接回退上一個快照
```go
if err != nil {
w.current.state.RevertToSnapshot(snap)
return nil, err
}
```
出錯的原因大概有以下幾個:
- 超出當前塊的`gas limit`
- `Nonce` 太低
- `Nonce` 太高
執行成功的話講交易和收據存入到`w.current`中。
⑦:執行交易的狀態更改,並組裝成最終塊
```go
w.commit(uncles, w.fullTaskHook, true, tstart)
```
執行交易的狀態更改,並組裝成最終塊是由下面的共識引擎所完成的事情:
```go
block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)
```
底層會呼叫 `state.IntermediateRoot`執行狀態更改。組裝成最終塊意味著到這打包任務完成。接著就是要提交新的挖礦任務。
----
## 提交新的挖礦任務
①:獲取`sealHash`(挖礦前的區塊雜湊),重複提交則跳過
```go
sealHash := w.engine.SealHash(task.block.Header()) // 返回挖礦前的塊的雜湊
if sealHash == prev {
continue
}
```
②:生成新的挖礦請求,結果返回到`reultCh`或者`StopCh`中
```go
w.engine.Seal(w.chain, task.block, w.resultCh, stopCh);
```
挖礦的結果會返回到`resultCh`中或者`stopCh`中,`resultCh`有資料成功出塊,`stopCh`不為空,則中斷挖礦執行緒。
---
## 成功出塊
`resultCh`有區塊資料,則成功挖出了塊,到最後的成功出塊我們還需要進行相應的驗證判斷。
①:塊為空或者鏈上已經有塊或者`pendingTasks`不存在相關的`sealhash`,跳過處理
```go
if block == nil {}
if w.chain.HasBlock(block.Hash(), block.NumberU64()) {}
task, exist := w.pendingTasks[sealhash] if !exist {}
```
②:更新`receipts`
```GO
for i, receipt := range task.receipts {
receipt.BlockHash = hash
...
}
```
③:提交塊和狀態到資料庫
```GO
_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true) // 互斥
```
④:廣播區塊並宣佈鏈插入事件
```GO
w.mux.Post(core.NewMinedBlockEvent{Block: block})
```
⑤:等待規範確認本地挖出的塊
新區塊並非立即穩定,暫時存入到未確認區塊集中。
```GO
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/#%E5%8A%A8%E6%80%81%E8%B0%83%E6%95%B4%E5%87%BA%E5%9D%97%E9%A2%91%