以太坊源码解析之一——出块与同步

以太坊解析之一——POW共识出块和同步过程

前言

一、一图展示以太坊出块过程

在这里插入图片描述
在这里插入图片描述

二、具体过程

1.geth启动

一切开始于geth启动指令:

1.1 启动geth同步节点

main.go:313:geth(ctx *cli.Context) main.go:319:makeFullNode(ctx)

1.2 注册以太坊服务

config.go:147:RegisterEthService(stack, &cfg.Eth) flags.go:1718:RegisterEthService(stack, &cfg.Eth)

1.3 实例化以太坊项目

flags.go:1727:eth.New(stack, cfg) backend.go:102:New(stack *node.Node, config *ethconfig.Config)

1.4 实例化过程,创建miner, worker

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()

1.5 开启四个协程

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:监测以决定是否主动出块

2.miner.start()启动

miner.start():

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()

3.mainLoop

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,//用来保存交易执行后的结果 )