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

以太坊解析之一——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,//用来保存交易执行后的结果
)
Subscribe to Nerbonic
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.