以太坊miner模块源码解析

您所在的位置:网站首页 以太坊源码分析 以太坊miner模块源码解析

以太坊miner模块源码解析

2023-12-31 18:24| 来源: 网络整理| 查看: 265

一、引言

在所有的区块链项目中,mining都是非常重要的一项功能。本文详细介绍了一个与mining紧密相关的模块 miner。 miner 是【矿工】的意思,但这个模块的功能,更像是现实中一个矿上的「调度室」,它管理着什么时候开始mining、什么时候开始挖一个新的区块等事件。 分析以太坊的 miner 模块的代码,个人觉得最重要的是学习它作为一个【调度器】是如何工作的。以便当我们自己需要写一个类似的功能的时候,可以有的放矢。因此,本文主要内容是从结构体的角度解析miner的工作原理。

二、miner模块工作的主要流程

miner 模块只有两个重要的结构体:Miner 和 Worker。本章主要介绍这两个对象的主要工作流程。知道了这两个对象的工作流程,基本就了解了miner的工作流程了。

1.Miner

Miner的功能比较简单,主要就是控制挖扩工作的开启(Start)和停止(Stop),以及设置一些参数和获取mining的状态。因此 Miner 对象的工作流程,主要体现在对mining工作的启停的控制上。

启停的控制本来很简单,但以太坊的mining有一个特性,就是在 downloader 模块同步区块时,会暂停mining;等区块同步完成以后,再恢复mining。这样一来就增加了一些复杂度。 Miner 对象是通过两个字段辅助实现这一特性的, 它们是:

Miner.canStart Miner.shouldStart

这里 canStart 代表【可不可以启动】,而 shouldStart 代表【应不应该启动】。 canStart 是先决条件,如果它为 0,无论 shouldStart 是什么值都不能启动mining;如果 canStart 为非 0,才看 shouldStart 是否应该启动。

知道了这两个字段的意义之后,不用看代码也能想明白,什么时候它们的值会是 0,什么时候会是 1。在 Miner对象创建之时,这时mining功能肯定是可以启动的,但还没有调用 Miner.Start 方法,所以不应该启动。所以此时 canStart 的值是 1,而 shouldStart 的值为 0:

func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, recommit time.Duration, gasFloor, gasCeil uint64, isLocalBlock func(block *types.Block) bool) *Miner { miner := &Miner{ ...... canStart: 1, // shouldStart 的值没有被设置,默认值为 0 } ...... }

在 Miner 对象创建后调用 Miner.Start 时, 代表「应该启动」mining功能了,至于「可不可以启动」,还要看 canStart 的值:

func (self *Miner) Start(coinbase common.Address) { // 应该启动 atomic.StoreInt32(&self.shouldStart, 1) ...... // 如果不可以启动,就直接返回;否则调用 worker.start 启动 if atomic.LoadInt32(&self.canStart) == 0 { return } self.worker.start() }

类似的,在调用 Miner.Stop 时,代表【应该停止】mining了(但这里不关心【可不可以停止】,因为随时可以停止):

func (self *Miner) Stop() { self.worker.stop() atomic.StoreInt32(&self.shouldStart, 0) }

以上是调用者对mining的控制。 那么当 downloader 模块开始同步时,按约定要暂时停止mining。此时 canStart 应该为 0;而 shouldStart 则要看当前的状态,如果当前正在mining,那么 shouldStart 应被设置为 1 (因为按理说此时 Miner 【应该】是在mining的,只不过被暂时停止了):

func (self *Miner) update() { events := self.mux.Subscribe( downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{}) for { select { case ev := self.Stop() atomic.StoreInt32(&self.shouldStart, 1) } ...... } ...... } } }

可以看到,在收到 downloader 开始同步的消息 downloader.StartEvent 后,会立即设置 canStart 为 0,即「、【不可以启动】;但如果当前正在mining,那么接着设置 shouldStart 为 1,即【应该启动】。

当 downloader 同步结束后,miner 模块又可以正常mining了。所以此时应设置 canStart 为 1;而如果 shouldStart 为 1,则说明mining功能被暂停了,现在应该恢复:

func (self *Miner) update() { events := self.mux.Subscribe( downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{}) for { select { case ev := self.Start(self.coinbase) } // stop immediately and ignore all further pending events return } ...... } } }

可以看到,在收到 downloader 同步结束的消息之后(无论成功或失败),会立即设置 canStart 为 1,即 【可以启动】;如果 shouldStart 为 1,则表示应该把mining功能启动起来,所以调用 self.Start 再次启动mining。

值得一提的是,这段代码中有个 return,即处理完直接返回了,所以 Miner.update 这个 go routine 也就退出了。以后如果 downloader 再同步区块,miner 也不会暂停了。

总体上, Miner 的主要流程就是控制mining功能的启停。由于要在区块同步时暂停mining,所以使用 canStart和 shouldStart 控制mining的启停。在任何时候,都要根据这两个字段判断【能不能启动挖mining】和【应不应该启动mining】,然后再作相应的操作启动mining(或不作任何操作)。

为什么要在区块同步时暂停mining?

miner 模块总是在本地主链上的最新的区块(也即【chain head】)的基础之上出块,如果【chain head】区块发生变化了,那么就需要以新的【chain head】为基础出块,之前正在出的块就得作废。而在区块同步时,【chain head】变化是很频繁的,若此时仍在mining,只能让 miner 模块不断的作废正在计算的区块,这显然是一种浪费,所以干脆暂停。至于为什么只暂停一次,主要是因为防止 DOS 攻击。如果每次同步都要暂停mining,试想如果搭建了一个mining节点开始mining,但总有别的节点告诉有区块需要同步,就不得不总是暂停mining去同步区块;如果这个节点是恶意的一直请求同步区块(即使都同步失败了),就会一直挖不了矿。

2.Worker

除了 Miner 对象外,miner 模块还有一个对象,就是 Worker。可以理解它为【工人】,因为具体的【mining调度】工作都是由 Worker对象完成的。

Worker对象的每一项工作,都是由一些消息触发的,因此乍看这个对象的代码,会感觉比较混乱。不过只要把握了它的主要流程,其实也是挺清晰的。下面是我画的关于 Worker对象的工作流程图: 在这里插入图片描述

从图中可以看到,出块的时机就是最顶上的几个 channel 接收到消息时。 首先,分别解释一下这几个 channel:

startCh

此消息在调用newWorker函数和调用worker.start方法时都会被触发,代表启动mining工作。收到此消息后,启动 timer 并发送消息给 newWorkCh 以发起一次新的出块任务。

timer.C

此消息是一个定时器消息,每隔一段时间都会发消息给 newWorkCh 发起一次出块任务。间隔时间由生成Worker对象的参数 recommit 决定(但 resubmitIntervalCh 和 resubmitAdjustCh 消息可以对其作出修改)。

chainHeadCh

此消息在有新的区块被加入到本地的区块数据库时被触发(此消息由 blockchain 模块发送)。收到消息后会发送消息给 newWorkCh 发起一次出块任务。

chainSideCh

此消息在本地有区块被加入到侧链时被触发。收到此消息后,将消息中的区块(即加入到侧链中的区块)加入到【叔块】列表中,并根据当前状态,可能选取两个叔块提交一个出块任务。(这个 channel 是以太坊鼓励出叔块的体现)

txsCh

此消息在收到新的 transaction 时被触发。一般情况下收到这个消息时不会提交新的出块任务,除非当前的共识算法是 clique 模块且配置里区块间隔值(Period)为 0。 在出块的 channel 被触发以后,所有新的出块任务被发送给 newWorkch。在这个 channel 的处理中,会调用 worker.commitNewWork 收集新块的基本信息,并提交出块任务到 taskCh 中。

在 taskCh 的处理中,会首先中止之前的出块任务。因为既然新的出块任务到达了,那么旧的出块任务中的信息显示已经过时了。在中止旧的任务后,将任务信息发送给 engine.Seal 对区块进行签名。这个过程是异步的。当签名成功后,共识模块(engine)会发送消息给 resultCh。至此,就挖到了一个新的模块。因此在 resultCh 的处理中,将这个新区块加入本地库,并广播给别的人知道。

基本上 Worker的工作流程就是这样的。虽然不算复杂,但在流程中有一些细节的操作,比如将要提交新的出块任务时,如何废除旧的任务;如何在提交新的签名任务时,中止对之前区块的签名。

三、总结

本篇文章主要介绍了以太坊的 miner 模块。文章主要总结了以太坊的 miner 模块的主要工作流程以及实现了哪功能,且这些功能是如体现在代码中的。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3