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

推荐阅读更多精彩内容