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:
- 接收到退出消息,退出整個協程。
- startCh:
命名協程 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 則僅包含了區塊頭哈希。
- 接收到新的消息 ev(事件 ChainSideEvent)
- 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()
- 當從側鏈區塊頭訂閱通道接收到錯誤消息時,結束整個協程。
- newWorkCh:
(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() 放棄本次簽名。
- 退出整個協程。
- 當接收到退出消息時
- taskCh:
(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:
- 接收到退出消息則中止整個協程。
- resultCh:
命名協程 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()
- case core.ErrGasLimitReached
- 在以下三種情況下,我們將中斷交易的執行。對于前兩種情況,半成品將被丟棄。對于第三種情況,半成品將被提交給共識引擎。需要特別說明的是,這一步會根據 w.current.header.GasLimit 和 w.current.gasPool.Gas() 計算事件 intervalAdjust 的字段 ratio,并將字段 inc 設為 true,然后將事件 intervalAdjust 發送給通道 w.resubmitAdjustCh,從而驅動命名協程 worker.newWorkLoop() 的工作流程。具備的可以參考代碼。
- 我們在挖掘時不會推送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
Contributor
- Windstamp, https://github.com/windstamp