bytom的挖矿流程是什么

免费建站   2024年05月10日 13:57  

本篇内容主要讲解“bytom的挖矿流程是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“bytom的挖矿流程是什么”吧!

当我们以bytom init --chain_id=solonet建立比原单机节点用于本地测试时,很快会发现自己将面临一个尴尬的问题:余额为0。就算我们使用bytom node --mining开启挖矿,理论上由于我们是单机状态,本机算力就是全网算力,应该每次都能够挖到,但是不知道为什么,在我尝试的时候发现总是挖不到,所以打算简单研究一下比原的挖矿流程,看看有没有办法能改点什么,给自己单机多挖点BTM以方便后面的测试。

所以在今天我打算通过源代码分析一下比原的挖矿流程,但是考虑到它肯定会涉及到比原的核心,所以太复杂的地方我就会先跳过,那些地方时机成熟的时候会彻底研究一下。

如果我们快速搜索一下,就能发现在比原代码中有一个类型叫CPUMiner,我们围绕着它应该就可以了。

首先还是从比原启动开始,看看CPUMiner是如何被启动的。

下面是bytom node --mining对应的入口函数:

cmd/bytomd/main.go#L54-L57

funcmain(){cmd:=cli.PrepareBaseCmd(commands.RootCmd,"TM",os.ExpandEnv(config.DefaultDataDir()))cmd.Execute()}

由于传入了参数node,所以创建Node并启动:

cmd/bytomd/commands/run_node.go#L41-L54

funcrunNode(cmd*cobra.Command,args[]string)error{//Create&startnoden:=node.NewNode(config)if_,err:=n.Start();err!=nil{//...}

在创建一个Node对象的时候,也会创建CPUMiner对象:

node/node.go#L59-L142

funcNewNode(config*cfg.Config)*Node{//...node.cpuMiner=cpuminer.NewCPUMiner(chain,accounts,txPool,newBlockCh)node.miningPool=miningpool.NewMiningPool(chain,accounts,txPool,newBlockCh)//...returnnode}

这里可以看到创建了两个与挖矿相关的东西,一个是NewCPUMiner,另一个是miningPool。我们先看NewCPUMiner对应的代码:

mining/cpuminer/cpuminer.go#L282-L293

funcNewCPUMiner(c*protocol.Chain,accountManager*account.Manager,txPool*protocol.TxPool,newBlockChchan*bc.Hash)*CPUMiner{return&CPUMiner{chain:c,accountManager:accountManager,txPool:txPool,numWorkers:defaultNumWorkers,updateNumWorkers:make(chanstruct{}),queryHashesPerSec:make(chanfloat64),updateHashes:make(chanuint64),newBlockCh:newBlockCh,}}

从这里的字段可以看到,CPUMiner在工作的时候:

可能需要用到外部的三个对象分别是:chain(代表本机持有的区块链),accountManager(管理帐户),txPool(交易池)

numWorkers:应该保持几个worker在挖矿,默认值defaultNumWorkers为常量1,也就是说默认只有一个worker。这对于多核cpu来说有点亏,真要挖矿的话可以把它改大点,跟核心数相同(不过用普通电脑不太可能挖到了)

updateNumWorkers:外界如果想改变worker的数量,可以通过向这个通道发消息实现。CPUMiner会监听它,并按要求增减worker

queryHashesPerSec:这个没用上,忽略吧。我发现比原的开发人员很喜欢预先设计,有很多这样没用上的代码

updateHashes: 这个没用上,忽略

newBlockCh: 一个来自外部的通道,用来告诉外面自己成功挖到了块,并且已经放进了本地区块链,其它地方就可以用它了(比如广播出去)

然而这里出现的并不是CPUMiner全部的字段,仅仅是需要特意初始化的几个。完整的在这里:

mining/cpuminer/cpuminer.go#L29-L45

typeCPUMinerstruct{sync.Mutexchain*protocol.ChainaccountManager*account.ManagertxPool*protocol.TxPoolnumWorkersuint64startedbooldiscreteMiningboolwgsync.WaitGroupworkerWgsync.WaitGroupupdateNumWorkerschanstruct{}queryHashesPerSecchanfloat64updateHasheschanuint64speedMonitorQuitchanstruct{}quitchanstruct{}newBlockChchan*bc.Hash}

可以看到还多出了几个:

sync.Mutex:为CPUMiner提供了锁,方便在不同的goroutine代码中进行同步

started:记录miner是否启动了

discreteMining:这个在当前代码中没有赋过值,永远是false,我觉得应该删除。已提issue #961

wg和workerWg:都是跟控制goroutine流程相关的

speedMonitorQuit:也没什么用,忽略

quit:外界可以给这个通道发消息来通知CPUMiner退出

再回到n.Start看看cpuMiner是何时启动的:

node/node.go#L169-L180

func(n*Node)OnStart()error{ifn.miningEnable{n.cpuMiner.Start()}//...}

由于我们传入了参数--mining,所以n.miningEnable是true,于是n.cpuMiner.Start会运行:

mining/cpuminer/cpuminer.go#L188-L205

func(m*CPUMiner)Start(){m.Lock()deferm.Unlock()ifm.started||m.discreteMining{return}m.quit=make(chanstruct{})m.speedMonitorQuit=make(chanstruct{})m.wg.Add(1)gom.miningWorkerController()m.started=truelog.Infof("CPUminerstarted")}

这段代码没太多需要说的,主要是通过判断m.started保证不会重复启动,然后把真正的工作放在了m.miningWorkerController()中:

mining/cpuminer/cpuminer.go#L126-L125

func(m*CPUMiner)miningWorkerController(){//1.varrunningWorkers[]chanstruct{}launchWorkers:=func(numWorkersuint64){fori:=uint64(0);i<numWorkers;i++{quit:=make(chanstruct{})runningWorkers=append(runningWorkers,quit)m.workerWg.Add(1)gom.generateBlocks(quit)}}runningWorkers=make([]chanstruct{},0,m.numWorkers)launchWorkers(m.numWorkers)out:for{select{//2.case<-m.updateNumWorkers:numRunning:=uint64(len(runningWorkers))ifm.numWorkers==numRunning{continue}ifm.numWorkers>numRunning{launchWorkers(m.numWorkers-numRunning)continue}fori:=numRunning-1;i>=m.numWorkers;i--{close(runningWorkers[i])runningWorkers[i]=nilrunningWorkers=runningWorkers[:i]}//3.case<-m.quit:for_,quit:=rangerunningWorkers{close(quit)}breakout}}m.workerWg.Wait()close(m.speedMonitorQuit)m.wg.Done()}

这个方法看起来代码挺多的,但是实际上做的事情还是比较好理清的,主要是做了三件事:

第1处代码是按指定的worker数量启动挖矿例程

第2处是监听应该保持的worker数量并增减

第3处在被知关闭的时候安全关闭

代码比较清楚,应该不需要多讲。

可以看第1处代码中,真正挖矿的工作是放在generateBlocks里的:

mining/cpuminer/cpuminer.go#L84-L119

func(m*CPUMiner)generateBlocks(quitchanstruct{}){ticker:=time.NewTicker(time.Second*hashUpdateSecs)deferticker.Stop()out:for{select{case<-quit:breakoutdefault:}//1.block,err:=mining.NewBlockTemplate(m.chain,m.txPool,m.accountManager)//...//2.ifm.solveBlock(block,ticker,quit){//3.ifisOrphan,err:=m.chain.ProcessBlock(block);err==nil{//...//4.blockHash:=block.Hash()m.newBlockCh<-&blockHash//...}}}m.workerWg.Done()}

方法里省略了一些不太重要的代码,我们可以从标注的几处看一下在做什么:

第1处通过mining.NewBlockTemplate根据模板生成了一个block

第2处是以暴力方式(从0开始挨个计算)来争夺对该区块的记帐权

第3处是通过chain.ProcessBlock(block)尝试把它加到本机持有的区块链上

第4处是向newBlockCh通道发出消息,通知外界自己挖到了新的块

mining.NewBlockTemplate

我们先看一下第1处中的mining.NewBlockTemplate:

mining/mining.go#L67-L154

funcNewBlockTemplate(c*protocol.Chain,txPool*protocol.TxPool,accountManager*account.Manager)(b*types.Block,errerror){//...returnb,err}

这个方法很长,但是内容都被我忽略了,原因是它的内容过于细节,并且已经触及到了比原的核心,所以现在大概了解一下就可以了。

比原在一个Block区块里,有一些基本信息,比如在其头部有前一块的hash值、挖矿难度值、时间戳等等,主体部有各种交易记录,以及多次层的hash摘要。在这个方法中,主要的逻辑就是去找到这些信息然后把它们包装成一个Block对象,然后交由后面处理。我觉得在我们还没有深刻理解比原的区块链结构和规则的情况下,看这些太细节的东西没有太大用处,所以先忽略,等以后合适的时候再回过头来看就简单了。

m.solveBlock

我们继续向下,当由NewBlockTemplate生成好了一个Block对象后,它会交给solveBlock方法处理:

mining/cpuminer/cpuminer.go#L50-L75

func(m*CPUMiner)solveBlock(block*types.Block,ticker*time.Ticker,quitchanstruct{})bool{//1.header:=&block.BlockHeaderseed,err:=m.chain.CalcNextSeed(&header.PreviousBlockHash)//...//2.fori:=uint64(0);i<=maxNonce;i++{//3.select{case<-quit:returnfalsecase<-ticker.C:ifm.chain.BestBlockHeight()>=header.Height{returnfalse}default:}//4.header.Nonce=iheaderHash:=header.Hash()//5.ifdifficulty.CheckProofOfWork(&headerHash,seed,header.Bits){returntrue}}returnfalse}

这个方法就是挖矿中我们最关心的部分了:争夺记帐权。

我把代码分成了4块,依次简单讲解:

第1处是从本地区块链中找到新生成的区块指定的父区块,并由它计算出来seed,它是如何计算出来的我们暂时不关心(比较复杂),此时只要知道它是用来检查工作量的就可以了

第2处是使用暴力方式来计算目标值,用于争夺记帐权。为什么说是暴力方式?因为挖矿的算法保证了想解开难题,没有比从0开始一个个计算更快的办法,所以这里从0开始依次尝试,直到maxNonce结束。maxNonce是一个非常大的数^uint64(0)(即2^64 - 1),基本上是不可能在一个区块时间内遍历完的。

第3处是在每次循环中进行计算之前,都看一看是否需要退出。在两种情况下应该退出,一是quit通道里有新消息,被人提醒退出(可能是时间到了);另一种是本地的区块链中已经收到了新的块,且高度比较自己高,说明已经有别人抢到了。

第4处是把当前循环的数字当作Nonce,计算出Hash值

第5处是调用difficulty.CheckProofOfWork来检查当前算出来的hash值是否满足了当前难度。如果满足就说明自己拥有了记帐权,这个块是有效的;否则就继续计算

然后我们再看一下第5处的difficulty.CheckProofOfWork:

consensus/difficulty/difficulty.go#L120-L123

funcCheckProofOfWork(hash,seed*bc.Hash,bitsuint64)bool{compareHash:=tensority.AIHash.Hash(hash,seed)returnHashToBig(compareHash).Cmp(CompactToBig(bits))<=0}

在这个方法里,可以看到出现了一个tensority.AIHash,这是比原独有的人工智能友好的工作量算法,相关论文的下载地址:https://github.com//bytom/wiki/download/tensority-v1.2.pdf,有兴趣的同学可以去看看。由于这个算法的难度肯定超出了本文的预期,所以就不研究它了。在以后,如果有机会有条件的话,也许我会试着理解一下(不要期待~)

从这个方法里可以看出,它是调用了tensority.AIHash中的相关方法进判断当前计算出来的hash是否满足难度要求。

在本文的开始,我们说过希望能找到一种方法修改比原的代码,让我们在solonet模式下,可以正常挖矿,得到BTM用于测试。看到这个方法的时候,我觉得已经找到了,我们只需要修改一下让它永远返回true即可:

funcCheckProofOfWork(hash,seed*bc.Hash,bitsuint64)bool{compareHash:=tensority.AIHash.Hash(hash,seed)returnHashToBig(compareHash).Cmp(CompactToBig(bits))<=0||true}

这里也许会让人觉得有点奇怪,为什么要在最后的地方加上|| true,而不是在前面直接返回true呢?这是因为,如果直接返回true,可能使得程序中关于时间戳检查的地方出现问题,出现如下的错误:

time="2018-05-17T12:10:14+08:00"level=errormsg="MinerfailonProcessBlockblock,timestampisnotinthevalidrange:invalidblock"height=32

原因还未深究,可能是因为原本的代码是需要消耗一些时间的,正好使得检查通过。如果直接返回true就太快了,反而使检查通过不了。不过我感觉这里是有一点问题的,留待以后再研究。

这样修改完以后,再重新编译并启动比原节点,每个块都能挖到了,差不多一秒一个块(一下子变成大富豪了:)

m.chain.ProcessBlock

我们此时该回到generateBlocks方法中的第3处,即:

mining/cpuminer/cpuminer.go#L84-L119

func(m*CPUMiner)generateBlocks(quitchanstruct{}){//...ifm.solveBlock(block,ticker,quit){//3.ifisOrphan,err:=m.chain.ProcessBlock(block);err==nil{//...//4.blockHash:=block.Hash()m.newBlockCh<-&blockHash//...}}}m.workerWg.Done()}

m.chain.ProcessBlock把刚才成功拿到记帐权的块向本地区块链上添加:

protocol/block.go#L191-L196

func(c*Chain)ProcessBlock(block*types.Block)(bool,error){reply:=make(chanprocessBlockResponse,1)c.processBlockCh<-&processBlockMsg{block:block,reply:reply}response:=<-replyreturnresponse.isOrphan,response.err}

可以看到这里实际上是把这个工作甩出去了,因为它把要处理的块放进了Chain.processBlockCh这个通道里,同时传过去的还有一个用于对方回复的通道reply。然后监听reply等消息就可以了。

那么谁将会处理c.processBlockCh里的内容呢?当然是由Chain。如果处理完没有出错,就进入到了第4块,把这个block的hash放在newBlockCh通道里。这个newBlockCh是由外面传入的,很多地方都会用到。当它里面有新的数据时,就说明本机挖到了新块(并且已经添加到了本机的区块链上),其它的地方就可以使用它进行别的操作(比如广播出去)

到此,相信大家对“bytom的挖矿流程是什么”有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

域名注册
购买VPS主机

您或许对下面这些文章有兴趣:                    本月吐槽辛苦排行榜

看贴要回贴有N种理由!看帖不回贴的后果你懂得的!


评论内容 (*必填):
(Ctrl + Enter提交)   

部落快速搜索栏

各类专题梳理

网站导航栏

X
返回顶部