在以太坊的 1.7.3 版本里,关于 miner.start()这个操作,不管是在开发环境当中,还是在公链环境之下,一直以来返回的都是 null ,而不是 true 。这给众多开发者造成了困扰,是一个让他们感到头疼的问题。那么,其背后到底隐藏着何种秘密?让我们一起深入钻研源码,去探寻原因,同时也借此去了解更多的底层知识。
挖矿入口程序
在以太坊中调用挖矿程序,其执行的操作与系统的 CPU 核心数有关。在 go 语言中,有对应的方法。查看 eth/api.go 文件里的代码,能发现一些有意思的地方。一是挖矿时传递的参数是系统 CPU 的核心数,这个数值是可以指定的,如果未指定,就默认按系统核心数来。二是如果已经处于挖矿状态下再次执行挖矿程序,能够更改使用的 CPU 核心数。三是如果系统已经在挖矿,返回的结果是 nil(null)。四是如果还没有开始挖矿,就会执行 api.e.(true)方法。
miner.start()
##或
miner.start(n)
接着来看 api.e.(true)方法所对应的代码内容。这里有一个重要的点,那就是要成功挖矿就必须要有有效的账号。即使已经成功开始挖矿,也就是执行了 go s.miner.Start(eb)之后,此方法返回的结果依然是 nil(null)。所以,综合这两段代码可以清楚地知道,在这个版本中,不管挖矿是否成功,返回值都是 null,这也就导致在这个版本中,无法依据返回值来确定是否成功开启了挖矿。
miner.Start(threads *rpc.HexNumber) (bool, error)
进一步分析挖矿代码
// Start the miner with the given number of threads. If threads is nil the number
// of workers started is equal to the number of logical CPUs that are usable by
// this process. If mining is already running, this method adjust the number of
// threads allowed to use.
func (api *PrivateMinerAPI) Start(threads *int) error {
// Set the number of threads if the seal engine supports it
if threads == nil {
threads = new(int)
} else if *threads == 0 {
*threads = -1 // Disable the miner from within
}
type threaded interface {
SetThreads(threads int)
}
if th, ok := api.e.engine.(threaded); ok {
log.Info("Updated mining threads", "threads", *threads)
th.SetThreads(*threads)
}
// Start the miner and return
if !api.e.IsMining() {
// Propagate the initial price point to the transaction pool
api.e.lock.RLock()
price := api.e.gasPrice
api.e.lock.RUnlock()
api.e.txPool.SetGasPrice(price)
return api.e.StartMining(true)
}
return nil
}
进一步去钻研 s.miner.Start(eb)的代码。这段代码主要的作用是把相关的参数项给设置好,然后开启挖矿操作。在这个过程当中,有一个日志信息是用来判断是否开启挖矿的,也就是“ ”。要是日志里出现了这行输出,就可以把它当作是已经开始挖矿的一种参考,不过这只是一个必要但不充分的条件。
stop函数
挖矿的 start 函数与挖矿的 stop 函数相比,差异较为明显。不管 stop 函数执行是否成功,也不论当前是否处于挖矿状态,只要执行 stop 函数,就必定会返回 true。
挖矿问题影响开发者判断
func (s *Ethereum) StartMining(local bool) error {
eb, err := s.Etherbase()
if err != nil {
log.Error("Cannot start mining without etherbase", "err", err)
return fmt.Errorf("etherbase missing: %v", err)
}
if clique, ok := s.engine.(*clique.Clique); ok {
wallet, err := s.accountManager.Find(accounts.Account{Address: eb})
if wallet == nil || err != nil {
log.Error("Etherbase account unavailable locally", "err", err)
return fmt.Errorf("signer missing: %v", err)
}
clique.Authorize(eb, wallet.SignHash)
}
if local {
// If local (CPU) mining is started, we can disable the transaction rejection
// mechanism introduced to speed sync times. CPU mining on mainnet is ludicrous
// so noone will ever hit this path, whereas marking sync done on CPU mining
// will ensure that private networks work in single miner mode too.
atomic.StoreUint32(&s.protocolManager.acceptTxs, 1)
}
go s.miner.Start(eb)
return nil
}
对于开发者来说,这种返回值的情况会带来很大的困扰。在实际的开发过程中,比如 2022 年的某些以太坊技术研发项目,开发团队 A 打算在以太坊 1.7.3 版本上进行挖矿操作相关功能的开发。他们通过 miner.start()的返回值来判断挖矿是否成功启动,但是结果总是让他们感到迷惑。原本觉得挖矿没有成功启动,可是查看日志时却有可能发现挖矿已经开始了,这严重影响了开发的进度以及对挖矿状态的准确判断。
对以太坊版本维护的启示
从这个问题可以看出以太坊的版本维护需要更加严谨。之前在 2019 年,以太坊经历了大规模的漏洞修复升级,之后提供了较为稳定的环境。然而,1.7.3 版本出现了 miner.start()返回值异常的情况,这显然是在代码的逻辑或设计上存在疏忽。相关的维护团队应该意识到,尽管底层代码非常复杂,但是面向用户和开发者的接口部分一定要做到准确和清晰。
如何规避这个问题
func (self *Miner) Start(coinbase common.Address) {
atomic.StoreInt32(&self.shouldStart, 1)
self.worker.setEtherbase(coinbase)
self.coinbase = coinbase
if atomic.LoadInt32(&self.canStart) == 0 {
log.Info("Network syncing, will start miner afterwards")
return
}
atomic.StoreInt32(&self.mining, 1)
log.Info("Starting mining operation")
self.worker.start()
self.worker.commitNewWork()
}
在这个版本中,若遇到 miner.start()返回 null 导致无法准确判断挖矿状态的情形,开发者可从多方面尝试进行规避。从时间方面,比如设定一个时间期限,在 2023 年 5 月之后就不再使用这个版本进行挖矿相关的开发。从技术层面来讲,可以参照其他版本的挖矿相关代码逻辑,自行构建一个用于判断挖矿是否开始的函数。在人员方面,可以寻求有经验的以太坊开发老手来协助处理这个问题,就如同以太坊社区里的资深开发者李四一样,他在处理这种特殊的代码问题上有着很多独特的见解。
大家在以太坊的开发进程里,有没有碰到过类似这般难以揣测的代码逻辑方面的问题?欢迎大家展开评论进行互动,给本文点个赞并且进行分享。
暂无评论
发表评论