死磕以太坊源码分析之挖矿流程分析
source link: https://segmentfault.com/a/1190000038460904
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
基本架构
以太坊挖矿的主要流程是由 miner
包负责的,下面是基本的一个架构:
首先外部是通过 miner
对象进行了操作, miner
里面则是实用 worker
对象来实现挖矿的整体功能。miner决定着是否停止挖矿或者是否可以开始挖矿,同时还可以设置矿工的地址来获取奖励。
真正调度处理挖矿相关细节的则是在worker.go里面,我们先来看一张总体的图。
上图我们看到有四个循环,分别通过几个 channel
负责不同的事:
newWorkLoop
-
startCh
:接收startCh
信号,开始挖矿 -
chainHeadCh
:表示接收到新区块,需要终止当前的挖矿工作,开始新的挖矿。 -
timer.C
:默认每三秒检查一次是否有新交易需要处理。如果有则需要重新开始挖矿。以便将加高的交易优先打包到区块中。
在 newWorkLoop
中还有一个辅助信号, resubmitAdjustCh
和 resubmitIntervalCh
。运行外部修改timer计时器的时钟。 resubmitAdjustCh
是根据历史情况重新计算一个合理的间隔时间。而 resubmitIntervalCh
则允许外部,实时通过 Miner
实例方法 SetRecommitInterval
修改间隔时间。
mainLoop
newWorkCh chainSideCh txsCh
TaskLoop
则是提交新的挖矿任务,而 resultLoop
则是成功出块之后做的一些处理。
启动挖矿
挖矿的参数设置
geth
挖矿的参数设置定义在 cmd/utils/flags.go
文件中
,
分割的多个远程服务器地址。 如:” 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.go
的 New
函数中:
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
-
如果碰到以下情况要进行交易执行的中断
worker worker
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
互相工作,从开启挖矿到生成新的挖矿任务到提交新的挖矿任务到最后的成功出块,这里面的共识处理细节不会提到,接下来的文章会说到。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK