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

By [Nerbonic](https://paragraph.com/@nerbonic) · 2022-03-03

---

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

前言
==

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

![在这里插入图片描述](https://storage.googleapis.com/papyrus_images/34c1e5cfed76588d39206be12ad1e33c994cc60428559ad0f56af57f09c01e84.png)

在这里插入图片描述

二、具体过程
======

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

---

*Originally published on [Nerbonic](https://paragraph.com/@nerbonic/A4UOZ4kLJ04HWonaXg42)*
