乐趣区

关于区块链:死磕以太坊源码分析之挖矿流程分析

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

代码分支:https://github.com/ethereum/g…

根本架构

以太坊挖矿的次要流程是由 miner 包负责的,上面是根本的一个架构:

首先内部是通过 miner 对象进行了操作,miner外面则是实用 worker 对象来实现挖矿的整体性能。miner 决定着是否进行挖矿或者是否能够开始挖矿,同时还能够设置矿工的地址来获取处分。

真正调度解决挖矿相干细节的则是在 worker.go 外面,咱们先来看一张总体的图。

上图咱们看到有四个循环,别离通过几个 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/blockchain…

https://learnblockchain.cn/bo…

https://yangzhe.me/2019/02/25…

退出移动版