死磕以太坊源码分析之挖矿流程分析

死磕以太坊源码分析之挖矿流程分析

代码分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9

基本架构

以太坊挖矿的主要流程是由miner包负责的,下面是基本的一个架构:

image-20201212125409326

首先外部是通过miner对象进行了操作,miner里面则是实用worker对象来实现挖矿的整体功能。miner决定着是否停止挖矿或者是否可以开始挖矿,同时还可以设置矿工的地址来获取奖励。

真正调度处理挖矿相关细节的则是在worker.go里面,我们先来看一张总体的图。

image-20201212201358073

上图我们看到有四个循环,分别通过几个channel负责不同的事:

newWorkLoop

  1. startCh:接收startCh信号,开始挖矿
  2. chainHeadCh:表示接收到新区块,需要终止当前的挖矿工作,开始新的挖矿。
  3. timer.C:默认每三秒检查一次是否有新交易需要处理。如果有则需要重新开始挖矿。以便将加高的交易优先打包到区块中。

newWorkLoop 中还有一个辅助信号,resubmitAdjustChresubmitIntervalCh。运行外部修改timer计时器的时钟。resubmitAdjustCh是根据历史情况重新计算一个合理的间隔时间。而resubmitIntervalCh则允许外部,实时通过 Miner 实例方法 SetRecommitInterval 修改间隔时间。

mainLoop

  1. newWorkCh:接收生成新的挖矿任务信号
  2. chainSideCh:接收区块链中加入了一个新区块作为当前链头的旁支的信号
  3. txsCh:接收交易池的Pending中新加入了交易事件的信号

TaskLoop则是提交新的挖矿任务,而resultLoop则是成功出块之后做的一些处理。


启动挖矿

挖矿的参数设置

geth挖矿的参数设置定义在 cmd/utils/flags.go 文件中

参数 默认值 用途
–mine false 是否开启自动挖矿
–miner.threads 0 挖矿时可用并行PoW计算的协程(轻量级线程)数。 兼容过时参数 —minerthreads。
–miner.notify 挖出新块时用于通知远程服务的任意数量的远程服务地址。 是用 ,分割的多个远程服务器地址。 如:”http://api.miner.com,http://api2.miner.com“
–miner.noverify false 是否禁用区块的PoW工作量校验。
–miner.gasprice 1000000000 wei 矿工可接受的交易Gas价格, 低于此GasPrice的交易将被拒绝写入交易池和不会被矿工打包到区块。
–miner.gastarget 8000000 gas 动态计算新区块燃料上限(gaslimit)的下限值。 兼容过时参数 —targetgaslimit。
–miner.gaslimit 8000000 gas 动态技术新区块燃料上限的上限值。
–miner.etherbase 第一个账户 用于接收挖矿奖励的账户地址, 默认是本地钱包中的第一个账户地址。
–miner.extradata geth版本号 允许矿工自定义写入区块头的额外数据。
–miner.recommit 3s 重新开始挖掘新区块的时间间隔。 将自动放弃进行中的挖矿后,重新开始一次新区块挖矿。

常见的启动挖矿的方式

参数设置挖矿

dgeth --dev --mine

控制台启动挖矿

miner.start(1)

rpc 启动挖矿

这是部署节点使用的方式,一般设置如下:

/geth --datadir "/data0" --nodekeyhex "27aa615f5fa5430845e4e99229def5f23e9525a20640cc49304f40f3b43824dc" --bootnodes $enodeid --mine --debug --metrics --syncmode="full" --gcmode=archive --istanbul.blockperiod 5 --gasprice 0 --port 30303 --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --nat any --allow-insecure-unlock


开始源码分析,进入到miner.goNew函数中:

func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, isLocalBlock func(block *types.Block) bool) *Miner {
    miner := &Miner{
        ...
    }
    go miner.update()
    return miner
}
func (miner *Miner) update() {
  switch ev.Data.(type) {
            case downloader.StartEvent:
                atomic.StoreInt32(&miner.canStart, 0)
                if miner.Mining() {
                    miner.Stop()
                    atomic.StoreInt32(&miner.shouldStart, 1)
                    log.Info("Mining aborted due to sync")
                }
            case downloader.DoneEvent, downloader.FailedEvent:
                shouldStart := atomic.LoadInt32(&miner.shouldStart) == 1

                atomic.StoreInt32(&miner.canStart, 1)
                atomic.StoreInt32(&miner.shouldStart, 0)
                if shouldStart {
                    miner.Start(miner.coinbase)
                }
}

一开始我们初始化的canStart=1 , 如果Downloader模块正在同步,则canStart=0,并且停止挖矿,如果Downloader模块Done或者Failed,则canStart=1,且同时shouldStart=0,miner将启动。

miner.Start(miner.coinbase)

func (miner *Miner) Start(coinbase common.Address) {
...
    miner.worker.start()
}
func (w *worker) start() {
...
    w.startCh <- struct{}{}
}

接下来将会进入到mainLoop中去处理startCh

①:清除过旧的挖矿任务

clearPending(w.chain.CurrentBlock().NumberU64())

②:提交新的挖矿任务

commit := func(noempty bool, s int32) {
...
        w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp}
...
    }

生成新的挖矿任务

根据newWorkCh生成新的挖矿任务,进入到CommitNewWork中:

①:组装header

header := &types.Header{ //组装header
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1), //num+1
        GasLimit:   core.CalcGasLimit(parent, w.config.GasFloor, w.config.GasCeil),
        Extra:      w.extra,
        Time:       uint64(timestamp),
    }

②:根据共识引擎吃初始化header的共识字段

w.engine.Prepare(w.chain, header); 

③:为当前挖矿新任务创建环境

 w.makeCurrent(parent, header)

④:添加叔块

叔块集分本地矿工打包区块和其他挖矿打包的区块。优先选择自己挖出的区块。选择时,将先删除太旧的区块,只从最近的7(staleThreshold)个高度中选择,最多选择两个叔块放入新区块中.在真正添加叔块的同时会进行校验,包括如下:

  • 叔块存在报错
  • 添加的uncle是父块的兄弟报错
  • 叔块的父块未知报错
commitUncles(w.localUncles)
commitUncles(w.remoteUncles)

⑤:如果noempty为false,则提交空块,不填充交易进入到区块中,表示提前挖矿

if !noempty {
  w.commit(uncles, nil, false, tstart)
}

⑥:填充交易到新区块中

6.1 从交易池中获取交易,并把交易分为本地交易和远程交易,本地交易优先,先将本地交易提交,再将外部交易提交。

localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending
    for _, account := range w.eth.TxPool().Locals() {
        if txs := remoteTxs[account]; len(txs) > 0 {
            delete(remoteTxs, account)
            localTxs[account] = txs
        }
    }
if len(localTxs) > 0 {
   txs := types.NewTransactionsByPriceAndNonce(w.current.signer, localTxs)
   if w.commitTransactions(txs, w.coinbase, interrupt) {
      return
   }
}
if len(remoteTxs) > 0 {
   ...
}

6.2提交交易

  • 首先校验有没有可用的Gas
  • 如果碰到以下情况要进行交易执行的中断
    • 新的头块事件到达,中断信号为 1 (整个任务会被丢弃)
    • worker 开启或者重启,中断信号为 1 (整个任务会被丢弃)
    • worker重新创建挖矿任务根据新的交易,中断信号为 2 (任务还是会被送入到共识引擎)

6.3开始执行交易

logs, err := w.commitTransaction(tx, coinbase)

6.4执行交易获取收据

receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig())

如果执行出错,直接回退上一个快照

if err != nil {
        w.current.state.RevertToSnapshot(snap)
        return nil, err
    }

出错的原因大概有以下几个:

  • 超出当前块的gas limit
  • Nonce 太低
  • Nonce 太高

执行成功的话讲交易和收据存入到w.current中。

⑦:执行交易的状态更改,并组装成最终块

w.commit(uncles, w.fullTaskHook, true, tstart)

执行交易的状态更改,并组装成最终块是由下面的共识引擎所完成的事情:

block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, w.current.receipts)

底层会调用 state.IntermediateRoot执行状态更改。组装成最终块意味着到这打包任务完成。接着就是要提交新的挖矿任务。


提交新的挖矿任务

①:获取sealHash(挖矿前的区块哈希),重复提交则跳过

sealHash := w.engine.SealHash(task.block.Header()) // 返回挖矿前的块的哈希
            if sealHash == prev {
                continue
            }

②:生成新的挖矿请求,结果返回到reultCh或者StopCh

w.engine.Seal(w.chain, task.block, w.resultCh, stopCh);

挖矿的结果会返回到resultCh中或者stopCh中,resultCh有数据成功出块,stopCh不为空,则中断挖矿线程。


成功出块

resultCh有区块数据,则成功挖出了块,到最后的成功出块我们还需要进行相应的验证判断。

①:块为空或者链上已经有块或者pendingTasks不存在相关的sealhash,跳过处理

if block == nil {}
if w.chain.HasBlock(block.Hash(), block.NumberU64()) {}
task, exist := w.pendingTasks[sealhash] if !exist {}

②:更新receipts

for i, receipt := range task.receipts {
  receipt.BlockHash = hash
  ...
}

③:提交块和状态到数据库

_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true) // 互斥

④:广播区块并宣布链插入事件

w.mux.Post(core.NewMinedBlockEvent{Block: block})

⑤:等待规范确认本地挖出的块

新区块并非立即稳定,暂时存入到未确认区块集中。

w.unconfirmed.Insert(block.NumberU64(), block.Hash())

总结&参考

整个挖矿流程还是比较的简单,通过 4 个Loop互相工作,从开启挖矿到生成新的挖矿任务到提交新的挖矿任务到最后的成功出块,这里面的共识处理细节不会提到,接下来的文章会说到。

https://mindcarver.cn

https://github.com/blockchainGuide

https://learnblockchain.cn/books/geth/part2/mine/design.html

https://yangzhe.me/2019/02/25/ethereum-miner/#%E5%8A%A8%E6%80%81%E8%B0%83%E6%95%B4%E5%87%BA%E5%9D%97%E9%A2%91%E7%8E%8

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容