Go-ethereum 源碼解析之 miner/worker.go (下)

Go-ethereum 源碼解析之 miner/worker.go (下)

Appendix D. 詳細批注

1. const

  • resultQueueSize: 指用于監聽驗證結果的通道(worker.resultCh)的緩存大小。這里的驗證結果是已經被簽名了的區塊。
  • txChanSize: 指用于監聽事件 core.NewTxsEvent 的通道(worker.txsCh)的緩存大小。這里的緩存大小引用自事務池的大小。其中,事件 core.NewTxsEvent 是事務列表( []types.Transaction)的封裝器。
  • chainHeadChanSize: 指用于監聽事件 core.ChainHeadEvent 的通道(worker.chainHeadCh)的緩存大小。事件 core.ChainHeadEvent 是區塊(types.Block)的封裝器。
  • chainSideChanSize: 指用于監聽事件 core.ChainSideEvent 的通道(worker.chainSideCh)的緩存大小。事件 core.ChainSideEvent 是區塊(types.Block)的封裝器。
  • resubmitAdjustChanSize: 指用于重新提交間隔調整的通道(worker.resubmitAdjustCh)的緩存大小。 緩存的消息結構為 intervalAdjust,用于描述下一次提交間隔的調整因數。
  • miningLogAtDepth: 指記錄成功挖礦時需要達到的確認數。是 miner.unconfirmedBlocks 的深度 。即本地節點挖出的最新區塊如果需要得到整個網絡的確認,需要整個網絡再挖出 miningLogAtDepth 個區塊。舉個例子:本地節點挖出了編號為 1 的區塊,需要等到整個網絡中某個節點(也可以是本地節點)挖出編號為 8 的區塊(8 = 1 + miningLogAtDepth, miningLogAtDepth = 7)之后,則編號為 1 的區塊就成為了經典鏈的一部分。
  • minRecommitInterval: 指使用任何新到達的事務重新創建挖礦區塊的最小時間間隔。當用戶設定的重新提交間隔太小時進行修正。
  • maxRecommitInterval: 指使用任何新到達的事務重新創建挖礦區塊的最大時間間隔。當用戶設定的重新提交間隔太大時進行修正。
  • intervalAdjustRatio: 指單個間隔調整對驗證工作重新提交間隔的影響因子。與參數 intervalAdjustBias 一起決定下一次提交間隔。
  • intervalAdjustBias: 指在新的重新提交間隔計算期間應用intervalAdjustBias,有利于增加上限或減少下限,以便可以訪問限制。與參數 intervalAdjustRatio 一起決定下一次提交間隔。
  • staleThreshold: 指可接受的舊區塊的最大深度。注意,目前,這個值與 miningLogAtDepth 都是 7,且表達的意思也基本差不多,是不是有一定的內存聯系。

2. type environment struct

數據結構 environment 描述了 worker 的當前環境,并且包含所有的當前狀態信息。

最主要的狀態信息有:簽名者(即本地節點的礦工)、狀態樹(主要是記錄賬戶余額等狀態?)、緩存的祖先區塊、緩存的叔區塊、當前周期內的事務數量、當前打包中區塊的區塊頭、事務列表(用于構建當前打包中區塊)、收據列表(用于和事務列表一一對應,構建當前打包中區塊)。

  • signer types.Signer: 簽名者,即本地節點的礦工,用于對區塊進行簽名。

  • state *state.StateDB: 狀態樹,用于描述賬戶相關的狀態改變,merkle trie 數據結構。可以在此修改本節節點的狀態信息。

  • ancestors mapset.Set: ??? ancestors 區塊集合(用于檢查叔區塊的有效性)。緩存。緩存數據結構中往往存的是區塊的哈希。可以簡單地認為區塊、區塊頭、區塊哈希、區塊頭哈希能夠等價地描述區塊,其中的任何一種方式都能惟一標識同一個區塊。甚至可以放寬到區塊編號。

  • family mapset.Set: ??? family 區塊集合(用于驗證無效叔區塊)。family 區塊集合比 ancestors 區塊集合多了各祖先區塊的叔區塊。ancestors 區塊集合是區塊的直接父區塊一級一級連接起來的。

  • uncles mapset.Set: 叔區塊集合,即當前區塊的叔區塊集合,或者說當前正在挖的區塊的叔區塊集合。

  • tcount int: 一個周期里面的事務數量

  • gasPool *core.GasPool: 用于打包事務的可用 gas

  • header *types.Header: 區塊頭。區塊頭需要滿足通用的以太坊協議共識,還需要滿足特定的 PoA 共識協議。與 PoA 共識協議相關的區塊頭 types.Header 字段用 Clique.Prepare() 方法進行主要的設置,Clique.Finalize() 方法進行最終的補充設置。那么以太坊協議共識相關的字段在哪里設置?或者說在 worker 的哪個方法中設置。

  • txs []*types.Transaction: 事務(types.Transaction)列表。當前需要打包的事務列表(或者備選事務列表),可不可以理解為事務池。

  • receipts []*types.Receipt: 收據(types.Receipt)列表。Receipt 表示 Transaction 一一對應的結果。

3. type task struct

數據結構 task 包含共識引擎簽名和簽名之后的結果提交的所有信息。

簽名即對已經組裝好的區塊添加最后的簽名信息。添加了簽名的區塊即為最終的結果區塊,即簽名區塊或待確認區塊。

數據結構 task 和數據結構 environment 的區別:

  • 數據結構 environment 用于 worker 的所有操作

  • 數據結構 task 僅用于 worker 的簽名相關操作

  • receipts []*types.Receipt: 收據(types.Receipt)列表

  • state *state.StateDB: 狀態樹,用于描述賬戶相關的狀態改變,merkle trie 數據結構。可以在此修改本節節點的狀態信息。

  • block *types.Block: 待簽名的區塊。此時,區塊已經全部組裝好了,包信了事務列表、叔區塊列表。同時,區塊頭中的字段已經全部組裝好了,就差最后的簽名。簽名后的區塊是在此原有區塊上新創建的區塊,并被發送到結果通道,用于驅動本地節點已經挖出新區塊之后的流程。

  • createdAt time.Time: task 的創建時間

數據結構 task 也是通道 worker.taskCh 發送或接收的消息。

4. const

  • commitInterruptNone 無效的中斷值
  • commitInterruptNewHead 用于描述新區塊頭到達的中斷值,當 worker 啟動或重新啟動時也是這個中斷值。
  • commitInterruptResubmit 用于描述 worker 根據接收到的新事務,中止之前挖礦,并重新開始挖礦的中斷值。

5. type newWorkReq struct

數據結構 newWorkReq 表示使用相應的中斷值通知程序提交新簽名工作的請求。

數據結構 newWorkReq 也是通道 worker.newWorkCh 發送或接收的消息。

  • interrupt *int32: 具體的中斷值,為 commitInterruptNewHead 或 commitInterruptResubmit 之一。
  • noempty bool: ??? 表示創建的區塊是否包含事務?
  • timestamp int64: ??? 表示區塊開始組裝的時間?

6. type intervalAdjust struct

數據結構 intervalAdjust 表示重新提交間隔調整。

  • ratio float64: 間隔調整的比例
  • inc bool: 是上調還是下調

在當前區塊時計算下一區塊的出塊大致時間,在基本的時間間隔之上進行一定的微調,微調的參數就是用數據結構 intervalAdjust 描述的,并發送給對應的通道 resubmitAdjustCh。下一個區塊在打包時從通道 resubmitAdjustCh 中獲取其對應的微調參數 intervalAdjust 實行微調。

7. type worker struct

worker 是負責向共識引擎提交新工作并且收集簽名結果的主要對象。

共識引擎會做哪些工作呢?

  • 通過方法 Clique.Prepare() 設置區塊頭中關于 PoA 共識的相關字段。
  • 通過方法 Clique.Finalize() 組裝可以被簽名的區塊。
  • 通過方法 Clieque.Seal() 對區塊進行簽名,并發送給結果通道 worker.resultsCh。
  • 通過方法 Clique.snapshot() 處理兩種快照:檢查點快照和投票快照。

那么共識引擎需要哪些輸入呢?

  • 區塊頭
  • 事務列表
  • 收據列表
  • 狀態樹
  • 叔區塊列表(PoA 共識協議中肯定為 nil)
  • 區塊,是個抽象概念,主要包含:區塊頭、事務列表、叔區塊列表,但是并不包含收據列表。

那么共識引擎會產生哪些輸出呢?

  • 方法 Clieque.Seal() 會將最終簽名后的區塊發送給結果通道 worker.resultsCh。

  • config *params.ChainConfig: 區塊鏈的鏈配置信息,包含鏈 ID,是 ethash 還是 clique 共識協議等

  • engine consensus.Engine: 共識引擎接口

  • eth Backend: 后端,包含區塊鏈和事務池,提供挖礦所需的所有方法

  • chain *core.BlockChain: 表示整個區塊鏈。這不和 eth 中的區塊鏈是同一個?

  • gasFloor uint64: 最低 gas

  • gasCeil uint64: 最高 gas

// 訂閱

  • mux *event.TypeMux: 可以簡單地理解為事件的訂閱管理器,即注冊事件的響應函數,和驅動事件的響應函數。
  • txsCh chan core.NewTxsEvent: 用于在不同協程之間交互事件 core.NewTxsEvent 的通道。事件 core.NewTxsEvent 是事務列表 []*types.Transaction 的封裝器,即通道 txsCh 用于在不同協程之間交互事務列表。命名協程 worker.mainLoop() 從通道 txsCh 接收事件 core.NewTxsEvent,即事務列表。使用通道 txsCh 作為只接收消息的通道向 core.TxPool 訂閱事件 core.NewTxsEvent,那么應該是從 core.TxPool 發送事件 core.NewTxsEvent 到通道 txsCh。
  • txsSub event.Subscription: 向事務池(core.TxPool)訂閱事件 core.NewTxsEvent,并使用通道 txsCh 作為此次訂閱接收消息的通道。代碼為 worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)。
  • chainHeadCh chan core.ChainHeadEvent: 用于在不同協程之間交互事件 core.ChainHeadEvent 的通道。事件 core.ChainHeadEvent 是區塊 types.Block 的封裝器,即通道 chainHeadCh 用于不同協程之間交互新挖出的區塊頭。命名協程 worker.newWorkLoop() 從通道 chainHeadCh 接收事件 core.ChainHeadEvent,即新的區塊頭。使用通道 chainHeadCh 作為只接收消息的通道向 core.BlockChain 訂閱事件 core.ChainHeadEvent,那么應該是從 core.BlockChain 發送事件 core.ChainHeadEvent 到通道 chainHeadCh。
  • chainHeadSub event.Subscription: 向區塊鏈(core.BlockChain)訂閱事件 core.ChainHeadEvent,并使用通道 chainHeadCh 作為此次訂閱接收消息的通道。代碼為 worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
  • chainSideCh chan core.ChainSideEvent: 用于在不同協程之間交互事件 core.ChainSideEvent 的通道。事件 core.ChainSideEvent 是區塊 types.Block 的封裝器,即通道 chainSideCh 用于不同協程之間交互新挖出的區塊頭。命名協程 worker.mainLoop() 從通道 chainSideCh 接收事件 core.ChainSideEvent,即新的叔區塊頭(但 PoA 不是不存在叔區塊?)。使用通道 chainSideCh 作為只接收消息的通道向 core.BlockChain 訂閱事件 core.ChainSideEvent,那么應該是從 core.BlockChain 發送事件 core.ChainSideEvent 到通道 chainSideCh。
  • chainSideSub event.Subscription: 向區塊鏈(core.BlockChain)訂閱事件 core.ChainSideEvent,并使用通道 chainSideCh 作為此次訂閱接收消息的通道。代碼為 worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)

// 通道

  • newWorkCh chan *newWorkReq: 通道 newWorkCh 用于在不同協程之間交互消息 newWorkReq 的通道。命名協程 worker.newWorkLoop() 將消息 newWorkReq 發送給通道 newWorkCh。命名協程 worker.mainLoop() 從通道 newWorkCh 中接收消息 newWorkReq。

  • taskCh chan *task: 通道 taskCh 用于在不同協程之間交互消息 task 的通道。(1)命名協程 worker.taskLoop() 從通道 taskCh 中接收消息 task。對接收到的消息 task 先存入待處理 map 中,其中 Key 為 task 中的區塊簽名哈希,Value 為 task。同時,將 task 中的區塊傳遞給共識引擎的簽名方法 w.engine.Seal() 進行簽名,同時將結果通道 w.resultCh 和退出通道 stopCh 也傳遞給共識引擎的簽名方法,以便從中接收簽名之后的區塊或者接收中止消息。(2)命名協程 worker.mainLoop() 中的方法 worker.commit() 將消息 task 發送給通道 taskCh。此方法先將當前環境中的區塊頭(w.current.header)、事務列表(w.current.txs)、收據列表(w.current.receipts)作為參數傳遞給共識引擎的方法 Finalize() 組裝出待簽名的區塊,代碼為 block = w.engine.Finalize(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)。需要注意的是,區塊 types.Block 中只包含區塊頭 types.Header、事務列表 []types.Transaction、叔區塊列表 []types.Header,并不包含收據列表 []types.Receipt,但是區塊頭 types.Header 中的字段 ReceiptHash 是收據列表樹的根哈希,所以也需要收據列表參數。將組裝后的待簽名區塊 types.Block,及前面解釋過的收據列表 []types.Receipt 等其它參數一起構建出新的任務 task 發送給通道 taskCh,同時輸出一條重要的日志信息:log.Info("Commit new mining work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(uncles), "txs", w.current.tcount, "gas", block.GasUsed(), "fees", feesEth, "elapsed", common.PrettyDuration(time.Since(start)))。到方法 commit() 這一步,已經組裝出了新的任務 task,并將此新任務 task 通過通道 taskCh 發送給命名協程 worker.taskLoop()。

  • resultCh chan *types.Block: 通道 resultCh 用于在不同協程之間交互消息 types.Block。(1)命名協程 worker.resultLoop() 從通道 resultCh 中接收消息 types.Block,且此區塊是被簽名過的。對于新接收到簽名區塊,首先判斷這個簽名區塊是否為重復的;其次,需要從待處理任務映射 w.pendingTasks 中獲得對應區塊簽名哈希的任務 task,如果沒找到則輸出一條重要的日志信息:log.Error("Block found but no relative pending task", "number", block.Number(), "sealhash", sealhash, "hash", hash)。并從 task 中恢復 receipts 和 logs。第三,將簽名區塊及其對應的收據列表和狀態樹等信息寫入數據庫。如果寫入失敗,則輸出一條重要的日志信息:log.Error("Failed writing block to chain", "err", err),否則輸出一條重要的日志信息:log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash, "elapsed", common.PrettyDuration(time.Since(task.createdAt)))。第四,通過新挖出的簽名區塊構建事件 core.NewMinedBlockEvent,并通過事件訂閱管理器中的方法 w.mux.Post() 將本地節點最新簽名的區塊向網絡中其它節點進行廣播,這是基于 p2p 模塊完成的。第五,同時構建事件 core.ChainEvent 和事件 core.ChainHeadEvent,或者構建事件 core.ChainSideEvent,并通過區塊鏈中的方法 w.chain.PostChainEvents() 進行廣播。需要注意的時,此廣播只是針對向本地節點進行了事件注冊的客戶端,且是通過 JSON-RPC 完成,和第四步中的向網絡中其它節點通過 p2p 進行廣播是完全不同的。這一部的廣播即使沒有事件接收方也沒有問題,因為這是業務邏輯層面的,而第四步中的廣播則是必須有接收方的,否則就會破壞以太坊協議本身。比如:我們可以注冊一個事件,用于監控是否有最新的區塊被挖出來,然后在此基礎上,查詢指定賬戶的最新余額。第六步,將新挖出來的簽名區塊,添加進待確認隊列中,代碼為:w.unconfirmed.Insert(block.NumberU64(), block.Hash())。(2)共識引擎中的簽名方法 Clique.Seal() 通過匿名協程將簽名后的簽名區塊 types.Block 發送到通道 resultCh。

  • startCh chan struct{}: 通道 startCh 用于在不同協程之間交互消息 struct{}。可以發現,消息 struct {} 沒有包含任何有意義的信息,這在 Go 中是一類特別重要的寫法,用于由某個協程向另一個協程發送開始或中止消息。(1)函數 newWorker() 向通道 startCh 發送消息 struct{},其中函數 newWorker() 應該是運行在主協程中或由其它某個包中的協程啟動。代碼為:worker.startCh <- struct{}{}。(2)方法 worker.start() 向通道 startCh 發送消息 struct{},其它同(1)。(3)命名協程 worker.newWorkLoop() 從通道 startCh 中接收消息 struct{}。需要注意的是,(1)和(2)都可以向通道 startCh 發送消息 struct{} 驅動命名協程 worker.newWorkLoop() 中邏輯。方法 worker.start() 表明 worker 是可以先停止的,而不關閉,之后可以重新啟動。

  • exitCh chan struct{}: 通道 exitCh 用于在不同協程之間交互消息 struct{}。可以參考通道 startCh 中的注釋。(1)函數 worker.close() 通過調用函數 close(w.exitCh) 整個關閉通道 exitCh。(2)命名協程 worker.newWorkLoop() 從通道 exitCh 中接收消息,從而結束整個協程。(3)命名協程 worker.mainLoop() 從通道 exitCh 中接收消息,從而結束整個協程。(4)命名協程 worker.taskLoop() 從通道 exitCh 中接收消息,從而結束整個協程。(5)命名協程 worker.resultLoop() 從通道 exitCh 中接收消息,從而結束整個協程。(6)命名協程 worker.mainLoop() 調用的方法 worker.commit() 從通道 exitCh 中接收消息,從而放棄后續的工作。

  • resubmitIntervalCh chan time.Duration: 通道 resubmitIntervalCh 用于在不同的協程之間交互消息 time.Duration。time.Duration 是 Go 語言標準庫中的類型,在這里通道 resubmitIntervalCh 起到一個定時器的作用,這也是 Go 語言中關于定時器的標準實現方式。(1)方法 worker.setRecommitInterval() 向通道 resubmitIntervalCh 發送消息 time.Duration,即設置定時器下一次觸發的時間。方法 worker.setRecommitInterval() 在方法 Miner.SetRecommitInterval() 中被調用,方法 Miner.SetRecommitInterval() 又在方法 PrivateMinerAPI.SetRecommitInterval() 中調用,這應該是從外部通過 JSON-RPC 接口驅動的。(2)命名協程 worker.newWorkLoop() 從通道 resubmitIntervalCh 中接收消息 time.Duration,即獲得希望定時器下一次觸發的時間,并根據需要對這個時間進行一定的修正。

  • resubmitAdjustCh chan *intervalAdjust: 通道 resubmitAdjustCh 用于在不同的協程之間交互消息 intervalAdjust。(1)命名協程 worker.newWorkLoop() 從通道 resubmitAdjustCh 中接收消息 intervalAdjust。(2)方法 worker.commitTransactions() 向通道 resubmitAdjustCh 中發送消息 intervalAdjust。通道 resubmitAdjustCh 與通道 resubmitIntervalCh 的作用類似,都是修改下一個區塊的出塊時間。只不過通道 resubmitAdjustCh 中交互的消息 time.Duration 是由外部通過 JSON-RPC 接口來設定的,而通道 resubmitIntervalCh 中交互的消息 intervalAdjust 是礦工根據上一個區塊的出塊時間基于算法自定調整的。

  • current *environment: 描述了 worker 的當前環境和狀態信息。具體的請參考對數據結構 environment 的注釋。

  • possibleUncles map[common.Hash]*types.Block: 可能的叔區塊集合。Key 為區塊哈希 common.Hash,Value 為區塊 types.Block。

  • unconfirmed *unconfirmedBlocks: 本地節點最近新挖出的區塊集合,用于等待網絡中其它節點的確認,從而成為經典鏈的一部分。具體的可以參考對數據結構 unconfirmedBlocks 的注釋。

  • mu sync.RWMutex: 鎖,用于保護字段 coinbase 和 extra。

  • coinbase common.Address: 礦工地址。

  • extra []byte: 分為三段:前 32 字節礦工可隨意填寫,最后 65 字節為對區塊頭的簽名,中間的字節為授權簽名者列表的有序列連接,且字節數為 20 的倍數。

  • pendingMu sync.RWMutex: 鎖,用于保護字段 pendingTasks。

  • pendingTasks map[common.Hash]*task: 待處理的任務映射,其中:Key 為 task 中包含的區塊的哈希值,Value 為 task。

  • snapshotMu sync.RWMutex: 鎖,用于保護字段 snapshotBlock 和 snapshotState。

  • snapshotBlock *types.Block: 區塊的快照。

  • snapshotState *state.StateDB: 狀態的快照。

// 原子狀態的計數器

  • running int32: 用于表示共識引擎是否正在運行。
  • newTxs int32: 自從上次簽名工作提交之后新到達的事務數量。上次簽名工作即指 worker 中已經通過調用共識引擎的 Finalize() 方法組裝好了待簽名的區塊,然后通過調用共識引擎的簽名方法 Clique.Seal() 對待簽名區塊進行簽名。即在上一個區塊被本地節點挖出之后,新來的事務數量。

// Test hooks

  • newTaskHook func(*task): 接收到新簽名任務時調用此方法。
  • skipSealHook func(*task) bool: 判定是否跳過簽名時調用 此方法。
  • fullTaskHook func(): 在推送完整簽名任務之前調用此方法。
  • resubmitHook func(time.Duration, time.Duration): 更新重新提交間隔時調用此方法。

(1) func newWorker(config *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, recommit time.Duration, gasFloor, gasCeil uint64) *worker

構造函數 newWorker() 用于根據給定參數構建 worker。

主要參數:

  • config *params.ChainConfig: 鏈的配置信息
  • engine consensus.Engine: 共識引擎
  • eth Backend: 以太坊本地節點的后端
  • mux *event.TypeMux: 事件訂閱管理器
  • recommit time.Duration: 下一次任務的基礎時間間隔
  • gasFloor, gasCeil uint64: Gas 的下限 gasFloor 和上限 gasCeil。

主要實現:

  • 首先構建對象 worker,并設定大部分字段的初始值。

  • 向事務池 core.TxPool 訂閱事件 core.NewTxsEvent,并通過通道 worker.txsCh 接收事件 core.NewTxsEvent。

    • worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)
  • 向區塊鏈 core.BlockChain 訂閱事件 core.ChainHeadEvent,并通過通道 worker.chainHeadCh 接收事件 core.ChainHeadEvent。

    • worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
  • 向區塊鏈 core.BlockChain 訂閱事件 core.ChainSideEvent,并通過通道 worker.chainSideCh 接收事件 worker.ChainSideEvent。

    • worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)
  • 如果用戶設定的重新提交間隔 recommit 太短,則重新設定 recommit = minRecommitInterval。同時,輸出日志信息:log.Warn("Sanitizing miner recommit interval", "provided", recommit, "updated", minRecommitInterval)

  • 啟動新的獨立協程運行方法 worker.mainLoop()。

  • 啟動新的獨立協程運行方法 worker.newWorkLoop(recommit)。

  • 啟動新的獨立協程運行方法 worker.resultLoop()。

  • 啟動新的獨立協程運行方法 worker.taskLoop()。

  • 提交第一個工作以初始化待處理狀態。即給通道 startCh 發送消息。

    • worker.startCh <- struct{}{}

(2) func (w *worker) setEtherbase(addr common.Address)

方法 setEtherbase() 設置用于初始化區塊 coinbase 字段的 etherbase。

參數:

  • addr common.Address: 地址

主要實現:

  • 加鎖和解鎖
  • w.coinbase = addr

(3) func (w *worker) setExtra(extra []byte)

方法 setExtra() 設置用于初始化區塊額外字段的內容。

參數:

  • extra []byte: 應該是用于區塊頭 types.Header 中的字段 Extra 的前 32 字節。這 32 字節是以太坊協議規定在區塊中用于存儲礦工相關的一些額外信息。上層調用方法 miner.Miner.SetExtra(),繼續上層調用方法為 eth.Ethereum 的構造函數 eth.New() 中的代碼 eth.miner.SetExtra(makeExtraData(config.MinerExtraData))。這個參數最終是通過 geth 的 MINER OPTIONS 命令行參數 --extradata,或者 ETHEREUM OPTIONS 的命令行參數 --config,這是一個 TOML 配置文件。

(4) func (w *worker) setRecommitInterval(interval time.Duration)

方法 setRecommitInterval() 更新礦工簽名工作重新提交的間隔。

參數:

  • interval time.Duration: 重新提交的時間間隔。

主要實現:

  • 將重新提交的間隔 interval 發送到通道 worker.resubmitIntervalCh,代碼為:w.resubmitIntervalCh <- interval。命名協程 worker.newWorkLoop() 會從通道 worker.resubmitIntervalCh 中接收此消息。

(5) func (w worker) pending() (types.Block, *state.StateDB)

方法 pending() 返回待處理的狀態和相應的區塊。

主要實現:

  • 加鎖、解鎖 snapshotMu。
  • 返回字段 snapshotBlock 和字段 snapshotState 的副本。

(6) func (w *worker) pendingBlock() *types.Block

方法 pendingBlock() 返回待處理的區塊。

主要實現:

  • 加鎖、解鎖 snapshotMu。
  • 返回字段 snapshotBlock。

(7) func (w *worker) start()

方法 start() 采用原子操作將 running 字段置為 1,并觸發新工作的提交。

主要實現:

  • atomic.StoreInt32(&w.running, 1)
  • w.startCh <- struct{}{}

(8) func (w *worker) stop()

方法 stop() 采用原子操作將 running 字段置為 0。

主要實現:

  • atomic.StoreInt32(&w.running, 0)

(9) func (w *worker) isRunning() bool

方法 isRunning() 返回 worker 是否正在運行的指示符。

主要實現:

  • return atomic.LoadInt32(&w.running) == 1

(10) func (w *worker) close()

方法 close() 終止由 worker 維護的所有后臺線程。注意 worker 不支持被關閉多次,這是由 Go 語言不允許多次關閉同一個通道決定的。

主要實現

  • close(w.exitCh)

(11) func (w *worker) newWorkLoop(recommit time.Duration)

方法 newWorkLoop() 是一個獨立的協程,基于接收到的事件提交新的挖礦工作。不妨將此協程稱作命名協程 worker.newWorkLoop()。

參數:

  • recommit time.Duration: 下一次提交間隔。

主要實現:

  • 定義了三個變量:
    • interrupt *int32: 中斷信號
    • minRecommit = recommit: 用戶指定的最小重新提交間隔
    • timestamp int64: 每輪挖礦的時間戳
  • 定義一個定時器,并丟棄初始的 tick
    • timer := time.NewTimer(0)
    • <-timer.C
  • 定義內部提交函數 commit()
    • 提交函數 commit() 使用給定信號中止正在進行的交易執行,并重新提交新信號。
    • 構建新工作請求 newWorkReq,并發送給通道 newWorkCh 來驅動命名協程 worker.mainLoop() 來重新提交任務。
    • 設置定時器 timer 的下一次時間。代碼為:timer.Reset(recommit)
    • 重置交易計數器。代碼為:atomic.StoreInt32(&w.newTxs, 0)
  • 定義內部函數 recalcRecommit()
    • 根據一套規則來計算重新提交間隔 recommit。
    • 具體規則后續補充注釋。
  • 定義內部函數 clearPending()
    • 此函數用于清除過期的待處理任務。
    • 參數
      • number uint64: 區塊編號
    • 加鎖 w.pendingMu.Lock()
    • 循環迭代 w.pendingTasks
      • 區塊簽名哈希 h
      • 任務 t
      • 如果 t 中的區塊編號比 number 要早 staleThreshold 個區塊,則將其從 w.pendingTasks 中刪除。
    • 解鎖 w.pendingMu.Unlock()
  • 在 for 循環中持續從通道 startCh、timer.C、resubmitIntervalCh、resubmitAdjustCh 和 exitCh 中接收消息,并執行相應的邏輯。
    • startCh:
      • 調用內部函數 clearPending() 清除鏈上當前區塊之前的過期待處理任務。
      • 調用內部函數 commit(false, commitInterruptNewHead) 提交新的 newWorkReq。
    • chainHeadCh:
      • 從通道 chainHeadCh 接收消息 head(事件 core.ChainHeadEvent)
      • 調用內部函數 clearPending() 清除 core.ChainHeadEvent 中區塊之前的過期待處理任務。
      • 調用內部函數 commit(false, commitInterruptNewHead) 提交新的 newWorkReq。
    • timer.C
      • 如果挖礦正在進行中,則定期重新提交新的工作周期以提取更高價格的交易。禁用待處理區塊的此開銷。
      • 如果交易計數器 w.newTxs 為 0
        • 重置定時器。代碼為:timer.Reset(recommit)
        • 退出本輪迭代。
      • 調用內部函數 commit(false, commitInterruptResubmit) 提交新的 newWorkReq。
    • timer.C:
      • 如果挖礦正在進行中,則定期重新提交新的工作周期以便更新到價格較高的交易。對于待處理中的區塊禁用此操作開銷。
        • 對于 poa 共識引擎,需要其配置的 Clique.Period > 0。!!!等于這里對于共識算法有個特殊處理。
      • 調用內部函數 commit(true, commitInterruptResubmit) 提交新的 newWorkReq。
      • 【批注 1】,這里用到了 time.Timer 將定時器,時間間隔為 recommit。
      • 【批注 2】,通道主要的作用是用于協程之間交互消息,那么實際上影響到的就是工作流程。這個定時器應該主要就是挖礦有周期性的概念,比如 15 秒產生一個塊。存在兩個定時間隔,一個是靜態配置的,另一個是由挖礦動態決定的。當挖礦的實際時間長于靜態設定的,那么可能需要做一些操作,比如重新挖礦等等吧。當挖礦的實際時間適于靜態設定的,可能不需要做什么操作。
    • resubmitIntervalCh:
      • 支持由用戶來重新設定重新提交的間隔。
      • 用戶設定的值不能小于 minRecommitInterval。
      • 如果回調函數 resubmitHook 不空,則調用。
    • resubmitAdjustCh:
      • 根據挖礦的反饋來動態地調整重新提交的間隔。
      • 如果回調函數 resubmitHook 不空,則調用。
    • exitCh:
      • 接收到退出消息,退出整個協程。

命名協程 worker.mainLoop() 用于根據接收到的事件生成簽名任務,命名協程 worker.taskLoop() 用于接收上述驗證任務并提交給共識引擎,命名協程 worker.resultLoop() 用于處理簽名結果的提交并更新相關數據到數據庫中。

(12) func (w *worker) mainLoop()

方法 mainLoop() 是一個獨立的協程,用于根據接收到的事件重新生成簽名任務。不妨將此協程稱作命名協程 worker.mainLoop()。

主要實現:

  • 在整個協程退出時,取消 txsSub、chainHeadSub、chainSideSub 這三個訂閱。
    • defer w.txsSub.Unsubscribe()
    • defer w.chainHeadSub.Unsubscribe()
    • defer w.chainSideSub.Unsubscribe()
  • 在 for 循環中持續從通道 newWorkCh、chainSideCh、txCh 和 exitCh 中接收消息,并執行相應的邏輯。
    • newWorkCh:
      • 根據新接收到的消息 req(數據結構為 newWorkReq),調用函數 commitNewWork() 提交新的任務。代碼為:w.commitNewWork(req.interrupt, req.noempty, req.timestamp)。需要說明的,雖然方法 commitNewWork() 中的參數沒有包含任何區塊、交易等信息,但這些信息都包含在當前環境 w.current 或 w 中。同時,任務最終通過通道 worker.taskCh 提交給命名協程 worker.taskLoop()。
    • chainSideCh:
      • 接收到新的消息 ev(事件 ChainSideEvent)
        • 如果 ev 中攜帶的區塊已經在 possibleUncles 中,則退出本輪迭代。
      • 把 ev 攜帶的區塊添加到 possibleUncles中。代碼為:w.possibleUncles[ev.Block.Hash()] = ev.Block。
      • 如果正在挖礦中的區塊所包含的叔區塊少于 2 個,且 ev 中攜帶的新叔區塊有效,則重新生成挖礦中的任務。見代碼:if w.isRunning() && w.current != nil && w.current.uncles.Cardinality() < 2
        • 獲取任務開始時間 start。代碼為:start := time.Now()
        • 通過方法 commitUncle() 將 ev 中攜帶的區塊添加到 current.uncles 中。如果成功
          • 定義新任務中所需要的區塊頭列表 uncles。代碼為:var uncles []*types.Header
          • 遍歷 w.current.uncles 中的每個 uncle hash
          • 從 possibleUncles 中找到 uncle hash 對應的區塊頭,并添加到 uncles 中。代碼為:uncles = append(uncles, uncle.Header())
          • 并根據最終獲得的所有叔區塊頭列表 uncles 來調用方法 commit() 提交最終區塊。代碼為:w.commit(uncles, nil, true, start)
          • 【批注 1】:possibleUncles 用于包含可能的叔區塊,起到一個緩沖的作用。 current.uncles 是當前要打包的區塊中已經被確認的叔區塊。
          • 【批注 2】:possibleUncles 是<區塊頭哈希>區塊構成的 map,current.uncles 則僅包含了區塊頭哈希。
    • txsCh:根據新接收到的消息 ev(事件 core.NewTxsEvent)
      • 如果不在挖礦狀態,則將交易置于待處理狀態。
      • 注意,收到的所有交易可能與已包含在當前挖礦區塊中的交易不連續。這些交易將自動消除。
      • if !w.isRunning() && w.current != nil
        • 加鎖、解鎖的方式獲取礦工地址 coinbase。代碼為:coinbase := w.coinbase
        • 定義變量 txs。代碼為:txs := make(map[common.Address]types.Transactions)
        • 遍歷消息 ev 中攜帶的交易列表,對于每個交易 tx
          • 還原出每個交易 tx 的發送者地址 acc
          • 更新映射 txs。代碼為:txs[acc] = append(txs[acc], tx)
        • 將 txs 轉換為 txset(數據結構為 types.TransactionsByPriceAndNonce),代碼為:txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs)
        • 提交交易列表 txset。代碼為:w.commitTransactions(txset, coinbase, nil)
        • 更新快照。代碼為:w.updateSnapshot()
      • else
        • 如果我們正在挖礦中,但沒有正在處理任何事情,請在新交易中醒來
        • if w.config.Clique != nil && w.config.Clique.Period == 0
          • w.commitNewWork(nil, false, time.Now().Unix())
      • 采用原子操作將 w.newTxs 的數量增加新接收到的事務數量。代碼為:atomic.AddInt32(&w.newTxs, int32(len(ev.Txs)))
    • w.exitCh
      • 當從退出通道接收到消息時,結束整個協程。
    • w.txsSub.Err()
      • 當從交易訂閱通道接收到錯誤消息時,結束整個協程。
    • w.chainHeadSub.Err()
      • 當從區塊頭訂閱通道接收到錯誤消息時,結束整個協程。
    • w.chainSideSub.Err()
      • 當從側鏈區塊頭訂閱通道接收到錯誤消息時,結束整個協程。

(13) func (w *worker) taskLoop()

方法 taskLoop() 是一個獨立的協程,用于從生成器中獲取待簽名任務,并將它們提交給共識引擎。不妨將此協程稱作命名協程 worker.taskLoop()。

主要實現:

  • 定義兩個變量:退出通道 stopCh 和上一個區塊哈希 prev
    • stopCh chan struct{}
    • prev common.Hash
  • 定義局部中斷函數 interrupt(),用于關閉退出通道 stopCh,結束所有從退出通道 stopCh 接收消息的協程,這里共識引擎方法 Seal() 中用于簽名的獨立匿名協程,退出通道 stopCh 是作為參數傳遞過去的。
    • close(stopCh)
  • 局部通道 stopCh 和內部函數 interrupt() 用于組合終止進行中的簽名任務(in-flight sealing task)。
  • 在 for 循環中持續從通道 taskCh 和 exitCh 中接收消息,并執行相應的邏輯。
    • taskCh:
      • 接收新任務 task
      • 如果回調 w.newTaskHook != nil,則調用回調函數 w.newTaskHook(task)
      • 獲取任務 task 中包含區塊的區塊簽名哈希 sealHash
      • 如果 sealHash == prev,則退出本輪迭代。
        • 過濾掉因重復提交產生的重復的簽名任務
      • 調用中斷函數 interrupt() 中止共識引擎方法 Seal() 中正在簽名的獨立匿名協程。這里是通過關閉退出通道 stopCh 實現的。
      • 給退出通道 stopCh 分配空間,并設置上一個區塊哈希 prev。
        • stopCh, prev = make(chan struct{}), sealHash
      • 如果回調函數 w.skipSealHook() 不為 nil 和 w.skipSealHook(task) 返回 true,則退出本輪迭代。
      • 通過對鎖 w.pendingMu 執行加鎖、解鎖,將任務 task 添加到 w.pendingTasks 中,為之后命名協程 worker.resultLoop() 中接收到已簽名區塊,查找包含該區塊的任務 task 而用。
      • 將任務 task 中包含的區塊提交給共識引擎進行簽名。代碼為:w.engine.Seal(w.chain, task.block, w.resultCh, stopCh)
        • 需要特別注意傳遞的兩個通道參數 w.resutlCh, stopCh
        • 通道 w.resultCh 用于從共識引擎的簽名方法 Seal() 中接收已簽名區塊。
        • 通道 stopCh 用于發送中止信號給共識引擎的簽名方法 Seal(),從而中止共識引擎正在進行的簽名操作。
        • 如果簽名失敗,則輸出日志信息:log.Warn("Block sealing failed", "err", err)
    • exitCh:
      • 當接收到退出消息時
        • 通過調用內部中斷函數 interrupt() 關閉中止通道 stopCh,從而使得共識引擎的簽名方法 Seal() 放棄本次簽名。
        • 退出整個協程。

(14) func (w *worker) resultLoop()

方法 resultLoop() 是一個獨立的協程,用于處理簽名區塊的提交和廣播,以及更新相關數據到數據庫。不妨將此協程稱作命名協程 worker.resultLoop()。

主要實現:

  • 在 for 循環中持續從通道 resultCh 和 exitCh 中接收消息,并執行相應的邏輯。
    • resultCh:
      • 接收已簽名區塊 block。
      • 如果 block == nil,則進入下一輪迭代。
      • 如果區塊 block 已經存在于經典鏈中,則進入下一輪迭代。
      • 定義兩個變量:
        • 區塊簽名哈希 sealhash,代碼為:sealhash = w.engine.SealHash(block.Header())
        • 區塊哈希 hash,代碼為:hash = block.Hash()
        • 分別計算區塊頭的驗證哈希 sealHash(不包括 extraData 中的最后 65 個字節的簽名信息),區塊的哈希 hash (即區塊頭的哈希,而且包含整個 extraData)。
      • 通過對鎖 w.pendingMu 進行加鎖和解鎖的方式從 w.pendingTasks 中找到 sealHash 對應的 task。這是找出已簽名區塊對應的任務 task,從中獲取需要的交易列表、交易回執列表等相關數據。
        • 如果 task 不存在,則輸出日志信息:log.Error("Block found but no relative pending task", "number", block.Number(), "sealhash", sealhash, "hash", hash)
        • 同時,退出本次迭代。
      • 定義兩個變量,交易回執列表 receipts,交易回執中包含的日志列表 logs。
        • receipts = make([]*types.Receipt, len(task.receipts))
        • logs []*types.Log
        • 這是因為不同的區塊可能會共享相同的區塊簽名哈希,建立這些副本是為了防止寫寫沖突。
        • 更新所有日志中的區塊哈希。這是因為對于這些日志來說,直到現在才知道對應的區塊哈希,而在創建單個交易的交易回執的接收日志時,并不知道對應的區塊哈希。
      • 更新 task.receipts 中各 receipt.Logs 的 BlockHash 值為 hash。
      • 通過方法 w.chain.WriteBlockWithState() 將區塊 block,交易回執列表 receipts,狀態數據庫 task.state 寫入數據庫,并返回寫入狀態 stat。stat 的取值:NonStatTy (0)、CanonStatTy (1)、SideStatTy(2)。
        • 如果寫入失敗,則輸出日志信息:log.Error("Failed writing block to chain", "err", err)。同時,退出本輪迭代。
      • 至此,成功的驗證了新的區塊。輸出日志信息:log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash, "elapsed", common.PrettyDuration(time.Since(task.createdAt)))
      • 將新產生的新區塊 block 廣播到網絡中的其他節點。這是通過構建事件 core.NewMinedBlockEvent 進調用 w.mux.Post() 實現的。代碼為:w.mux.Post(core.NewMinedBlockEvent{Block: block})
      • 定義變量事件列表 events
      • 根據寫入數據庫返回的狀態 stat 的值:
        • case core.CanonStatTy:在事件列表 events 中添加新的事件 core.ChainEvent、core.ChainHeadEvent
        • case core.SideStatTy:在事件列表 events 中添加新的事件 core.ChainSideEvent。
      • 通過方法 w.chain.PostChainEvents() 廣播事件。代碼為: w.chain.PostChainEvents(events, logs)
      • 將已簽名區塊插入待確認區塊列表中。代碼為:w.unconfirmed.Insert(block.NumberU64(), block.Hash())
    • exitCh:
      • 接收到退出消息則中止整個協程。

命名協程 worker.resultLoop() 從通道 resultCh 中接收消息 types.Block,且此區塊是被簽名過的。對于新接收到簽名區塊,首先判斷這個簽名區塊是否為重復的;其次,需要從待處理任務映射 w.pendingTasks 中獲得對應區塊簽名哈希的任務 task,如果沒找到則輸出一條重要的日志信息:log.Error("Block found but no relative pending task", "number", block.Number(), "sealhash", sealhash, "hash", hash)。并從 task 中恢復 receipts 和 logs。第三,將簽名區塊及其對應的收據列表和狀態樹等信息寫入數據庫。如果寫入失敗,則輸出一條重要的日志信息:log.Error("Failed writing block to chain", "err", err),否則輸出一條重要的日志信息:log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash, "elapsed", common.PrettyDuration(time.Since(task.createdAt)))。第四,通過新挖出的簽名區塊構建事件 core.NewMinedBlockEvent,并通過事件訂閱管理器中的方法 w.mux.Post() 將本地節點最新簽名的區塊向網絡中其它節點進行廣播,這是基于 p2p 模塊完成的。第五,同時構建事件 core.ChainEvent 和事件 core.ChainHeadEvent,或者構建事件 core.ChainSideEvent,并通過區塊鏈中的方法 w.chain.PostChainEvents() 進行廣播。需要注意的時,此廣播只是針對向本地節點進行了事件注冊的客戶端,且是通過 JSON-RPC 完成,和第四步中的向網絡中其它節點通過 p2p 進行廣播是完全不同的。這一部的廣播即使沒有事件接收方也沒有問題,因為這是業務邏輯層面的,而第四步中的廣播則是必須有接收方的,否則就會破壞以太坊協議本身。比如:我們可以注冊一個事件,用于監控是否有最新的區塊被挖出來,然后在此基礎上,查詢指定賬戶的最新余額。第六步,將新挖出來的簽名區塊,添加進待確認隊列中,代碼為:w.unconfirmed.Insert(block.NumberU64(), block.Hash())。

(15) func (w *worker) makeCurrent(parent *types.Block, header *types.Header) error

方法 makeCurrent() 為當前周期創建新的環境 environment。

參數:

  • parent *types.Block: 父區塊
  • header *types.Header: 當前區塊頭

主要實現:

  • 先通過父區塊狀態樹的根哈希從區塊鏈中獲取狀態信息 state (state.StateDB),如果失敗,直接返回錯誤
  • 構建當前環境 environment 的對象 env
    • 設定字段 signer 為 types.EIP155Signer
    • 設定字段 state 為前面獲取的 state
    • 設定字段 header 為參數 header
    • 默認初始化其它字段
  • 從區塊鏈中獲取父區塊之前的 7 個高度的所有區塊,包含叔區塊
    • 所有的直系父區塊添加到字段 ancestors
    • 所有的直系父區塊和叔區塊添加到字段 family
  • 將字段 tcount 設為 0
  • 將環境 env 賦值給字段 worker.current

(16) func (w *worker) commitUncle(env *environment, uncle *types.Header) error

方法 commitUncle() 將給定的區塊添加至叔區塊集合中,如果添加失敗則返回錯誤。

參數:

  • env *environment: 當前環境,里面組織了本次周期里需要的所有信息
  • uncle *types.Header: 叔區塊的區塊頭

主要實現:

  • 獲取叔區塊 hash。見代碼:hash := uncle.Hash()。
  • 判定叔區塊是否惟一。見代碼:if env.uncles.Contains(hash) { return errors.New("uncle not unique") }
  • 判定叔區塊是否為兄弟區塊。見代碼:if env.header.ParentHash == uncle.ParentHash { return errors.New("uncleis sibling") }
  • 判定叔區塊的父區塊是否存在于鏈上。見代碼:if !env.ancestors.Contains(uncle.ParentHash) { return errors.New("uncle's parent unknown") }
  • 判定叔區塊是否已經存在于鏈上。見代碼:if env.family.Contains(hash) { return errors.New("uncle already included") }
  • 上述四個判定都通過,則添加到當前區塊的叔區塊列表中。見代碼:env.uncles.Add(uncle.Hash())

(17) func (w *worker) updateSnapshot()

方法 updateSnapshot() 更新待處理區塊和狀態的快照。注意,此函數確保當前變量是線程安全的。

主要實現:
- 加鎖、解鎖 w.snapshotMu
- 定義叔區塊頭列表 uncles
- 對于 w.current.uncles 中的每個叔區塊頭 uncle,如果存在于
w.possibleUncles 中,則將其沒回到 uncles 中。
- 由 w.current.header, w.current.txs, uncles, w.current.receipts 構建出快照區塊 w.snapshotBlock。
- 由 w.current.state 的副本構建出快照狀態 w.snapshotState。

(18) func (w *worker) commitTransaction(tx types.Transaction, coinbase common.Address) ([]types.Log, error):

方法 commitTransaction() 提交交易 tx,并附上交易的發起者地址。此方法會生成交易的交易回執。

參數:

  • tx *types.Transaction: 具體的一次交易信息。
  • coinbase common.Address: 交易的發起方地址,可以明確指定。如果為空,則為區塊簽名者的地址。

返回值:

  • []*types.Log: 交易回執中的日志信息。

主要實現:

  • 先對狀態樹進行備份 snap,代碼為:snap := w.current.state.Snapshot()
  • 通過對交易 tx 及交易發起者 coinbase 調用方法 core.ApplyTransaction() 獲得交易回執 receipt。
    • 如果失敗,則將狀態樹恢復到之前的狀態 snap,并直接返回。
  • 更新交易列表。代碼為 w.current.txs = append(w.current.txs, tx)
  • 更新交易回執列表。代碼為 w.current.receipts = append(w.current.receipts, receipt)

(19) func (w *worker) commitTransactions(txs *types.TransactionsByPriceAndNonce, coinbase common.Address, interrupt *int32) bool:

方法 commitTransactions() 提交交易列表 txs,并附上交易的發起者地址。根據整個交易列表 txs 是否都被有效提交,返回 true 或 false。

參數:

  • txs *types.TransactionsByPriceAndNonce: 交易列表的管理器,同時根據價格和隨機數值進行排序,每次輸出一個排序最靠前的交易。具體的注釋,參考 types.TransactionsByPriceAndNonce。
  • coinbase common.Address: 交易的發起方地址,可以明確指定。如果為空,則為區塊簽名者的地址。
  • interrupt *int32: 中斷信號值。需要特別說明,這是個指針類型的值,意味著后續的每輪迭代都能讀取外部對于參數 interrupt 的更新。同時,此方法還能將內部對于參數 interrupt 的修改反饋給外部調用者。

返回值:

  • 整個交易列表是否都被正確處理。

主要實現:

  • 如果 w.current 為空,直接返回。
  • 如果 w.current.gasPool 為空,則初始化為 w.current.header.GasLimit
  • 匯總的事件日志,代碼為:var coalescedLogs []*types.Log
  • 循環處理交易列表 txs:
    • 在以下三種情況下,我們將中斷交易的執行。對于前兩種情況,半成品將被丟棄。對于第三種情況,半成品將被提交給共識引擎。需要特別說明的是,這一步會根據 w.current.header.GasLimit 和 w.current.gasPool.Gas() 計算事件 intervalAdjust 的字段 ratio,并將字段 inc 設為 true,然后將事件 intervalAdjust 發送給通道 w.resubmitAdjustCh,從而驅動命名協程 worker.newWorkLoop() 的工作流程。具備的可以參考代碼。
      • (1)新的區塊頭塊事件到達,中斷信號為1。
      • (2)對象 worker 啟動或重啟,中斷信號為1。
      • (3)對象 worker 用任何新到達的交易重新創建挖掘區塊,中斷信號為2。
      • 直接返回,退出整個循環和此方法。見代碼:return atomic.LoadInt32(interrupt) == commitInterruptNewHead
    • 如果沒有足夠的 Gas 進行任何進一步的交易,那么就退出循環。見代碼:if w.current.gasPool.Gas() < params.TxGas
      • 輸出一條重要的日志信息:log.Trace("Not enough gas for further transactions", "have", w.current.gasPool, "want", params.TxGas)
      • 需要說明的,已經提交并得到正常處理的交易仍然不變。
    • 獲取下一個交易 tx,如果為空則退出整個循環。
    • 獲取交易的發起者 from。見代碼:from, _ := types.Sender(w.current.signer, tx)
      • 這里可能會忽略錯誤。交易在被加入交易池時已經得到了檢查。
      • 無論當前的 hf 如何,我們都使用 eip155 簽名者。
    • 檢查交易 tx 是否重播受保護。如果我們不在 EIP155 hf 階段,請在我們開始之前開始忽略發送方。
      • 即過濾掉此交易。當然,仍然要從 txs 中剔除。見代碼:txs.Pop(); continue
    • 開始執行交易:
      • 更新狀態樹。需要說明的是,這一步會記錄交易在區塊中的索引。見代碼:w.current.state.Prepare(tx.Hash(), common.Hash{}, w.current.tcount)
      • 通過方法 worker.commitTransaction() 提交交易。見代碼:logs, err := w.commitTransaction(tx, coinbase)。根據返回值 err 決定后面的操作:
        • case core.ErrGasLimitReached
          • 彈出當前超出 Gas 的交易,而不從賬戶中轉移下一個交易。這是因為,該賬戶已經支付不起 Gas 了,所以不需要再處理該賬戶的其它交易。這個實現有點漂亮!!!
          • 輸出重要的日志信息:log.Trace("Gas limit exceeded for current block", "sender", from)
          • txs.Pop()
        • case core.ErrNonceTooLow
          • 交易池和礦工之間的新區塊頭通知數據競爭,轉移該賬戶下一個交易。
          • 輸出重要的日志信息:log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
          • txs.Shift()
        • case core.ErrNonceTooHigh
          • 事務池和礦工之間的重組通知數據競爭,跳過 account 的所有交易
          • 輸出重要的日志信息:log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
          • txs.Pop()
        • case nil
          • 一切正常,收集日志并從同一帳戶轉移下一個交易
          • coalescedLogs = append(coalescedLogs, logs...)
          • w.current.tcount++,需要增加當前區塊的交易索引。
          • txs.Shift()
        • default:
          • 奇怪的錯誤,丟棄事務并獲得下一個(注意,nonce-too-high子句將阻止我們徒勞地執行)。
          • 輸出重要的日志信息:log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
          • txs.Shift()
  • 我們在挖掘時不會推送pendingLogsEvent。原因是當我們開采時,工人將每3秒鐘再生一次采礦區。為了避免推送重復的pendingLog,我們禁用掛起的日志推送。
    • 構建日志集合 coalescedLogs 的副本 cpy,避免同步問題
    • 啟動一個獨立的匿名協程,將日志集合的副本 cpy 通過方法 TypeMux.Post() 發送出去。
  • 如果當前間隔大于用戶指定的間隔,則通知重新提交循環以減少重新提交間隔。代碼為:w.resubmitAdjustCh <- &intervalAdjust{inc: false}。即將事件 intervalAdjust 發送到通道 w.resubmitAdjustCh,從而驅動命名協和 worker.newWorkLoop() 的后續邏輯。

(20) func (w *worker) commitNewWork(interrupt *int32, noempty bool, timestamp int64):

方法 commitNewWork() 基于父區塊生成幾個新的簽名任務。

參數:

  • interrupt *int32: 中斷信號,值為:commitInterruptNone (0)、commitInterruptNewHead (1)、commitInterruptResubmit (2) 之一。
  • noempty bool: ???
  • timestamp int64: ??? 區塊時間?

主要實現:

  • 加鎖、解鎖 w.mu。說明對整個方法進行了加鎖處理。
  • 獲取當前時間 tstart,代碼為:tstart := time.Now()
  • 獲取父區塊 parent,即區塊鏈上的當前區塊。代碼為:parent := w.chain.CurrentBlock()
  • 根據父區塊的時間,調整下一個區塊的時間。
  • 如果挖礦太超前,計算超前時間 wait,并睡眠 wait 時間。同時,輸出日志:log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait))
  • 獲取父區塊編號 num,代碼為:num := parent.Number()
  • 構建打包中的區塊頭 header,代碼為:
    header := &types.Header{
    ParentHash: parent.Hash(),
    Number: num.Add(num, common.Big1),
    GasLimit: core.CalcGasLimit(parent, w.gasFloor, w.gasCeil),
    Extra: w.extra,
    Time: big.NewInt(timestamp),
    }
  • 只有在共識引擎正在運行中,才設置 coinbase(避免虛假區塊獎勵)
    • 如果 w.coinbase == (common.Address{}),則輸出日志信息:log.Error("Refusing to mine without etherbase")。同時,退出整個方法。
    • header.Coinbase = w.coinbase
  • 調用共識引擎的方法 Prepare() 設置區塊頭 header 中的共識字段。如果失敗,則輸出日志信息:log.Error("Failed to prepare header for mining", "err", err)。同時,退出整個方法。
  • 處理 DAO 硬分叉相關內容,暫時忽略。
  • 構建挖礦的當前環境,代碼為:w.makeCurrent(parent, header)。如果失敗,輸出日志:log.Error("Failed to create mining context", "err", err)。同時,退出整個方法。
  • env := w.current
  • 對 env 應用 DAO 相關操作。
  • 刪除 w.possibleUncles 中相對于當前區塊太舊的叔區塊
  • 遍歷 w.possibleUncles 累計當前區塊的叔區塊列表 uncles,最多支持 2 個叔區塊。
    • 下一個可能的叔區塊(hash 和 uncle)
    • 如果叔區塊列表 uncles 的長度已經達到 2,則退出遍歷操作。
    • 通過 w.commitUncle() 提交叔區塊 uncle
      • 如果失敗,輸出日志:log.Trace("Possible uncle rejected", "hash", hash, "reason", err)
      • 如果成功,輸出日志:log.Debug("Committing new uncle to block", "hash", hash)。同時,uncles = append(uncles, uncle.Header())
  • if !noempty
    • 基于臨時復制狀態創建空區塊以提前進行簽名,而無需等待區塊執行完成。
    • w.commit(uncles, nil, false, tstart)
  • 使用所有可用的待處理交易填充區塊。代碼為:pending, err := w.eth.TxPool().Pending()。如果失敗,則輸出日志:log.Error("Failed to fetch pending transactions", "err", err)。同時,退出整個方法。需要說明的是,從交易池中獲取所有待處理的交易列表,pending 的數據結構為:map[common.Address]types.Transactions。
  • 如果沒有待處理的交易列表
    • 更新快照。代碼為:w.updateSnapshot()
    • 退出整個方法。
  • 將交易池中的交易 pending 劃分為本地交易列表 localTxs 和遠程交易列表 remoteTxs。本地交易即提交者為 w.coinbase。
    • 具體方法為將事務池中地址為 w.coinbase 的放入本地事務列表,否則放入遠程事務列表。
  • 如果本地交易列表 localTxs 的長度大于 0
    • 將 localTxs 封裝為數據結構 types.NewTransactionsByPriceAndNonce。代碼為:txs := types.NewTransactionsByPriceAndNonce(w.current.signer, localTxs)
    • 提交交易列表。代碼為:w.commitTransactions(txs, w.coinbase, interrupt)。如果失敗,退出整個方法。
  • 如果本地交易列表 remoteTxs 的長度大于 0
    • 將 remoteTxs 封裝為數據結構 types.NewTransactionsByPriceAndNonce。代碼為:txs := types.NewTransactionsByPriceAndNonce(w.current.signer, remoteTxs)
    • 提交交易列表。代碼為:w.commitTransactions(txs, w.coinbase, interrupt)。如果失敗,退出整個方法。
  • 調用方法 w.commit() 組裝出最終的任務 task。

(21) func (w worker) commit(uncles []types.Header, interval func(), update bool, start time.Time) error

方法 commit() 運行任何交易的后續狀態修改,組裝最終區塊,并在共識引擎運行時提交新工作。

參數:

  • uncles []*types.Header: 叔區塊列表
  • interval func(): 中斷函數
  • update bool: 是否更新快照
  • start time.Time: 方法被調用的時間

返回值:

  • 如果出錯則返回出錯消息,否則返回 nil。

主要實現:

  • 為了避免在不同任務之間的交互,通過深度拷貝構建 current.receipts 的副本 receipts。
  • 構建狀態數據庫 w.current.state 的副本 s。
  • 調用共識引擎的方法 Finalize() 構建出最終待簽名的區塊 block。需要特別說明的是:對于待組裝的區塊來說,除了叔區塊列表 uncles 是作為參數傳入之外,其它的關鍵信息,如:區塊頭、交易列表、交易回執列表都是在當前環境 w.current 中獲取的。
  • 如果對象 worker 正在運行中:
    • 如果中斷函數 interval 非空,則調用函數 interval()。
    • 構建任務 task,并將其發送到通道 taskCh,從而驅動命名協程 worker.taskLoop() 的工作流程。
      • 刪除待確認區塊列表中的過期區塊,代碼為:w.unconfirmed.Shift(block.NumberU64() - 1)
      • 累計區塊 block 中所有交易消耗 Gas 的總和 feesWei。第 i 個交易 tx 消耗的 Gas 計算方式: receipts[i].GasUsed * tx.GasPrice()
      • 將 feesWei 轉換成 feesEth,即消耗的總以太幣。
      • 至此,已經打包好了最終的待簽名區塊。輸出一條重要的日志信息:log.Info("Commit new mining work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(uncles), "txs", w.current.tcount, "gas", block.GasUsed(), "fees", feesEth, "elapsed", common.PrettyDuration(time.Since(start)))
    • 持續監聽通道 worker.exitCh,如果接收到中止消息則輸出日志:log.Info("Worker has exited")
  • 如果 update 為 true,則更新快照:
    • 調用 w.updateSnapshot() 更新待處理的快照和狀態。

方法 worker.commit() (由命名協程 worker.mainLoop() 調用)將消息 task 發送給通道 taskCh。此方法先將當前環境中的區塊頭(w.current.header)、事務列表(w.current.txs)、收據列表(w.current.receipts)作為參數傳遞給共識引擎的方法 Finalize() 組裝出待簽名的區塊,代碼為 block = w.engine.Finalize(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)。需要注意的是,區塊 types.Block 中只包含區塊頭 types.Header、事務列表 []types.Transaction、叔區塊列表 []types.Header,并不包含收據列表 []types.Receipt,但是區塊頭 types.Header 中的字段 ReceiptHash 是收據列表樹的根哈希,所以也需要收據列表參數。將組裝后的待簽名區塊 types.Block,及前面解釋過的收據列表 []types.Receipt 等其它參數一起構建出新的任務 task 發送給通道 taskCh,同時輸出一條重要的日志信息:log.Info("Commit new mining work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(uncles), "txs", w.current.tcount, "gas", block.GasUsed(), "fees", feesEth, "elapsed", common.PrettyDuration(time.Since(start)))。到方法 commit() 這一步,已經組裝出了新的任務 task,并將此新任務 task 通過通道 taskCh 發送給命名協程 worker.taskLoop()。

Reference

  1. https://github.com/ethereum/go-ethereum/blob/master/miner/worker.go

Contributor

  1. Windstamp, https://github.com/windstamp
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容