
一切开始于geth启动指令:
main.go:313:geth(ctx *cli.Context) main.go:319:makeFullNode(ctx)
config.go:147:RegisterEthService(stack, &cfg.Eth) flags.go:1718:RegisterEthService(stack, &cfg.Eth)
flags.go:1727:eth.New(stack, cfg) backend.go:102:New(stack *node.Node, config *ethconfig.Config)
backend.go:102:New(stack *node.Node, config *ethconfig.Config) backend.go:230:eth.miner = miner.New() miner.go:68:New() miner.go:76:worker:newWorker()//实例化出块工作流,以作准备 worker.go:190:newWorker()
worker.go:227-230: go worker.mainLoop() go worker.newWorkLoop(recommit) go worker.resultLoop() go worker.taskLoop() miner.go:78:go miner.update()//启动update协程监测以决定是出块还是收块 miner.go:97:dlEventCh := events.Chan()//看看可能有没有新块,有就收,没有就监听下startCh miner.go:131-134: case addr := <-miner.startCh://监听startCh,有就开始出块:miner.worker.start()
经过启动后,geth开启了5个协程: 即:
mainloop:用来进行计算、状态改变 taskloop:用来封装区块 resultloop:用来收区块,校验,上链,广播 newworkloop:用来接收任务
以及:
update:监测以决定是否主动出块
api.go:101:Start(threads *int)//threads:选择用几个线程出
func (api *PrivateminerAPI) Start(threads *int) error {
if threads == nil {
return api.e.StartMining(runtime.NumCPU())
}
return api.e.StartMining(*threads)
}
backend.go:444:StartMining(threads int) backend.go:482:go s.miner.Start(eb) //eb:etherbase(收益地址) miner.go:147:Start(coinbase common.Address) miner.go:147:Start(coinbase common.Address) miner.go:148:miner.startCh <- coinbase
startCh放入Ch,由newworkloop接收
worker.go:374:case <-w.startCh://监听到开始的Ch信号,clear清除Pending worker.go:377:commit(false, commitInterruptNewHead) worker.go:348-354:newWorkCh信号
至此,miner.start()作用完成,总结就是 发newWorkCh与startCh信号供监听用
worker.go:227-230: (这里是同时开启四个协程来并发,下面依照流程挨个解释) go worker.mainLoop() go worker.newWorkLoop(recommit) go worker.resultLoop() go worker.taskLoop()
worker.go:433:mainLoop()
worker.go:其中分为三种情况: 还记的第二张的newWorkCh吗,在这里接收 并进行【1、出新块操作】 441:1、出新块:(只考虑它):commitNewWork() 478:2、考虑分叉(与叔块相关) 504:3、如果没有在出,但是收到txs,那就自动清除
worker.go:869:commitNewWork():
...
//检查系统时间和当前区块时间差异
//新建一个区块头,填入父区块号,区块高度,gasLimit,时间戳等
//检查coinbase是否存在
//Prepare方法是consenus包下面定义的Engine的接口,由consenus/ethash和clique包分别实现其所有方法,包含Prepare方法
//ethash实现的是pos算法,clique实现的是poa算法,poa算法主要运行在测试网上,之所以不在测试网也使用pos是因为测试网算力不足,pos会遭到攻击
//本文主要分析ethash算法过程,除了特别说明,下文所有介绍的Engine接口的实现算法都是ethash算法
//Prepare方法是计算当前区块所需要的难度值,即header.Difficulty,方便后续计算hash的时候做比较(pos算法精髓)
if err := w.engine.Prepare(w.chain, header); err != nil {
log.Error("Failed to prepare header for mining", "err", err)
return
}
//检查是否有DAO硬分叉
//945,946:组装叔块,叔块不能距离当前块的高度超过7
//948-952:装个空块,不等tx执行完毕,后面954再去填充这个块
//组装交易池中的交易
//954组装成区块并987提交新交易
w.commit(uncles, w.fullTaskHook, true, tstart)
worker.go:992:commit():
代码如下(示例): func (w *worker) commit(uncles []*types.Header, interval func(), update bool, start time.Time) error { ... //FinalizeAndAssemble也是ethash包中的方法,根据传入参数组装成区块 block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, receipts) ... //将组装好的区块和数据发送给taskCh这个channel,该channel是在taskLoop中监听的channel,详见下文 case w.taskCh <- &task{receipts: receipts, state: s, block: block, createdAt: time.Now()}: //*******!!!!!重点关注此1005行 w.unconfirmed.Shift(block.NumberU64() - 1) log.Info("Commit new mining work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(uncles), "txs", w.current.tcount, "gas", block.GasUsed(), "fees", totalFees(block, receipts), "elapsed", common.PrettyDuration(time.Since(start))) //上面这个loginfo就是我们在geth命令行中看见的 ... } commit信息进入taskCh中,被taskloop监听到,后面的任务就是封装块并出块 附:具体打包(即ethash计算过程)区块过程参见FinalizeAndAssemble(这里面包含了奖励),consensus/clique/clique.go中的Finalize consensus/ethash/consensus.go: 597:FinalizeAndAssemble():并调用 589:Finalize:并调用 641:accumulateRewards 最后在667: state.AddBalance(header.Coinbase, reward) 看名字就能知道了 4.taskloop worker.go:535:taskloop(): worker.go:552:newTaskHook():钩子监听 worker.go:555:sealHash=SealHash(task.block.Header()): Seal封装区块 worker.go:567,570(关注resultCh): 567:pendingTasks[sealHash] = task 570:engine.Seal(链,块, resultCh, ..):封装好了 出块过程完成,出块成功,注意还没完,还得广播,来看resultLoop() 5.resultLoop 来看worker.go:582:resultLoop() worker.go:585:block := <-w.resultCh监听到块 worker.go:610-624:块的信息保存到全局receipt worker.go:625:块写到链中, 626:进行写入链,验证,最后输出我们见到的信息 在这里插入图片描述 worker.go:635:提交广播区块mux.Post() event.go:83:进行提交广播区块mux.Post() event.go:97:区块广播(递送)sub.deliver(event) event.go:204:区块广播(递送)sub.deliver(event) event.go:204:区块广播到Ch:postC <- event: 6.如何收新块 接下来看看怎么接收传过来的块,我们回到update中 miner.go:78:go miner.update()//启动update协程监测以决定是出块还是收块 miner.go:97:dlEventCh := events.Chan()//看看可能有没有新块,有就收,没有就监听下startCh 假设有了dlEventCh新块 case downloader.StartEvent: wasMining := miner.Mining()//你是不是在出 miner.worker.stop()//不管怎么样不要出了 canStart = false//用户,你也不许自己开始出 if wasMining {//如果你真的之前在出 // Resume mining after sync was finished shouldStart = true//你应该开始同步了 log.Info("Mining aborted due to sync")//告诉用户因为同步所以不出了 } 后面的也是miner.worker.start(),只不过因为没有newWorkCh,就不会主动出了,接下来需要参见sync.go 总结 现在可以用比喻的方式来看一下各个部分是干什么的: miner:总包工头,也就是用户 worker:总包工头助理(代替总包统管所有事务) Work: 代表了要干的事情 ethash/Clique: 干活的工具 其中,ethash代表了POW共识,Clique代表了POA共识 接下来 我们是如何使用共识工具的,即commit函数当中的 block, err := w.engine.FinalizeAndAssemble(w.chain, w.current.header, s, w.current.txs, uncles, receipts) 以此为入口,可以继续深入了解共识如何工作,这是FinalizeAndAssemble的函数定义: FinalizeAndAssemble( chain ChainHeaderReader, //当前链,用来代表整条链 header *types.Header, //当前区块头指针 state *state.StateDB, //用来保存账户等等等数据的本地节点数据库 txs []*types.Transaction,//要被打包的所有交易 uncles []*types.Header, //叔块的区块头,用来给那些本来计算出来叔块的人奖励 receipts []*types.Receipt,//用来保存交易执行后的结果 )
