├── README.md ├── funding.json ├── resources └── engine.svg └── sequencer ├── 00-how-sequencer-generates-L2-blocks.md ├── 01-how-block-sync.md ├── 02-how-optimism-use-libp2p.md ├── 03-how-batcher-works.md ├── 04-how-derivation-works.md ├── 05-how-proposer-works.md └── 06-Upgrade-of-OPStack-in-EIP-4844.md /README.md: -------------------------------------------------------------------------------- 1 | # Understanding Optimism Codebase (CN) 2 | 3 | 此文档为对Optimism的codebase进行全面讲解,意在帮助新来到Optimism的开发者快速上手,和真正了解在codebase的代码流中是怎么工作的。 4 | 5 | ## 项目目录 6 | 7 | ### 已完成: 8 | 9 | - [**00-sequencer如何产生一个新的L2区块**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/00-how-sequencer-generates-L2-blocks.md) 10 | - [**01-区块是如何同步的**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/01-how-block-sync.md) 11 | - [**02-libp2p在op-stack中的使用**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/02-how-optimism-use-libp2p.md) 12 | - [**03-op-batcher工作原理**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/03-how-batcher-works.md) 13 | - [**04-L2派生(derivation)原理**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/04-how-derivation-works.md) 14 | - [**05-op-proposer工作原理**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/05-how-proposer-works.md) 15 | - [**06-OP-Stack在EIP-4844中的升级**](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/06-Upgrade-of-OPStack-in-EIP-4844.md) 16 | - [**07-什么是fault-proof**](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/01-what-is-fault-proof.md) 17 | - [**08-Fault-Dispute-Game**](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/02-fault-dispute-game.md) 18 | - [**09-Cannon**](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/03-cannon.md) 19 | - [**10-op-program**](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/04-op-program.md) 20 | - [**11-op-challenger**](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/05-op-challenger.md) 21 | 22 | --- 23 | 24 | ### [The-book-of-optimism-fault-proof](https://github.com/joohhnnn/The-book-of-optimism-fault-proof) 25 | 26 | --- 27 | 28 | ### 待开始: 29 | 30 | - [op-e2e](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/tree/main/op-e2e): 使用Go进行所有Bedrock组件的端到端测试 31 | - [op-heartbeat](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/tree/main/op-heartbeat): 心跳监控服务 32 | - [op-service](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/tree/main/op-service): 通用代码库实用程序 33 | - [op-wheel](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/tree/main/op-wheel): 数据库实用程序 34 | 35 | ## 联系信息 36 | 37 | 若您有任何疑惑,或需在开发公共产品方面获得协助,请随时通过电子邮件 [joohhnnn8@gmail.com](mailto:joohhnnn8@gmail.com) 与我联系。如我有空闲时间,将十分乐意提供帮助。 38 | 39 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x881736756bdcc544ef526f7719608161ca00c6aed5d8f9b8837bdc1914f2abc6" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/engine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Rollup Driver
Rollup Driver
Engine API
Engine API
PayloadAttributes

- timestamp
- random
- suggestedFeeRecipient
- transactions
PayloadAttributes...
ForkChoiceState

- headBlockHash
- safeBlockHash
- finalizedBlockHash
ForkChoiceState...
FCS
FCS
PA
PA
payloadID
payloadID
Initiate block
production
Initiate block...
engine_forkchoiceUpdatedV1
engine_forkchoiceUpdatedV1
payload
payload
engine_getPayloadV1
(payloadID)
engine_getPayloadV1...
engine_newPayloadV1
(payload)
engine_newPayloadV1...
engine_forkchoiceUpdatedV1
engine_forkchoiceUpdatedV1
FCS
FCS
Current L2
block hash
Current L2...
payload.blockHash
payload.blockHash
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /sequencer/00-how-sequencer-generates-L2-blocks.md: -------------------------------------------------------------------------------- 1 | 2 | # Sequencer 工作流程 3 | 4 | Sequencer 在 Layer 2 (L2) 解决方案中起到核心作用,主要负责交易汇总,L1 数据派生,L2 区块生成,L1 batch 数据提交,以及 L1 中 L2 state root 的提议。在本文中,我们将深入探讨 Sequencer 的工作原理和相关代码实现。在这部分我们主要讨论L2区块的产生流程 5 | 6 | ## L2 区块生成 7 | 8 | 在一个更宏观的层面,sequencer在L2 区块的生成过程中实际上只是创建一个只包含 deposit 的模板块的 payload 。该 payload 随后被发送给 Execution Layer (EL),EL 从 txpool 中提取交易,然后进行 payload 包装以生成实际的区块。 9 | 10 | ### 代码流程 11 | 12 | 当操作节点(opnode)启动后,Driver 会启动一个 eventloop。在这个 eventloop 中,我们定义了 `sequencerCh` 通道和 `planSequencerAction` 方法。 13 | 14 | ```go 15 | sequencerTimer := time.NewTimer(0) 16 | var sequencerCh <-chan time.Time 17 | planSequencerAction := func() { 18 | delay := s.sequencer.PlanNextSequencerAction() 19 | sequencerCh = sequencerTimer.C 20 | if len(sequencerCh) > 0 { // 确保通道在重置前已被清空 21 | <-sequencerCh 22 | } 23 | sequencerTimer.Reset(delay) 24 | } 25 | ``` 26 | 27 | 在 `planSequencerAction` 方法中,我们重新设置了通道信号接收计时器的时间。而 `PlanNextSequencerAction` 方法则用于计算 `RunNextSequencerAction` 的延迟时间。 28 | 29 | #### 延迟时间解释 30 | 31 | 在这里,“延迟时间”是一个重要的概念。它决定了执行下一个序列化动作之前应该等待的时间。通过动态计算延迟时间,我们可以更灵活地控制序列化的频率和时机,从而优化系统的性能。 32 | 33 | ### Event Loop 的循环结构 34 | 35 | 在 event loop 的 for 循环中,首先进行了一系列的检查。例如,我们检查是否启用了 sequencer 和 L1 状态是否已准备好,以确定是否可以触发下一个 sequencer 操作。 36 | ```go 37 | for { 38 | // 主条件:检查 Sequencer 是否启用和 L1 状态是否准备好 39 | // 在这个 if 语句中,我们检查了几个关键条件来确定是否可以进行 sequencing,包括: 40 | // - Sequencer 是否启用 41 | // - Sequencer 是否停止 42 | // - L1 状态是否准备好 43 | // - Derivation pipeline 的引擎是否准备好 44 | if s.driverConfig.SequencerEnabled && !s.driverConfig.SequencerStopped && 45 | s.l1State.L1Head() != (eth.L1BlockRef{}) && s.derivation.EngineReady() { 46 | 47 | // 检查安全滞后 48 | // 在这段代码中,我们监视安全和不安全的 L2 头之间的滞后,以确定是否需要暂停新区块的创建。 49 | if s.driverConfig.SequencerMaxSafeLag > 0 && s.derivation.SafeL2Head().Number+s.driverConfig.SequencerMaxSafeLag <= s.derivation.UnsafeL2Head().Number { 50 | if sequencerCh != nil { 51 | s.log.Warn( 52 | "Delay creating new block since safe lag exceeds limit", 53 | "safe_l2", s.derivation.SafeL2Head(), 54 | "unsafe_l2", s.derivation.UnsafeL2Head(), 55 | ) 56 | sequencerCh = nil 57 | } 58 | // 更新 Sequencer 操作 59 | // 如果 sequencer 正在构建一个新的区块,并且 L1 状态已准备好,我们将更新下一个 sequencer 动作的触发器。 60 | } else if s.sequencer.BuildingOnto().ID() != s.derivation.UnsafeL2Head().ID() { 61 | planSequencerAction() 62 | } 63 | // 默认条件:在所有其他情况下,我们将 sequencerCh 设置为 nil,这意味着没有计划任何新的 sequencer 动作。 64 | } else { 65 | sequencerCh = nil 66 | } 67 | } 68 | ``` 69 | 70 | ### 总结 71 | 72 | 在 event loop 的循环结构中,我们进行了一系列的检查来确定是否可以触发下一个 sequencer 操作。 73 | 74 | 75 | 76 | 在通过检查的过程中,第一次planSequencerAction设置了计时器。 77 | 78 | 接下来查看 79 | ```go 80 | select { 81 | case <-sequencerCh: 82 | payload, err := s.sequencer.RunNextSequencerAction(ctx) 83 | if err != nil { 84 | s.log.Error("Sequencer critical error", "err", err) 85 | return 86 | } 87 | if s.network != nil && payload != nil { 88 | // Publishing of unsafe data via p2p is optional. 89 | // Errors are not severe enough to change/halt sequencing but should be logged and metered. 90 | if err := s.network.PublishL2Payload(ctx, payload); err != nil { 91 | s.log.Warn("failed to publish newly created block", "id", payload.ID(), "err", err) 92 | s.metrics.RecordPublishingError() 93 | } 94 | } 95 | planSequencerAction() // schedule the next sequencer action to keep the sequencing looping 96 | ``` 97 | 这部分代码是等待刚才计时器到达设定的时间后,被计时器发出的消息所触发。它首先尝试执行下一个序列化动作。如果这个动作成功了,它会尝试通过网络来发布新创建的负载。无论如何,它最终都会调用 planSequencerAction 函数来计划下一个序列化动作,这样就创建了一个持续的循环来处理序列化动作。 98 | 99 | 接下来让我们查看被触发的RunNextSequencerAction函数的内容 100 | ```go 101 | // RunNextSequencerAction starts new block building work, or seals existing work, 102 | // and is best timed by first awaiting the delay returned by PlanNextSequencerAction. 103 | // If a new block is successfully sealed, it will be returned for publishing, nil otherwise. 104 | // 105 | // Only critical errors are bubbled up, other errors are handled internally. 106 | // Internally starting or sealing of a block may fail with a derivation-like error: 107 | // - If it is a critical error, the error is bubbled up to the caller. 108 | // - If it is a reset error, the ResettableEngineControl used to build blocks is requested to reset, and a backoff applies. 109 | // No attempt is made at completing the block building. 110 | // - If it is a temporary error, a backoff is applied to reattempt building later. 111 | // - If it is any other error, a backoff is applied and building is cancelled. 112 | // 113 | // Upon L1 reorgs that are deep enough to affect the L1 origin selection, a reset-error may occur, 114 | // to direct the engine to follow the new L1 chain before continuing to sequence blocks. 115 | // It is up to the EngineControl implementation to handle conflicting build jobs of the derivation 116 | // process (as verifier) and sequencing process. 117 | // Generally it is expected that the latest call interrupts any ongoing work, 118 | // and the derivation process does not interrupt in the happy case, 119 | // since it can consolidate previously sequenced blocks by comparing sequenced inputs with derived inputs. 120 | // If the derivation pipeline does force a conflicting block, then an ongoing sequencer task might still finish, 121 | // but the derivation can continue to reset until the chain is correct. 122 | // If the engine is currently building safe blocks, then that building is not interrupted, and sequencing is delayed. 123 | func (d *Sequencer) RunNextSequencerAction(ctx context.Context) (*eth.ExecutionPayload, error) { 124 | if onto, buildingID, safe := d.engine.BuildingPayload(); buildingID != (eth.PayloadID{}) { 125 | if safe { 126 | d.log.Warn("avoiding sequencing to not interrupt safe-head changes", "onto", onto, "onto_time", onto.Time) 127 | // approximates the worst-case time it takes to build a block, to reattempt sequencing after. 128 | d.nextAction = d.timeNow().Add(time.Second * time.Duration(d.config.BlockTime)) 129 | return nil, nil 130 | } 131 | payload, err := d.CompleteBuildingBlock(ctx) 132 | if err != nil { 133 | if errors.Is(err, derive.ErrCritical) { 134 | return nil, err // bubble up critical errors. 135 | } else if errors.Is(err, derive.ErrReset) { 136 | d.log.Error("sequencer failed to seal new block, requiring derivation reset", "err", err) 137 | d.metrics.RecordSequencerReset() 138 | d.nextAction = d.timeNow().Add(time.Second * time.Duration(d.config.BlockTime)) // hold off from sequencing for a full block 139 | d.CancelBuildingBlock(ctx) 140 | d.engine.Reset() 141 | } else if errors.Is(err, derive.ErrTemporary) { 142 | d.log.Error("sequencer failed temporarily to seal new block", "err", err) 143 | d.nextAction = d.timeNow().Add(time.Second) 144 | // We don't explicitly cancel block building jobs upon temporary errors: we may still finish the block. 145 | // Any unfinished block building work eventually times out, and will be cleaned up that way. 146 | } else { 147 | d.log.Error("sequencer failed to seal block with unclassified error", "err", err) 148 | d.nextAction = d.timeNow().Add(time.Second) 149 | d.CancelBuildingBlock(ctx) 150 | } 151 | return nil, nil 152 | } else { 153 | d.log.Info("sequencer successfully built a new block", "block", payload.ID(), "time", uint64(payload.Timestamp), "txs", len(payload.Transactions)) 154 | return payload, nil 155 | } 156 | } else { 157 | err := d.StartBuildingBlock(ctx) 158 | if err != nil { 159 | if errors.Is(err, derive.ErrCritical) { 160 | return nil, err 161 | } else if errors.Is(err, derive.ErrReset) { 162 | d.log.Error("sequencer failed to seal new block, requiring derivation reset", "err", err) 163 | d.metrics.RecordSequencerReset() 164 | d.nextAction = d.timeNow().Add(time.Second * time.Duration(d.config.BlockTime)) // hold off from sequencing for a full block 165 | d.engine.Reset() 166 | } else if errors.Is(err, derive.ErrTemporary) { 167 | d.log.Error("sequencer temporarily failed to start building new block", "err", err) 168 | d.nextAction = d.timeNow().Add(time.Second) 169 | } else { 170 | d.log.Error("sequencer failed to start building new block with unclassified error", "err", err) 171 | d.nextAction = d.timeNow().Add(time.Second) 172 | } 173 | } else { 174 | parent, buildingID, _ := d.engine.BuildingPayload() // we should have a new payload ID now that we're building a block 175 | d.log.Info("sequencer started building new block", "payload_id", buildingID, "l2_parent_block", parent, "l2_parent_block_time", parent.Time) 176 | } 177 | return nil, nil 178 | } 179 | } 180 | ``` 181 | 这段代码定义了一个名为 RunNextSequencerAction 的方法,它是 Sequencer 结构的一部分。这个方法的目的是管理区块的创建和封装过程,根据当前的状态和遇到的任何错误来决定下一步的操作。 182 | 183 | 以下是该方法的主要工作流程和组件: 184 | 检查当前的区块创建状态: 185 | 使用 d.engine.BuildingPayload() 来检查当前是否有一个正在创建的区块。 186 | 187 | 处理正在创建的区块: 188 | 如果有一个正在创建的区块,它会检查是否安全继续创建。如果是这样,它会稍后重新尝试。如果不是这样,它会尝试完成区块创建。 189 | 190 | 错误处理: 191 | 在尝试完成区块创建时可能会遇到各种错误。这些错误被分类并适当处理: 192 | 193 | 严重错误:这些错误会被传递给调用者。 194 | 重置错误:这会导致排序器重置,并延迟后续的排序尝试。 195 | 临时错误:这只会导致短暂的延迟再次尝试。 196 | 其他错误:这将取消当前的区块创建任务,并稍后重新尝试。 197 | 成功创建区块: 198 | 如果区块成功创建,它将记录一个消息,并返回新创建的区块和nil错误。 199 | 200 | 开始一个新的区块创建任务: 201 | 如果当前没有区块正在创建,它将开始一个新的区块创建任务。这包括一个与上面相似的错误处理流程。 202 | 203 | 日志记录: 204 | 在整个方法中,根据不同的情况和结果,会有多个日志消息被记录,以帮助跟踪排序器的状态和行为。 205 | 206 | 让我们来突出关键的步骤,主要是两部分,一部分是完成完全的构建,一个是开启新的区块的构建。 207 | 208 | 209 | 首先让我们看一下开始新的区块构建的过程 210 | ```go 211 | func (d *Sequencer) StartBuildingBlock(ctx context.Context) error { 212 | … 213 | attrs, err := d.attrBuilder.PreparePayloadAttributes(fetchCtx, l2Head, l1Origin.ID()) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | … 219 | attrs.NoTxPool = uint64(attrs.Timestamp) > l1Origin.Time+d.config.MaxSequencerDrift 220 | 221 | … 222 | // Start a payload building process. 223 | errTyp, err := d.engine.StartPayload(ctx, l2Head, attrs, false) 224 | if err != nil { 225 | return fmt.Errorf("failed to start building on top of L2 chain %s, error (%d): %w", l2Head, errTyp, err) 226 | } 227 | … 228 | } 229 | ``` 230 | 231 | 在这段代码中, `RunNextSequencerAction` 方法及其在区块创建和封装过程中的作用如下 232 | 233 | 234 | ## 方法细节和解释 235 | 236 | 在这一部分,我们将深入探讨创建新区块的方法及其组成部分。 237 | 238 | ### Optimism 中的重要概念 239 | 240 | 在我们深入探讨 `PreparePayloadAttributes` 之前,我们需要先理解 Optimism 网络中的两个重要概念:**Sequencing Window** 和 **Sequencing Epoch**。 241 | 242 | #### Sequencing Window 243 | 244 | 在合并后的以太坊网络中,L1 的固定区块时间是 12 秒,且L1中的epoch是由32个L1区块组成(这里L1的Epoch和下面的Sequencing Epoch概念不同)。 L2 的区块时间是 2 秒。基于这个设置,我们可以明确“Sequencing Window”的概念,并通过一个示例来阐述它: 245 | 246 | - **示例**:如果我们设定一个“Sequencing Window”为 3600 个 L1 区块,那么这个窗口的总时间将为 43200 秒(12 秒/区块 × 3600 区块 = 43200 秒)。在这 43200 秒|12 小时的时间段里,理论上可以产生 21600 个 L2 区块(43200 秒/2 秒 = 21600)。Sequencing Epoch 即在某时刻起的21600秒内L2中 (Block N ~ Block N + 21600) 的范围. 247 | 248 | #### Sequencing Epoch 249 | 250 | “Sequencing Epoch”是根据特定的“Sequencing Window”派生的一系列 L2 区块。 251 | 252 | - **示例**:同上Sequencing Window示例 253 | 254 | ### 适应网络变化 255 | 256 | 在一些特殊情况下,为了保持网络的活跃性,我们可以通过增加“epoch”的长度来应对 L1 插槽被跳过或临时失去与 L1 的连接的情况。相反,为了防止 L2 时间戳逐渐超前于 L1,我们可能需要缩短“epoch”的时间来进行调整。 257 | 258 | 通过这样的设计,系统能够灵活而高效地调整区块生成的策略,确保网络的稳定和安全。 259 | 260 | ### 函数详解 261 | 262 | 在下面的函数中,我们可以看到传入的 epoch 参数是 `l1Origin.ID()`。这符合我们对 epoch 编号的定义。函数负责准备创建新 L2 块的所有必要属性。 263 | 264 | ```go 265 | attrs, err := d.attrBuilder.PreparePayloadAttributes(fetchCtx, l2Head, l1Origin.ID()) 266 | ``` 267 | ```go 268 | func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Context, l2Parent eth.L2BlockRef, epoch eth.BlockID) (attrs *eth.PayloadAttributes, err error) { 269 | var l1Info eth.BlockInfo 270 | var depositTxs []hexutil.Bytes 271 | var seqNumber uint64 272 | 273 | sysConfig, err := ba.l2.SystemConfigByL2Hash(ctx, l2Parent.Hash) 274 | if err != nil { 275 | return nil, NewTemporaryError(fmt.Errorf("failed to retrieve L2 parent block: %w", err)) 276 | } 277 | 278 | // If the L1 origin changed this block, then we are in the first block of the epoch. In this 279 | // case we need to fetch all transaction receipts from the L1 origin block so we can scan for 280 | // user deposits. 281 | if l2Parent.L1Origin.Number != epoch.Number { 282 | info, receipts, err := ba.l1.FetchReceipts(ctx, epoch.Hash) 283 | if err != nil { 284 | return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block info and receipts: %w", err)) 285 | } 286 | if l2Parent.L1Origin.Hash != info.ParentHash() { 287 | return nil, NewResetError( 288 | fmt.Errorf("cannot create new block with L1 origin %s (parent %s) on top of L1 origin %s", 289 | epoch, info.ParentHash(), l2Parent.L1Origin)) 290 | } 291 | 292 | deposits, err := DeriveDeposits(receipts, ba.cfg.DepositContractAddress) 293 | if err != nil { 294 | // deposits may never be ignored. Failing to process them is a critical error. 295 | return nil, NewCriticalError(fmt.Errorf("failed to derive some deposits: %w", err)) 296 | } 297 | // apply sysCfg changes 298 | if err := UpdateSystemConfigWithL1Receipts(&sysConfig, receipts, ba.cfg); err != nil { 299 | return nil, NewCriticalError(fmt.Errorf("failed to apply derived L1 sysCfg updates: %w", err)) 300 | } 301 | 302 | l1Info = info 303 | depositTxs = deposits 304 | seqNumber = 0 305 | } else { 306 | if l2Parent.L1Origin.Hash != epoch.Hash { 307 | return nil, NewResetError(fmt.Errorf("cannot create new block with L1 origin %s in conflict with L1 origin %s", epoch, l2Parent.L1Origin)) 308 | } 309 | info, err := ba.l1.InfoByHash(ctx, epoch.Hash) 310 | if err != nil { 311 | return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block info: %w", err)) 312 | } 313 | l1Info = info 314 | depositTxs = nil 315 | seqNumber = l2Parent.SequenceNumber + 1 316 | } 317 | 318 | // Sanity check the L1 origin was correctly selected to maintain the time invariant between L1 and L2 319 | nextL2Time := l2Parent.Time + ba.cfg.BlockTime 320 | if nextL2Time < l1Info.Time() { 321 | return nil, NewResetError(fmt.Errorf("cannot build L2 block on top %s for time %d before L1 origin %s at time %d", 322 | l2Parent, nextL2Time, eth.ToBlockID(l1Info), l1Info.Time())) 323 | } 324 | 325 | l1InfoTx, err := L1InfoDepositBytes(seqNumber, l1Info, sysConfig, ba.cfg.IsRegolith(nextL2Time)) 326 | if err != nil { 327 | return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err)) 328 | } 329 | 330 | txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)) 331 | txs = append(txs, l1InfoTx) 332 | txs = append(txs, depositTxs...) 333 | 334 | return ð.PayloadAttributes{ 335 | Timestamp: hexutil.Uint64(nextL2Time), 336 | PrevRandao: eth.Bytes32(l1Info.MixDigest()), 337 | SuggestedFeeRecipient: predeploys.SequencerFeeVaultAddr, 338 | Transactions: txs, 339 | NoTxPool: true, 340 | GasLimit: (*eth.Uint64Quantity)(&sysConfig.GasLimit), 341 | }, nil 342 | } 343 | ``` 344 | 在这个函数中,我们可以看到传入的epoch参数是l1Origin.ID()。符合我们epoch编号的定义。 345 | 346 | 如代码所示,`PreparePayloadAttributes` 准备新区块的有效载荷属性,它首先根据L1和L2的父块信息确定是否需要获取新的L1存款和系统配置数据。然后它创建一个特殊的系统交易,其中包含与L1块相关的信息和系统配置。这个特殊的交易和其他可能的L1存款交易一起构成了一个交易集,这将被包含在新的L2块的有效负载中。函数确保了时间的一致性和正确的序列号分配,最后返回一个包含所有这些信息的PayloadAttributes结构,以用于新L2块的创建。但在这里,我们只是准备了一个初步的 payload,它仅包含 L1 中的 deposit 交易。之后,我们调用 `StartPayload` 来开始 payload 的下一步构建。 347 | 348 | 349 | 在获取Attribute后,我们继续往下看 350 | ```go 351 | attrs.NoTxPool = uint64(attrs.Timestamp) > l1Origin.Time+d.config.MaxSequencerDrift 352 | ``` 353 | 判断是否需要产生空区块,注意这里的空区块也至少包含L1信息存款和任何用户存款。如果需要产生空区块,我们通过设置NoTxPool为true来处理,这将导致排序器不包含来自事务池的任何交易。 354 | 355 | 接下来会调用StartPayload去开启这个payload的构建 356 | ```go 357 | errTyp, err := d.engine.StartPayload(ctx, l2Head, attrs, false) 358 | if err != nil { 359 | // 如果在启动有效载荷构建过程时出现错误,则返回格式化的错误消息 360 | return fmt.Errorf("failed to start building on top of L2 chain %s, error (%d): %w", l2Head, errTyp, err) 361 | } 362 | ``` 363 | #### StartPayload 函数 364 | 365 | `StartPayload` 主要是触发了ForkchoiceUpdate和更新了EngineQueue中的building的一些状态,如buildingID等,后续再次RunNextSequencerAction时会根据这个id来找找到正在构建的ID 366 | 367 | ```go 368 | func (eq *EngineQueue) StartPayload(ctx context.Context, parent eth.L2BlockRef, attrs *eth.PayloadAttributes, updateSafe bool) (errType BlockInsertionErrType, err error) { 369 | if eq.isEngineSyncing() { 370 | return BlockInsertTemporaryErr, fmt.Errorf("engine is in progess of p2p sync") 371 | } 372 | if eq.buildingID != (eth.PayloadID{}) { 373 | eq.log.Warn("did not finish previous block building, starting new building now", "prev_onto", eq.buildingOnto, "prev_payload_id", eq.buildingID, "new_onto", parent) 374 | // TODO: maybe worth it to force-cancel the old payload ID here. 375 | } 376 | fc := eth.ForkchoiceState{ 377 | HeadBlockHash: parent.Hash, 378 | SafeBlockHash: eq.safeHead.Hash, 379 | FinalizedBlockHash: eq.finalized.Hash, 380 | } 381 | id, errTyp, err := StartPayload(ctx, eq.engine, fc, attrs) 382 | if err != nil { 383 | return errTyp, err 384 | } 385 | eq.buildingID = id 386 | eq.buildingSafe = updateSafe 387 | eq.buildingOnto = parent 388 | return BlockInsertOK, nil 389 | } 390 | ``` 391 | ```go 392 | func StartPayload(ctx context.Context, eng Engine, fc eth.ForkchoiceState, attrs *eth.PayloadAttributes) (id eth.PayloadID, errType BlockInsertionErrType, err error) { 393 | … 394 | fcRes, err := eng.ForkchoiceUpdate(ctx, &fc, attrs) 395 | … 396 | } 397 | ``` 398 | 399 | 在这个函数中内部调用ForkchoiceUpdate,我们可以看到一个新的 Payload ID 被创建,并且正在构建的 ID 和其他相关参数也被更新。 400 | 401 | ### ForkchoiceUpdate 函数 402 | 403 | 紧接着,`ForkchoiceUpdate` 函数被调用来处理 ForkChoice 的更新。这个函数是一个包装函数,它调用 `engine_forkchoiceUpdatedV1` 来触发 EL 产生新的区块。 404 | 405 | 406 | ForkchoiceUpdate函数是对调用的包装方法,其内部处理了对engine层(op-geth)的engineApi的调用,这里调用了engine_forkchoiceUpdatedV1去由EL产生区块 407 | ```go 408 | var result eth.ForkchoiceUpdatedResult 409 | err := s.client.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV1", fc, attributes) 410 | ``` 411 | 412 | 这个函数内部调用了 `engine_forkchoiceUpdatedV1` 方法来处理 Fork Choice 的更新和新的 Payload 的创建。 413 | 414 | ### op-geth 中的 ForkchoiceUpdated 函数 415 | 416 | 接下来,我们将视角转到 op-geth 中来看一下 `forkchoiceUpdated` 函数的实现。 417 | 418 | 在op-geth中,处理改请求的为forkchoiceUpdated函数,此函数首先获取和验证与提供的 fork choice 状态相关的各种区块,然后基于这些信息和可选的负载属性来创建一个新的负载(即一个新的区块)。如果负载创建成功,它将返回一个包含新负载 ID 的有效响应,否则它将返回一个错误。 419 | 关键代码如下 420 | ```go 421 | if payloadAttributes != nil { 422 | if api.eth.BlockChain().Config().Optimism != nil && payloadAttributes.GasLimit == nil { 423 | return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("gasLimit parameter is required")) 424 | } 425 | transactions := make(types.Transactions, 0, len(payloadAttributes.Transactions)) 426 | for i, otx := range payloadAttributes.Transactions { 427 | var tx types.Transaction 428 | if err := tx.UnmarshalBinary(otx); err != nil { 429 | return engine.STATUS_INVALID, fmt.Errorf("transaction %d is not valid: %v", i, err) 430 | } 431 | transactions = append(transactions, &tx) 432 | } 433 | args := &miner.BuildPayloadArgs{ 434 | Parent: update.HeadBlockHash, 435 | Timestamp: payloadAttributes.Timestamp, 436 | FeeRecipient: payloadAttributes.SuggestedFeeRecipient, 437 | Random: payloadAttributes.Random, 438 | Withdrawals: payloadAttributes.Withdrawals, 439 | NoTxPool: payloadAttributes.NoTxPool, 440 | Transactions: transactions, 441 | GasLimit: payloadAttributes.GasLimit, 442 | } 443 | id := args.Id() 444 | // If we already are busy generating this work, then we do not need 445 | // to start a second process. 446 | if api.localBlocks.has(id) { 447 | return valid(&id), nil 448 | } 449 | payload, err := api.eth.Miner().BuildPayload(args) 450 | if err != nil { 451 | log.Error("Failed to build payload", "err", err) 452 | return valid(nil), engine.InvalidPayloadAttributes.With(err) 453 | } 454 | api.localBlocks.put(id, payload) 455 | return valid(&id), nil 456 | } 457 | ``` 458 | 在这里,首先把刚才我们在op-node中创建的payload加载到args当中,再把args传到BuildPayload函数当中 459 | ```go 460 | // buildPayload builds the payload according to the provided parameters. 461 | func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { 462 | // Build the initial version with no transaction included. It should be fast 463 | // enough to run. The empty payload can at least make sure there is something 464 | // to deliver for not missing slot. 465 | empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, true, args.Transactions, args.GasLimit) 466 | if err != nil { 467 | return nil, err 468 | } 469 | // Construct a payload object for return. 470 | payload := newPayload(empty, args.Id()) 471 | if args.NoTxPool { // don't start the background payload updating job if there is no tx pool to pull from 472 | return payload, nil 473 | } 474 | 475 | // Spin up a routine for updating the payload in background. This strategy 476 | // can maximum the revenue for including transactions with highest fee. 477 | go func() { 478 | // Setup the timer for re-building the payload. The initial clock is kept 479 | // for triggering process immediately. 480 | timer := time.NewTimer(0) 481 | defer timer.Stop() 482 | 483 | // Setup the timer for terminating the process if SECONDS_PER_SLOT (12s in 484 | // the Mainnet configuration) have passed since the point in time identified 485 | // by the timestamp parameter. 486 | endTimer := time.NewTimer(time.Second * 12) 487 | 488 | for { 489 | select { 490 | case <-timer.C: 491 | start := time.Now() 492 | block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, false, args.Transactions, args.GasLimit) 493 | if err == nil { 494 | payload.update(block, fees, time.Since(start)) 495 | } 496 | timer.Reset(w.recommit) 497 | case <-payload.stop: 498 | log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery") 499 | return 500 | case <-endTimer.C: 501 | log.Info("Stopping work on payload", "id", payload.id, "reason", "timeout") 502 | return 503 | } 504 | } 505 | }() 506 | return payload, nil 507 | } 508 | ``` 509 | 初始化阶段: 510 | 511 | 首先,它使用提供的参数(但不包含任何交易)快速构建一个初始版本的空负载(即一个不包含任何交易的区块)。 512 | 如果在这一步中遇到错误,它将返回该错误。 513 | 构建返回负载对象: 514 | 515 | 接着,它使用刚创建的空区块来创建一个负载对象,该对象将被返回给调用者。 516 | 如果参数 args.NoTxPool 为真,这意味着没有交易池来从中获取交易,函数将结束并返回当前的负载对象。 517 | 后台更新负载: 518 | 519 | 如果 args.NoTxPool 为假,则启动一个后台goroutine来定期更新负载,以包含更多的交易和更新状态。 520 | 这个后台进程有两个计时器: 521 | 一个用于控制何时重新创建负载来包含新的交易。 522 | 另一个用于设置一个超时,超过这个时间后,后台进程将停止工作。 523 | 在每次计时器触发时,它都会尝试获取一个新的区块来更新负载,如果成功,它将更新负载对象中的数据。 524 | 如果负载被交付或时间超时,后台进程将停止。 525 | 通过这种方式,buildPayload 函数确保了一个初始的负载快速可用,同时后台进程尽可能地通过包含更多的交易来优化负载。这种策略旨在最大化通过包含高费用交易来获得的收入。 526 | 527 | 528 | 529 | 那么当args.NoTxPool为假时,究竟是怎么运行的呢? 530 | 答案藏在 getSealingBlock函数里 531 | ```go 532 | func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, withdrawals types.Withdrawals, noTxs bool, transactions types.Transactions, gasLimit *uint64) (*types.Block, *big.Int, error) { 533 | req := &getWorkReq{ 534 | params: &generateParams{ 535 | timestamp: timestamp, 536 | forceTime: true, 537 | parentHash: parent, 538 | coinbase: coinbase, 539 | random: random, 540 | withdrawals: withdrawals, 541 | noUncle: true, 542 | noTxs: noTxs, 543 | txs: transactions, 544 | gasLimit: gasLimit, 545 | }, 546 | result: make(chan *newPayloadResult, 1), 547 | } 548 | select { 549 | case w.getWorkCh <- req: 550 | result := <-req.result 551 | if result.err != nil { 552 | return nil, nil, result.err 553 | } 554 | return result.block, result.fees, nil 555 | case <-w.exitCh: 556 | return nil, nil, errors.New("miner closed") 557 | } 558 | } 559 | ``` 560 | 561 | 在这个部分,我们看到 `mainLoop` 函数通过监听 `getWorkCh` 通道来接收新的 Payload 创建请求。一旦接收到请求,它就会触发 `generateWork` 函数来开始新 Payload 的创建过程。 562 | ```go 563 | case req := <-w.getWorkCh: 564 | block, fees, err := w.generateWork(req.params) 565 | req.result <- &newPayloadResult{ 566 | err: err, 567 | block: block, 568 | fees: fees, 569 | } 570 | ``` 571 | ### GenerateWork 函数 572 | 573 | `GenerateWork` 函数是新 Payload 创建流程的最后一步。它负责准备工作并创建新的区块。 574 | 575 | ```go 576 | 577 | 578 | // generateWork generates a sealing block based on the given parameters. 579 | func (w *worker) generateWork(genParams *generateParams) (*types.Block, *big.Int, error) { 580 | work, err := w.prepareWork(genParams) 581 | if err != nil { 582 | return nil, nil, err 583 | } 584 | defer work.discard() 585 | if work.gasPool == nil { 586 | work.gasPool = new(core.GasPool).AddGas(work.header.GasLimit) 587 | } 588 | 589 | for _, tx := range genParams.txs { 590 | from, _ := types.Sender(work.signer, tx) 591 | work.state.SetTxContext(tx.Hash(), work.tcount) 592 | _, err := w.commitTransaction(work, tx) 593 | if err != nil { 594 | return nil, nil, fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err) 595 | } 596 | work.tcount++ 597 | } 598 | 599 | // forced transactions done, fill rest of block with transactions 600 | if !genParams.noTxs { 601 | interrupt := new(atomic.Int32) 602 | timer := time.AfterFunc(w.newpayloadTimeout, func() { 603 | interrupt.Store(commitInterruptTimeout) 604 | }) 605 | defer timer.Stop() 606 | 607 | err := w.fillTransactions(interrupt, work) 608 | if errors.Is(err, errBlockInterruptedByTimeout) { 609 | log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) 610 | } 611 | } 612 | block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts, genParams.withdrawals) 613 | if err != nil { 614 | return nil, nil, err 615 | } 616 | return block, totalFees(block, work.receipts), nil 617 | } 618 | ``` 619 | 620 | 在这个函数中,我们可以看到详细的区块创建过程,包括交易的处理和区块的最终组装。 621 | 622 | 623 | ## 区块完成和确认流程 624 | 625 | 在这一部分,我们将继续探讨 Sequencer 模式下的区块产生流程。这个阶段主要涉及到区块的完成和确认过程。以下我们将详细分析每个步骤和函数的作用。 626 | 627 | ### 区块的构建和优化 628 | 629 | 在开始阶段,我们首先在内存池中构建一个新的区块。这里特别注意到 `NoTxPool` 参数的应用,它是之前在 Sequencer 中设置的。这一段代码负责区块的初步构建和后续的优化工作。 630 | 631 | 其中关键步骤在于 632 | ```go 633 | if !genParams.noTxs { 634 | interrupt := new(atomic.Int32) 635 | timer := time.AfterFunc(w.newpayloadTimeout, func() { 636 | interrupt.Store(commitInterruptTimeout) 637 | }) 638 | defer timer.Stop() 639 | 640 | err := w.fillTransactions(interrupt, work) 641 | if errors.Is(err, errBlockInterruptedByTimeout) { 642 | log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) 643 | } 644 | } 645 | ``` 646 | 这一部分终于用到了在之前在sequencer中设置的NoTxPool参数,然后开始在内存池中构建新的区块(这里的内存池里的交易来自自身和其他节点,且由于gossip是默认关闭的,其他节点之间是没有内存池交易互通的,因此这就是为什么sequencer的内存池是私有的原因) 647 | 648 | 至此,block已经在sequencer的节点中产生区块了。但是buildPayload函数创建的是一个初始的、结构上正确的区块,并在一个后台进程中不断优化它以增加其交易内容和潜在的矿工收益,但它的有效性和认可是依赖于后续的网络共识过程的。也就是说他还需要后续的步骤来停止这个更新,而确定一个最终的块。 649 | 650 | 接下来让我们回到sequencer当中 651 | 在当前阶段,我们已经确定了在EngineQueue当中的buildingID已经设置,并且由这个payload派生的区块已经在op-geth中产生。 652 | 接下来,sequencer由于在最开始设置的time定时器触发,再次调用RunNextSequencerAction方法。 653 | 进入判断 但是在这次,我们的buildingID已经存在,因此进入CompleteBuildingBlock的阶段。 654 | ```go 655 | if onto, buildingID, safe := d.engine.BuildingPayload(); buildingID != (eth.PayloadID{}) { 656 | … 657 | payload, err := d.CompleteBuildingBlock(ctx) 658 | … 659 | } 660 | ``` 661 | CompleteBuildingBlock在内部调用了ConfirmPayload方法 662 | ```go 663 | // ConfirmPayload ends an execution payload building process in the provided Engine, and persists the payload as the canonical head. 664 | // If updateSafe is true, then the payload will also be recognized as safe-head at the same time. 665 | // The severity of the error is distinguished to determine whether the payload was valid and can become canonical. 666 | func ConfirmPayload(ctx context.Context, log log.Logger, eng Engine, fc eth.ForkchoiceState, id eth.PayloadID, updateSafe bool) (out *eth.ExecutionPayload, errTyp BlockInsertionErrType, err error) { 667 | payload, err := eng.GetPayload(ctx, id) 668 | … 669 | … 670 | status, err := eng.NewPayload(ctx, payload) 671 | … 672 | … 673 | fcRes, err := eng.ForkchoiceUpdate(ctx, &fc, nil) 674 | … 675 | return payload, BlockInsertOK, nil 676 | } 677 | ``` 678 | 在这里可以参考这张oplabs给出来的插图 679 | 680 | ![ENGINE](../resources/engine.svg) 681 | 682 | 这张导图主要是描述了create blocks的过程, 683 | 684 | #### Rollup 驱动程序实际上并不真正创建区块。相反,它通过 Engine API 指导执行引擎这样做。在上述每次块派生循环的迭代中,rollup 驱动程序将制作一个 payload 属性对象并将其发送到执行引擎。然后执行引擎将 payload 属性对象转换为一个区块,并将其添加到链中。Rollup 驱动程序的基本序列如下: 685 | 686 | 1. 使用 payload 属性对象调用 engine_forkChoiceUpdatedV1。我们现在先跳过 fork choice state 参数的详细信息 - 只需知道它的一个字段是 L2 链的 headBlockHash,它被设置为 L2 链尖端的区块哈希。Engine API 返回一个 payload ID。 687 | 2. 使用第1步返回的 payload ID 调用 engine_getPayloadV1。引擎 API 返回一个包含区块哈希作为其字段之一的 payload 对象。 688 | 3. 使用第2步返回的 payload 调用 engine_newPayloadV1。 689 | 4. 使用 fork choice 参数的 headBlockHash 设置为第2步返回的区块哈希来调用 engine_forkChoiceUpdatedV1。现在,L2 链的尖端是在第1步中创建的区块。 690 | 691 | 第一步的engine_forkChoiceUpdatedV1是我们从一开始开始构建的过程,而第二三四步就在ConfirmPayload方法里。 692 | 693 | 第二步 GetPayload方法 694 | 获取我们第一步构建的块的ExecutionPayload 695 | ```go 696 | // Resolve returns the latest built payload and also terminates the background 697 | // thread for updating payload. It's safe to be called multiple times. 698 | func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope { 699 | payload.lock.Lock() 700 | defer payload.lock.Unlock() 701 | 702 | select { 703 | case <-payload.stop: 704 | default: 705 | close(payload.stop) 706 | } 707 | if payload.full != nil { 708 | return engine.BlockToExecutableData(payload.full, payload.fullFees) 709 | } 710 | return engine.BlockToExecutableData(payload.empty, big.NewInt(0)) 711 | } 712 | ``` 713 | GetPayload方法通过向我们第一步开启的协程中的payload.stop通道发送型号,来停止block的重构。同时将最新的block的数据(ExecutionPayload)发送回sequencer(op-node) 714 | 715 | 第三步 NewPayload方法 716 | ```go 717 | func (api *ConsensusAPI) newPayload(params engine.ExecutableData) (engine.PayloadStatusV1, error) { 718 | … 719 | block, err := engine.ExecutableDataToBlock(params) 720 | if err != nil { 721 | log.Debug("Invalid NewPayload params", "params", params, "error", err) 722 | return engine.PayloadStatusV1{Status: engine.INVALID}, nil 723 | } 724 | … 725 | if err := api.eth.BlockChain().InsertBlockWithoutSetHead(block); err != nil { 726 | log.Warn("NewPayloadV1: inserting block failed", "error", err) 727 | 728 | api.invalidLock.Lock() 729 | api.invalidBlocksHits[block.Hash()] = 1 730 | api.invalidTipsets[block.Hash()] = block.Header() 731 | api.invalidLock.Unlock() 732 | 733 | return api.invalid(err, parent.Header()), nil 734 | } 735 | … 736 | } 737 | ``` 738 | 这里先根据我们最终确认的payload相关参数,构建一个区块,再将这个区块插入我们的blockChain当中。 739 | 740 | 第四步 ForkchoiceUpdate方法 741 | 742 | ```go 743 | func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payloadAttributes *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { 744 | … 745 | … 746 | if update.FinalizedBlockHash != (common.Hash{}) { 747 | if merger := api.eth.Merger(); !merger.PoSFinalized() { 748 | merger.FinalizePoS() 749 | } 750 | // If the finalized block is not in our canonical tree, somethings wrong 751 | finalBlock := api.eth.BlockChain().GetBlockByHash(update.FinalizedBlockHash) 752 | if finalBlock == nil { 753 | log.Warn("Final block not available in database", "hash", update.FinalizedBlockHash) 754 | return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not available in database")) 755 | } else if rawdb.ReadCanonicalHash(api.eth.ChainDb(), finalBlock.NumberU64()) != update.FinalizedBlockHash { 756 | log.Warn("Final block not in canonical chain", "number", block.NumberU64(), "hash", update.HeadBlockHash) 757 | return engine.STATUS_INVALID, engine.InvalidForkChoiceState.With(errors.New("final block not in canonical chain")) 758 | } 759 | // Set the finalized block 760 | api.eth.BlockChain().SetFinalized(finalBlock.Header()) 761 | } 762 | … 763 | } 764 | ``` 765 | 通过SetFinalized将我们之前几步产生的block进行Finalized 766 | 此方法将一个特定的区块标记为已“最终确定(finalized)”。在区块链网络中,当一个区块被标记为“最终确定(finalized)”时,意味着该区块及其所有先前的区块都不可逆转,它们将永远成为区块链的一部分。这是一个非常重要的安全特性,确保一旦一个区块被最终确定,则它不可能被另一个分叉所替代。 767 | 768 | 769 | 这样,一个基础的l2的block的构建就算是完成了,后续的工作就是把这个新的l2的信息记录在sequencer当中,让我们返回到ConfirmPayload函数中 770 | ```go 771 | payload, errTyp, err := ConfirmPayload(ctx, eq.log, eq.engine, fc, eq.buildingID, eq.buildingSafe) 772 | if err != nil { 773 | return nil, errTyp, fmt.Errorf("failed to complete building on top of L2 chain %s, id: %s, error (%d): %w", eq.buildingOnto, eq.buildingID, errTyp, err) 774 | } 775 | ref, err := PayloadToBlockRef(payload, &eq.cfg.Genesis) 776 | if err != nil { 777 | return nil, BlockInsertPayloadErr, NewResetError(fmt.Errorf("failed to decode L2 block ref from payload: %w", err)) 778 | } 779 | 780 | eq.unsafeHead = ref 781 | eq.engineSyncTarget = ref 782 | eq.metrics.RecordL2Ref("l2_unsafe", ref) 783 | eq.metrics.RecordL2Ref("l2_engineSyncTarget", ref) 784 | ``` 785 | 786 | 可以看到payload被解析为PayloadToBlockRe(PayloadToBlockRef extracts the essential L2BlockRef information from an execution payload, falling back to genesis information if necessary.)例如unsafeHead。这些数据会被后续的例如区块传播等步骤使用 787 | 788 | 这样一个完整sequencer模式下的区块产生的流程将结束了。 下一章讲继续介绍在产生完区块后,sequencer是如何把这个区块传播给其他例如verifier的节点的。 789 | -------------------------------------------------------------------------------- /sequencer/01-how-block-sync.md: -------------------------------------------------------------------------------- 1 | # optimism中区块的传递 2 | 3 | 区块的传递是整个optimism rollup系统中较为重要的概念,在这一章节,我们将从介绍optimism中多种sync方式的原理,来揭开整个系统里区块的传递过程。 4 | 5 | ## 区块类型 6 | 7 | 在进行进一步深入前,让我们了解一些基本的概念。 8 | 9 | - **Unsafe L2 Block (不安全的 L2 区块)**: 10 | - 这是指 L1 链上最高的 L2 区块,其 L1 起源是规范 L1 链的 *可能* 扩展(如 op-node 所知)。这意味着,尽管该区块链接到 L1 链,但其完整性和正确性尚未得到充分验证。 11 | 12 | - **Safe L2 Block (安全的 L2 区块)**: 13 | - 这是指 L1 链上最高的 L2 区块,其 epoch 的序列窗口在规范的 L1 链中是完整的(如 op-node 所知)。这意味着该区块的所有前提条件都已在 L1 链上得到验证,因此它被认为是安全的。 14 | 15 | - **Finalized L2 Block (定稿的 L2 区块)**: 16 | - 这是指已知完全源自定稿 L1 区块数据的 L2 区块。这意味着该区块不仅安全,而且已根据 L1 链的数据完全确认,不会再发生更改。 17 | 18 | 19 | ## sync类型 20 | 21 | 1. **op-node p2p gossip 同步**: 22 | - op-node 通过 p2p gossip 协议接收最新的不安全区块,由 sequencer 推送的。 23 | 24 | 2. **op-node 基于libp2p的请求-响应的逆向区块头同步**: 25 | - 通过此同步方式,op-node 可以填补不安全区块的任何缺口。 26 | 27 | 3. **执行层(EL,又名 engine sync)同步**: 28 | - 在 op-node 中有两个标志,允许来自 gossip 的不安全区块触发引擎中向这些区块的长范围同步。相关的标志是 `--l2.engine-sync` 和 `--l2.skip-sync-start-check`(用于处理非常旧的安全区块)。然后,如果为此设置了 EL,它可以执行任何同步,例如 snap-sync(需要 op-geth p2p 连接等,并且需要从某些节点进行同步)。 29 | 30 | 4. **op-node RPC 同步**: 31 | - 这是一种基于可信 RPC 方法的同步,当 L1 出现问题时,这种同步方式相对简单。 32 | 33 | ## op-node p2p gossip 同步 34 | 35 | 这种同步的场景处于:当l2的块新产生的时候,即在上一节我们讨论的sequencer模式下是如何产生新的区块的。 36 | 37 | 当产生新的区块后,sequencer通过基于libp2p的P2P网络的pub/sub(广播/订阅)模块,向’新unsafe区块‘ topic 发出广播。所有订阅了此topic的节点都会直接或间接的收到这一广播消息。[详情可以查看](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/02-how-optimism-use-libp2p.md#gossip%E4%B8%8B%E7%9A%84%E5%8C%BA%E5%9D%97%E4%BC%A0%E6%92%AD) 38 | 39 | ## op-node 基于libp2p的请求-响应的逆向区块头同步 40 | 41 | 这种同步的场景处于:当节点因为特殊情况,比如宕机后重新链接,可能会产生一些没有同步上的区块(gaps) 42 | 43 | 当这种情况出现的时候,可以通过p2p网络的反向链的方式快速同步,即通过使用libp2p原生的stream流来和其他p2p节点建立链接,同时发送同步请求。[详情可以查看](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/02-how-optimism-use-libp2p.md#%E5%BD%93%E5%AD%98%E5%9C%A8%E7%BC%BA%E5%A4%B1%E5%8C%BA%E5%9D%97%E9%80%9A%E8%BF%87p2p%E5%BF%AB%E9%80%9F%E5%90%8C%E6%AD%A5) 44 | 45 | ## 执行层(EL,又名 engine sync)同步 46 | 47 | 这种同步的场景处于:当有较多区块,一个大范围区块需要同步的时候,从l1慢慢派生比较慢,想要快速同步。 48 | 49 | 使用`--l2.engine-sync` 和 `--l2.skip-sync-start-check`去启动op-node,发送的payload来达到发送长范围同步请求的目的。 50 | 51 | ### 代码层讲解 52 | 53 | 首先我们来看一下这两个标志的定义 54 | 55 | 在 `op-node/flags/flags.go` 中定义并解释了这两个flag的作用 56 | 57 | - **L2EngineSyncEnabled Flag (`l2.engine-sync`)**: 58 | - 该标志用于启用或禁用执行引擎的 P2P 同步功能。当设置为 `true` 时,它允许执行引擎通过 P2P 网络与其他节点同步区块数据。它的默认值是 `false`,意味着在默认情况下,该 P2P 同步功能是禁用的。 59 | 60 | - **SkipSyncStartCheck Flag (`l2.skip-sync-start-check`)**: 61 | - 该标志用于在确定同步起始点时,跳过对不安全 L2 区块的 L1 起源一致性的合理性检查。当设置为 `true` 时,它会推迟 L1 起源的验证。如果你正在使用 `l2.engine-sync`,建议启用此标志来跳过初始的一致性检查。它的默认值是 `false`,意味着在默认情况下,该合理性检查是启用的。 62 | 63 | ```go 64 | L2EngineSyncEnabled = &cli.BoolFlag{ 65 | Name: "l2.engine-sync", 66 | Usage: "Enables or disables execution engine P2P sync", 67 | EnvVars: prefixEnvVars("L2_ENGINE_SYNC_ENABLED"), 68 | Required: false, 69 | Value: false, 70 | } 71 | SkipSyncStartCheck = &cli.BoolFlag{ 72 | Name: "l2.skip-sync-start-check", 73 | Usage: "Skip sanity check of consistency of L1 origins of the unsafe L2 blocks when determining the sync-starting point. " + 74 | "This defers the L1-origin verification, and is recommended to use in when utilizing l2.engine-sync", 75 | EnvVars: prefixEnvVars("L2_SKIP_SYNC_START_CHECK"), 76 | Required: false, 77 | Value: false, 78 | } 79 | ``` 80 | #### L2EngineSyncEnabled 81 | 82 | L2EngineSyncEnabled标志用于在op-node接收到新的unsafe的payload(区块)后,发送给op-geth进一步验证时,触发op-geth的p2p之间sync,在sync期间所有的unsafe区块都会被视为通过验证,并进行下一个unsafe的流程。op-geth内部的p2p sync比较适用于长范围的unsafe区块的获取。其实在op-geth内部,不管L2EngineSyncEnabled标志有没有启用,在遇到parent区块不存在的时候,都会开启sync去同步数据。 83 | 84 | 让我们深入代码层面看一下 85 | 首先是 `op-node/rollup/derive/engine_queue.go` 86 | 87 | `EngineSync`为L2EngineSyncEnabled标志的具体表达。在这里嵌套在两个检查函数当中。 88 | 89 | ```go 90 | // checkNewPayloadStatus checks returned status of engine_newPayloadV1 request for next unsafe payload. 91 | // It returns true if the status is acceptable. 92 | func (eq *EngineQueue) checkNewPayloadStatus(status eth.ExecutePayloadStatus) bool { 93 | if eq.syncCfg.EngineSync { 94 | // Allow SYNCING and ACCEPTED if engine P2P sync is enabled 95 | return status == eth.ExecutionValid || status == eth.ExecutionSyncing || status == eth.ExecutionAccepted 96 | } 97 | return status == eth.ExecutionValid 98 | } 99 | 100 | // checkForkchoiceUpdatedStatus checks returned status of engine_forkchoiceUpdatedV1 request for next unsafe payload. 101 | // It returns true if the status is acceptable. 102 | func (eq *EngineQueue) checkForkchoiceUpdatedStatus(status eth.ExecutePayloadStatus) bool { 103 | if eq.syncCfg.EngineSync { 104 | // Allow SYNCING if engine P2P sync is enabled 105 | return status == eth.ExecutionValid || status == eth.ExecutionSyncing 106 | } 107 | return status == eth.ExecutionValid 108 | } 109 | ``` 110 | 111 | 让我们把视角转到op-geth的 `eth/catalyst/api.go`当中,当parent区块缺失后,触发sync,并且返回SYNCING Status 112 | 113 | ```go 114 | func (api *ConsensusAPI) newPayload(params engine.ExecutableData) (engine.PayloadStatusV1, error) { 115 | … 116 | // If the parent is missing, we - in theory - could trigger a sync, but that 117 | // would also entail a reorg. That is problematic if multiple sibling blocks 118 | // are being fed to us, and even more so, if some semi-distant uncle shortens 119 | // our live chain. As such, payload execution will not permit reorgs and thus 120 | // will not trigger a sync cycle. That is fine though, if we get a fork choice 121 | // update after legit payload executions. 122 | parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1) 123 | if parent == nil { 124 | return api.delayPayloadImport(block) 125 | } 126 | … 127 | } 128 | ``` 129 | 130 | ```go 131 | func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (engine.PayloadStatusV1, error) { 132 | … 133 | if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil { 134 | log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash()) 135 | return engine.PayloadStatusV1{Status: engine.SYNCING}, nil 136 | } 137 | … 138 | } 139 | ``` 140 | #### SkipSyncStartCheck 141 | SkipSyncStartCheck这个标识符主要是帮助在选择sync模式下,优化性能和减少不必要的检查。在已确认找到一个符合条件的L2块后,代码会跳过进一步的健全性检查,以加速同步或其他后续处理。这是一种优化手段,用于在确定性高的情况下快速地进行操作。 142 | 143 | 在`op-node/rollup/sync/start.go`目录中 144 | 145 | FindL2Heads函数通过从给定的“开始”(start)点(即之前的不安全L2区块)开始逐步回溯,来查找这三种类型的区块。在回溯过程中,该函数会检查各个L2区块的L1源是否与已知的L1规范链匹配,以及是否符合其他一些条件和检查。这允许函数更快地确定L2的“安全”头部,从而可能加速整个同步过程。 146 | 147 | ```go 148 | func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain, lgr log.Logger, syncCfg *Config) (result *FindHeadsResult, err error) { 149 | … 150 | for { 151 | 152 | … 153 | 154 | if syncCfg.SkipSyncStartCheck && highestL2WithCanonicalL1Origin.Hash == n.Hash { 155 | lgr.Info("Found highest L2 block with canonical L1 origin. Skip further sanity check and jump to the safe head") 156 | n = result.Safe 157 | continue 158 | } 159 | // Pull L2 parent for next iteration 160 | parent, err := l2.L2BlockRefByHash(ctx, n.ParentHash) 161 | if err != nil { 162 | return nil, fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.ParentHash, err) 163 | } 164 | 165 | // Check the L1 origin relation 166 | if parent.L1Origin != n.L1Origin { 167 | // sanity check that the L1 origin block number is coherent 168 | if parent.L1Origin.Number+1 != n.L1Origin.Number { 169 | return nil, fmt.Errorf("l2 parent %s of %s has L1 origin %s that is not before %s", parent, n, parent.L1Origin, n.L1Origin) 170 | } 171 | // sanity check that the later sequence number is 0, if it changed between the L2 blocks 172 | if n.SequenceNumber != 0 { 173 | return nil, fmt.Errorf("l2 block %s has parent %s with different L1 origin %s, but non-zero sequence number %d", n, parent, parent.L1Origin, n.SequenceNumber) 174 | } 175 | // if the L1 origin is known to be canonical, then the parent must be too 176 | if l1Block.Hash == n.L1Origin.Hash && l1Block.ParentHash != parent.L1Origin.Hash { 177 | return nil, fmt.Errorf("parent L2 block %s has origin %s but expected %s", parent, parent.L1Origin, l1Block.ParentHash) 178 | } 179 | } else { 180 | if parent.SequenceNumber+1 != n.SequenceNumber { 181 | return nil, fmt.Errorf("sequence number inconsistency %d <> %d between l2 blocks %s and %s", parent.SequenceNumber, n.SequenceNumber, parent, n) 182 | } 183 | } 184 | 185 | n = parent 186 | 187 | // once we found the block at seq nr 0 that is more than a full seq window behind the common chain post-reorg, then use the parent block as safe head. 188 | if ready { 189 | result.Safe = n 190 | return result, nil 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | 197 | ### op-node RPC 同步 198 | 199 | 这种同步场景处于: 当你有信任的l2 rpc节点的时候,我们可以直接和rpc通信,发送较短范围的同步请求,和2类似。如果设置,在反向链同步中会优先使用RPC而不是P2P同步。 200 | 201 | #### 关键代码 202 | 203 | `op-node/node/node.go` 204 | 205 | 初始化rpcSync,如果rpcSyncClient设置,赋值给rpcSync 206 | 207 | ```go 208 | func (n *OpNode) initRPCSync(ctx context.Context, cfg *Config) error { 209 | rpcSyncClient, rpcCfg, err := cfg.L2Sync.Setup(ctx, n.log, &cfg.Rollup) 210 | if err != nil { 211 | return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err) 212 | } 213 | if rpcSyncClient == nil { // if no RPC client is configured to sync from, then don't add the RPC sync client 214 | return nil 215 | } 216 | syncClient, err := sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, rpcCfg) 217 | if err != nil { 218 | return fmt.Errorf("failed to create sync client: %w", err) 219 | } 220 | n.rpcSync = syncClient 221 | return nil 222 | } 223 | ``` 224 | 225 | 启动node,如果rpcSync非空,开启rpcSync eventloop 226 | 227 | ```go 228 | func (n *OpNode) Start(ctx context.Context) error { 229 | n.log.Info("Starting execution engine driver") 230 | 231 | // start driving engine: sync blocks by deriving them from L1 and driving them into the engine 232 | if err := n.l2Driver.Start(); err != nil { 233 | n.log.Error("Could not start a rollup node", "err", err) 234 | return err 235 | } 236 | 237 | // If the backup unsafe sync client is enabled, start its event loop 238 | if n.rpcSync != nil { 239 | if err := n.rpcSync.Start(); err != nil { 240 | n.log.Error("Could not start the backup sync client", "err", err) 241 | return err 242 | } 243 | n.log.Info("Started L2-RPC sync service") 244 | } 245 | 246 | return nil 247 | } 248 | ``` 249 | 250 | `op-node/sources/sync_client.go` 251 | 252 | 一旦接收到s.requests通道里的信号后(区块号),调用fetchUnsafeBlockFromRpc函数从RPC节点中获取相应的区块信息。 253 | 254 | ```go 255 | // eventLoop is the main event loop for the sync client. 256 | func (s *SyncClient) eventLoop() { 257 | defer s.wg.Done() 258 | s.log.Info("Starting sync client event loop") 259 | 260 | backoffStrategy := &retry.ExponentialStrategy{ 261 | Min: 1000 * time.Millisecond, 262 | Max: 20_000 * time.Millisecond, 263 | MaxJitter: 250 * time.Millisecond, 264 | } 265 | 266 | for { 267 | select { 268 | case <-s.resCtx.Done(): 269 | s.log.Debug("Shutting down RPC sync worker") 270 | return 271 | case reqNum := <-s.requests: 272 | _, err := retry.Do(s.resCtx, 5, backoffStrategy, func() (interface{}, error) { 273 | // Limit the maximum time for fetching payloads 274 | ctx, cancel := context.WithTimeout(s.resCtx, time.Second*10) 275 | defer cancel() 276 | // We are only fetching one block at a time here. 277 | return nil, s.fetchUnsafeBlockFromRpc(ctx, reqNum) 278 | }) 279 | if err != nil { 280 | if err == s.resCtx.Err() { 281 | return 282 | } 283 | s.log.Error("failed syncing L2 block via RPC", "err", err, "num", reqNum) 284 | // Reschedule at end of queue 285 | select { 286 | case s.requests <- reqNum: 287 | default: 288 | // drop syncing job if we are too busy with sync jobs already. 289 | } 290 | } 291 | } 292 | } 293 | } 294 | ``` 295 | 296 | 接下来我们来看看从哪里往`s.requests`通道发送信号的呢? 297 | 同文件下的`RequestL2Range`函数,此函数介绍一个需要同步的区块范围,然后将任务通过for循环,分别发送出去。 298 | ```go 299 | func (s *SyncClient) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { 300 | // Drain previous requests now that we have new information 301 | for len(s.requests) > 0 { 302 | select { // in case requests is being read at the same time, don't block on draining it. 303 | case <-s.requests: 304 | default: 305 | break 306 | } 307 | } 308 | 309 | endNum := end.Number 310 | if end == (eth.L2BlockRef{}) { 311 | n, err := s.rollupCfg.TargetBlockNumber(uint64(time.Now().Unix())) 312 | if err != nil { 313 | return err 314 | } 315 | if n <= start.Number { 316 | return nil 317 | } 318 | endNum = n 319 | } 320 | 321 | // TODO(CLI-3635): optimize the by-range fetching with the Engine API payloads-by-range method. 322 | 323 | s.log.Info("Scheduling to fetch trailing missing payloads from backup RPC", "start", start, "end", endNum, "size", endNum-start.Number-1) 324 | 325 | for i := start.Number + 1; i < endNum; i++ { 326 | select { 327 | case s.requests <- i: 328 | case <-ctx.Done(): 329 | return ctx.Err() 330 | } 331 | } 332 | return nil 333 | } 334 | ``` 335 | 336 | 在外层的OpNode类型的RequestL2Range实现方法里。可以清楚的看到rpcSync类型的反向链同步是优先选择的。 337 | 338 | ```go 339 | func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { 340 | if n.rpcSync != nil { 341 | return n.rpcSync.RequestL2Range(ctx, start, end) 342 | } 343 | if n.p2pNode != nil && n.p2pNode.AltSyncEnabled() { 344 | if unixTimeStale(start.Time, 12*time.Hour) { 345 | n.log.Debug("ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, "end", end, "start_time", start.Time) 346 | return nil 347 | } 348 | return n.p2pNode.RequestL2Range(ctx, start, end) 349 | } 350 | n.log.Debug("ignoring request to sync L2 range, no sync method available", "start", start, "end", end) 351 | return nil 352 | } 353 | ``` 354 | ## 总结 355 | 理解了这些同步方式后,我们知道了unsafe的payload(区块)究竟是怎么进行传递的。不同的sync模块对应着在不同场景下的区块数据传递。那么整个网络中如何一步步的将unsafe的区块变成safe区块,然后再进行finalized的呢?这些内容会在其他章节进行讲解。 -------------------------------------------------------------------------------- /sequencer/02-how-optimism-use-libp2p.md: -------------------------------------------------------------------------------- 1 | # optimism中的libp2p应用 2 | 3 | 在本节中,主要用于讲解optimism是如何使用libp2p来完成op-node中的p2p网络建立的。 4 | p2p网络主要是用于在不同的node中传递信息,例如sequencer完成unsafe的区块构建后,通过p2p的gossiphub的pub/sub进行传播。libp2p还处理了其他,例如网络,寻址等在p2p网络中的基础件层。 5 | 6 | ## 了解libp2p 7 | 8 | libp2p(简称来自“库对等”或“library peer-to-peer”)是一个面向对等(P2P)网络的框架,能够帮助开发P2P应用程序。它包含了一套协议、规范和库,使网络参与者(也称为“对等体”或“peers”)之间的P2P通信变得更为简便 ([source](https://docs.libp2p.io/introduction/what-is-libp2p))。libp2p最初是作为IPFS(InterPlanetary File System,星际文件系统)项目的一部分,后来它演变成了一个独立的项目,成为了分布式网络的模块化网络堆栈 ([source](https://proto.school/introduction-to-libp2p))。 9 | 10 | libp2p是IPFS社区的一个开源项目,欢迎广泛社区的贡献,包括帮助编写规范、编码实现以及创建示例和教程 ([source](https://libp2p.io/))。libp2p是由多个构建模块组成的,每个模块都有非常明确、有文档记录且经过测试的接口,使得它们可组合、可替换,因此可升级 ([source](https://medium.com/@mycoralhealth/understanding-ipfs-in-depth-5-6-what-is-libp2p-fd42ed27e656))。libp2p的模块化特性使得开发人员可以选择并使用仅对他们的应用程序必要的组件,从而在构建P2P网络应用程序时促进了灵活性和效率。 11 | 12 | ### 相关资源 13 | - [libp2p的官方文档](https://docs.libp2p.io/) 14 | - [libp2p的GitHub仓库](https://github.com/libp2p) 15 | - [在ProtoSchool上的libp2p简介](https://proto.school/introduction-to-libp2p) 16 | 17 | libp2p的模块化架构和开源特性为开发强大、可扩展和灵活的P2P应用程序提供了良好的环境,使其成为分布式网络和网络应用程序开发领域的重要参与者。 18 | 19 | 20 | ### libp2p实现方式 21 | 22 | 在使用libp2p时,你会需要实现和配置一些核心组件以构建你的P2P网络。以下是libp2p在应用中的一些主要实现方面: 23 | 24 | #### 1. **节点创建与配置**: 25 | - 创建和配置libp2p节点是最基本的步骤,这包括设置节点的网络地址、身份和其他基本参数。 26 | 关键使用代码: 27 | ```go 28 | libp2p.New() 29 | ``` 30 | 31 | #### 2. **传输协议**: 32 | - 选择和配置你的传输协议(例如TCP、WebSockets等)以确保节点之间的通信。 33 | 关键使用代码: 34 | ```go 35 | tcpTransport := tcp.NewTCPTransport() 36 | ``` 37 | 38 | #### 3. **多路复用和流控制**: 39 | - 实现多路复用来允许在单一的连接上处理多个并发的数据流。 40 | - 实现流量控制来管理数据的传输速率和处理速率。 41 | 关键使用代码: 42 | ```go 43 | yamuxTransport := yamux.New() 44 | ``` 45 | 46 | #### 4. **安全和加密**: 47 | - 配置安全传输层以确保通信的安全性和隐私。 48 | - 实现加密和身份验证机制以保护数据和验证通信方。 49 | 关键使用代码: 50 | ```go 51 | tlsTransport := tls.New() 52 | ``` 53 | 54 | #### 5. **协议和消息处理**: 55 | - 定义和实现自定义协议来处理特定的网络操作和消息交换。 56 | - 处理接收到的消息并根据需要发送响应。 57 | 关键使用代码: 58 | ```go 59 | host.SetStreamHandler("/my-protocol/1.0.0", myProtocolHandler) 60 | ``` 61 | 62 | #### 6. **发现和路由**: 63 | - 实现节点发现机制来找到网络中的其他节点。 64 | - 实现路由逻辑以确定如何将消息路由到网络中的正确节点。 65 | 关键使用代码: 66 | ```go 67 | dht := kaddht.NewDHT(ctx, host, datastore.NewMapDatastore()) 68 | ``` 69 | 70 | #### 7. **网络行为和策略**: 71 | - 定义和实现网络的行为和策略,例如连接管理、错误处理和重试逻辑。 72 | 关键使用代码: 73 | ```go 74 | connManager := connmgr.NewConnManager(lowWater, highWater, gracePeriod) 75 | ``` 76 | 77 | #### 8. **状态管理和存储**: 78 | - 管理节点和网络的状态,包括连接状态、节点列表和数据存储。 79 | 关键使用代码: 80 | ```go 81 | peerstore := pstoremem.NewPeerstore() 82 | ``` 83 | 84 | #### 9. **测试和调试**: 85 | - 为你的libp2p应用编写测试以确保其正确性和可靠性。 86 | - 使用调试工具和日志来诊断和解决网络问题。 87 | 关键使用代码: 88 | ```go 89 | logging.SetLogLevel("libp2p", "DEBUG") 90 | ``` 91 | 92 | #### 10. **文档和社区支持**: 93 | - 查阅libp2p的文档以了解其各种组件和API。 94 | - 与libp2p社区交流以获取支持和解决问题。 95 | } 96 | 97 | 98 | 以上是使用libp2p时需要考虑和实现的一些主要方面。每个项目的具体实现可能会有所不同,但这些基本方面是构建和运行libp2p应用所必需的。在实现这些功能时,可以参考libp2p的[官方文档](https://docs.libp2p.io/)和[GitHub仓库](https://github.com/libp2p/go-libp2p/tree/master/examples)中的示例代码和教程。 99 | 100 | 101 | ## 在OP-node中libp2p的使用 102 | 103 | 为了弄清楚op-node和libp2p的关系,我们必须弄清楚几个问题 104 | 105 | - 为什么选择libp2p?为什么不选择devp2p(geth使用devp2p) 106 | - OP-node有哪些数据或者流程和p2p网络紧密相关 107 | - 这些功能是如何在代码层实现的 108 | 109 | ### op-node需要libp2p网络的原因 110 | 111 | **首先我们要了解为什么optimism需要p2p网络** 112 | libp2p是一个模块化的网络协议,允许开发人员构建去中心化的点对点应用,适用于多种用例 ([source](https://ethereum.stackexchange.com/questions/12290/what-is-the-distinction-between-libp2p-devp2p-and-rlpx))([source](https://www.geeksforgeeks.org/what-is-the-difference-between-libp2p-devp2p-and-rlpx/))。而devp2p主要用于以太坊生态系统,专为以太坊应用定制 ([source](https://docs.libp2p.io/concepts/similar-projects/devp2p/))。libp2p的灵活性和广泛适用性可能使其成为开发人员的首选。 113 | 114 | ### op-node主要使用libp2p的功能点 115 | 116 | - 用于sequencer将产生的unsafe的block传递到其他非sequencer节点 117 | - 用于非sequencer模式下的其他节点当出现gap时进行快速同步(反向链同步) 118 | - 用于采用积分声誉系统来规范整体节点的良好环境 119 | 120 | ### 代码实现 121 | 122 | #### host自定义初始化 123 | 124 | host可以理解为是p2p的节点,当开启这个节点的时候,需要针对自己的项目进行一些特殊的初始化配置 125 | 126 | 现在让我们看一下 `op-node/p2p/host.go`文件中的`Host`方法, 127 | 128 | 该函数主要用于设置 libp2p 主机并进行各种配置。以下是该函数的关键部分以及各部分的简单中文描述: 129 | 130 | 1. **检查是否禁用 P2P** 131 | 如果 P2P 被禁用,函数会直接返回。 132 | 133 | 2. **从公钥获取 Peer ID** 134 | 使用配置中的公钥来生成 Peer ID。 135 | 136 | 3. **初始化 Peerstore** 137 | 创建一个基础的 Peerstore 存储。 138 | 139 | 4. **初始化扩展 Peerstore** 140 | 在基础 Peerstore 的基础上,创建一个扩展的 Peerstore。 141 | 142 | 5. **将私钥和公钥添加到 Peerstore** 143 | 在 Peerstore 中存储 Peer 的私钥和公钥。 144 | 145 | 6. **初始化连接控制器(Connection Gater)** 146 | 用于控制网络连接。 147 | 148 | 7. **初始化连接管理器(Connection Manager)** 149 | 用于管理网络连接。 150 | 151 | 8. **设置传输和监听地址** 152 | 设置网络传输协议和主机的监听地址。 153 | 154 | 9. **创建 libp2p 主机** 155 | 使用前面的所有设置来创建一个新的 libp2p 主机。 156 | 157 | 10. **初始化静态 Peer** 158 | 如果有配置静态 Peer,进行初始化。 159 | 160 | 11. **返回主机** 161 | 最后,函数返回创建好的 libp2p 主机。 162 | 163 | 这些关键部分负责 libp2p 主机的初始化和设置,每个部分都负责主机配置的一个特定方面。 164 | 165 | 166 | ```go 167 | func (conf *Config) Host(log log.Logger, reporter metrics.Reporter, metrics HostMetrics) (host.Host, error) { 168 | if conf.DisableP2P { 169 | return nil, nil 170 | } 171 | pub := conf.Priv.GetPublic() 172 | pid, err := peer.IDFromPublicKey(pub) 173 | if err != nil { 174 | return nil, fmt.Errorf("failed to derive pubkey from network priv key: %w", err) 175 | } 176 | 177 | basePs, err := pstoreds.NewPeerstore(context.Background(), conf.Store, pstoreds.DefaultOpts()) 178 | if err != nil { 179 | return nil, fmt.Errorf("failed to open peerstore: %w", err) 180 | } 181 | 182 | peerScoreParams := conf.PeerScoringParams() 183 | var scoreRetention time.Duration 184 | if peerScoreParams != nil { 185 | // Use the same retention period as gossip will if available 186 | scoreRetention = peerScoreParams.PeerScoring.RetainScore 187 | } else { 188 | // Disable score GC if peer scoring is disabled 189 | scoreRetention = 0 190 | } 191 | ps, err := store.NewExtendedPeerstore(context.Background(), log, clock.SystemClock, basePs, conf.Store, scoreRetention) 192 | if err != nil { 193 | return nil, fmt.Errorf("failed to open extended peerstore: %w", err) 194 | } 195 | 196 | if err := ps.AddPrivKey(pid, conf.Priv); err != nil { 197 | return nil, fmt.Errorf("failed to set up peerstore with priv key: %w", err) 198 | } 199 | if err := ps.AddPubKey(pid, pub); err != nil { 200 | return nil, fmt.Errorf("failed to set up peerstore with pub key: %w", err) 201 | } 202 | 203 | var connGtr gating.BlockingConnectionGater 204 | connGtr, err = gating.NewBlockingConnectionGater(conf.Store) 205 | if err != nil { 206 | return nil, fmt.Errorf("failed to open connection gater: %w", err) 207 | } 208 | connGtr = gating.AddBanExpiry(connGtr, ps, log, clock.SystemClock, metrics) 209 | connGtr = gating.AddMetering(connGtr, metrics) 210 | 211 | connMngr, err := DefaultConnManager(conf) 212 | if err != nil { 213 | return nil, fmt.Errorf("failed to open connection manager: %w", err) 214 | } 215 | 216 | listenAddr, err := addrFromIPAndPort(conf.ListenIP, conf.ListenTCPPort) 217 | if err != nil { 218 | return nil, fmt.Errorf("failed to make listen addr: %w", err) 219 | } 220 | tcpTransport := libp2p.Transport( 221 | tcp.NewTCPTransport, 222 | tcp.WithConnectionTimeout(time.Minute*60)) // break unused connections 223 | // TODO: technically we can also run the node on websocket and QUIC transports. Maybe in the future? 224 | 225 | var nat lconf.NATManagerC // disabled if nil 226 | if conf.NAT { 227 | nat = basichost.NewNATManager 228 | } 229 | 230 | opts := []libp2p.Option{ 231 | libp2p.Identity(conf.Priv), 232 | // Explicitly set the user-agent, so we can differentiate from other Go libp2p users. 233 | libp2p.UserAgent(conf.UserAgent), 234 | tcpTransport, 235 | libp2p.WithDialTimeout(conf.TimeoutDial), 236 | // No relay services, direct connections between peers only. 237 | libp2p.DisableRelay(), 238 | // host will start and listen to network directly after construction from config. 239 | libp2p.ListenAddrs(listenAddr), 240 | libp2p.ConnectionGater(connGtr), 241 | libp2p.ConnectionManager(connMngr), 242 | //libp2p.ResourceManager(nil), // TODO use resource manager interface to manage resources per peer better. 243 | libp2p.NATManager(nat), 244 | libp2p.Peerstore(ps), 245 | libp2p.BandwidthReporter(reporter), // may be nil if disabled 246 | libp2p.MultiaddrResolver(madns.DefaultResolver), 247 | // Ping is a small built-in libp2p protocol that helps us check/debug latency between peers. 248 | libp2p.Ping(true), 249 | // Help peers with their NAT reachability status, but throttle to avoid too much work. 250 | libp2p.EnableNATService(), 251 | libp2p.AutoNATServiceRateLimit(10, 5, time.Second*60), 252 | } 253 | opts = append(opts, conf.HostMux...) 254 | if conf.NoTransportSecurity { 255 | opts = append(opts, libp2p.Security(insecure.ID, insecure.NewWithIdentity)) 256 | } else { 257 | opts = append(opts, conf.HostSecurity...) 258 | } 259 | h, err := libp2p.New(opts...) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | staticPeers := make([]*peer.AddrInfo, len(conf.StaticPeers)) 265 | for i, peerAddr := range conf.StaticPeers { 266 | addr, err := peer.AddrInfoFromP2pAddr(peerAddr) 267 | if err != nil { 268 | return nil, fmt.Errorf("bad peer address: %w", err) 269 | } 270 | staticPeers[i] = addr 271 | } 272 | 273 | out := &extraHost{ 274 | Host: h, 275 | connMgr: connMngr, 276 | log: log, 277 | staticPeers: staticPeers, 278 | quitC: make(chan struct{}), 279 | } 280 | out.initStaticPeers() 281 | if len(conf.StaticPeers) > 0 { 282 | go out.monitorStaticPeers() 283 | } 284 | 285 | out.gater = connGtr 286 | return out, nil 287 | } 288 | ``` 289 | 290 | #### gossip下的区块传播 291 | 292 | gossip在分布式系统中用于确保数据一致性,并修复由多播引起的问题。它是一种通信协议,其中信息从一个或多个节点发送到网络中的其他节点集,当网络中的一组客户端同时需要相同的数据时,这会很有用。当sequencer产生出unsafe状态的区块的时候,就是通过gossip网络传递给其他节点的。 293 | 294 | 首先让我们来看看节点是在哪里加入gossip网络的, 295 | `op-node/p2p/node.go`中的`init`方法,在节点初始化的时候,调用JoinGossip方法加入了gossip网络 296 | 297 | ```go 298 | func (n *NodeP2P) init(resourcesCtx context.Context, rollupCfg *rollup.Config, log log.Logger, setup SetupP2P, gossipIn GossipIn, l2Chain L2Chain, runCfg GossipRuntimeConfig, metrics metrics.Metricer) error { 299 | … 300 | // note: the IDDelta functionality was removed from libP2P, and no longer needs to be explicitly disabled. 301 | n.gs, err = NewGossipSub(resourcesCtx, n.host, rollupCfg, setup, n.scorer, metrics, log) 302 | if err != nil { 303 | return fmt.Errorf("failed to start gossipsub router: %w", err) 304 | } 305 | n.gsOut, err = JoinGossip(resourcesCtx, n.host.ID(), n.gs, log, rollupCfg, runCfg, gossipIn) 306 | … 307 | } 308 | ``` 309 | 310 | 接下来来到`op-node/p2p/gossip.go`中 311 | 312 | 以下是 `JoinGossip` 函数中执行的主要操作的简单概述: 313 | 314 | 1. **验证器创建**: 315 | - `val` 被赋予 `guardGossipValidator` 函数调用的结果,目的是为八卦消息创建验证器,该验证器检查网络中传播的区块的有效性。 316 | 317 | 2. **区块主题名称生成**: 318 | - 使用 `blocksTopicV1` 函数生成 `blocksTopicName`,该函数根据配置(`cfg`)中的 `L2ChainID` 格式化字符串。格式化的字符串遵循特定的结构:`/optimism/{L2ChainID}/0/blocks`。 319 | 320 | 3. **主题验证器注册**: 321 | - 调用 `ps` 的 `RegisterTopicValidator` 方法,以将 `val` 注册为区块主题的验证器。还指定了一些验证器的配置选项,例如3秒的超时和4的并发级别。 322 | 323 | 4. **加入主题**: 324 | - 函数通过调用 `ps.Join(blocksTopicName)` 尝试加入区块八卦主题。如果出现错误,它将返回一个错误消息,指示无法加入主题。 325 | 326 | 5. **事件处理器创建**: 327 | - 通过调用 `blocksTopic.EventHandler()` 尝试为区块主题创建事件处理器。如果出现错误,它将返回一个错误消息,指示无法创建处理器。 328 | 329 | 6. **记录主题事件**: 330 | - 生成了一个新的goroutine来使用 `LogTopicEvents` 函数记录主题事件。 331 | 332 | 7. **主题订阅**: 333 | - 函数通过调用 `blocksTopic.Subscribe()` 尝试订阅区块八卦主题。如果出现错误,它将返回一个错误消息,指示无法订阅。 334 | 335 | 8. **订阅者创建**: 336 | - 使用 `MakeSubscriber` 函数创建了一个 `subscriber`,该函数封装了一个 `BlocksHandler`,该处理器处理来自 `gossipIn` 的 `OnUnsafeL2Payload` 事件。生成了一个新的goroutine来运行提供的 `subscription`。 337 | 338 | 9. **创建并返回发布者**: 339 | - 创建了一个 `publisher` 实例并返回,该实例配置为使用提供的配置和区块主题。 340 | 341 | ```go 342 | func JoinGossip(p2pCtx context.Context, self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) { 343 | val := guardGossipValidator(log, logValidationResult(self, "validated block", log, BuildBlocksValidator(log, cfg, runCfg))) 344 | blocksTopicName := blocksTopicV1(cfg) // return fmt.Sprintf("/optimism/%s/0/blocks", cfg.L2ChainID.String()) 345 | err := ps.RegisterTopicValidator(blocksTopicName, 346 | val, 347 | pubsub.WithValidatorTimeout(3*time.Second), 348 | pubsub.WithValidatorConcurrency(4)) 349 | if err != nil { 350 | return nil, fmt.Errorf("failed to register blocks gossip topic: %w", err) 351 | } 352 | blocksTopic, err := ps.Join(blocksTopicName) 353 | if err != nil { 354 | return nil, fmt.Errorf("failed to join blocks gossip topic: %w", err) 355 | } 356 | blocksTopicEvents, err := blocksTopic.EventHandler() 357 | if err != nil { 358 | return nil, fmt.Errorf("failed to create blocks gossip topic handler: %w", err) 359 | } 360 | go LogTopicEvents(p2pCtx, log.New("topic", "blocks"), blocksTopicEvents) 361 | 362 | subscription, err := blocksTopic.Subscribe() 363 | if err != nil { 364 | return nil, fmt.Errorf("failed to subscribe to blocks gossip topic: %w", err) 365 | } 366 | 367 | subscriber := MakeSubscriber(log, BlocksHandler(gossipIn.OnUnsafeL2Payload)) 368 | go subscriber(p2pCtx, subscription) 369 | 370 | return &publisher{log: log, cfg: cfg, blocksTopic: blocksTopic, runCfg: runCfg}, nil 371 | } 372 | ``` 373 | 374 | 这样,一个非sequencer节点的订阅就已经建立了,接下来让我们把目光移到sequencer模式的节点当中,然后看看他是如果将区块广播出去的。 375 | 376 | `op-node/rollup/driver/state.go` 377 | 378 | 在eventloop中通过循环来等待sequencer模式中新的payload的产生(unsafe区块),然后将这个payload通过PublishL2Payload传播到gossip网络中 379 | ```go 380 | func (s *Driver) eventLoop() { 381 | … 382 | for(){ 383 | … 384 | select { 385 | case <-sequencerCh: 386 | payload, err := s.sequencer.RunNextSequencerAction(ctx) 387 | if err != nil { 388 | s.log.Error("Sequencer critical error", "err", err) 389 | return 390 | } 391 | if s.network != nil && payload != nil { 392 | // Publishing of unsafe data via p2p is optional. 393 | // Errors are not severe enough to change/halt sequencing but should be logged and metered. 394 | if err := s.network.PublishL2Payload(ctx, payload); err != nil { 395 | s.log.Warn("failed to publish newly created block", "id", payload.ID(), "err", err) 396 | s.metrics.RecordPublishingError() 397 | } 398 | } 399 | planSequencerAction() // schedule the next sequencer action to keep the sequencing looping 400 | … 401 | } 402 | … 403 | } 404 | … 405 | } 406 | ``` 407 | 这样,一个新的payload的就进入到gossip网络中了。 408 | 409 | 在libp2p的pubsub系统中,节点首先从其他节点接收消息,然后检查消息的有效性。如果消息有效并且符合节点的订阅标准,节点会考虑将其转发给其他节点。基于某些策略,如网络拓扑和节点的订阅情况,节点会决定是否将消息转发给其它节点。如果决定转发,节点会将消息发送给与其连接并订阅了相同主题的所有节点。在转发过程中,为防止消息在网络中无限循环,通常会有机制来跟踪已转发的消息,并确保不会多次转发同一消息。同时,消息可能具有“生存时间”(TTL)属性,定义了消息可以在网络中转发的次数或时间,每当消息被转发时,TTL值都会递减,直到消息不再被转发为止。在验证方面,消息通常会通过一些验证过程,例如检查消息的签名和格式,以确保消息的完整性和真实性。在libp2p的pubsub模型中,这个过程确保了消息能够广泛传播到网络中的许多节点,同时避免了无限循环和网络拥塞,实现了有效的消息传递和处理。 410 | 411 | #### 区块的校验 412 | 413 | 与 L1 类似,节点在接收区块时会进行校验。Optimism 的主要区别在于,在 L1 中,验证的是多个选定的 beacon 节点的签名,而在 Optimism 中,只验证 sequencer 节点的签名。 414 | 415 | 以签署和验证签名的过程为例: 416 | 417 | - 当 sequencer 通过 P2P 网络发布区块时,[sequencer 会对区块进行签名。](https://github.com/ethereum-optimism/optimism/blob/c5007bb4be66e08b9e4db51c72096912d69eeb0c/op-node/p2p/gossip.go#L547) 418 | 419 | ```golang 420 | 421 | func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error { 422 | …… 423 | sig, err := signer.Sign(ctx, SigningDomainBlocksV1, p.cfg.L2ChainID, payloadData) 424 | if err != nil { 425 | return fmt.Errorf("failed to sign execution payload with signer: %w", err) 426 | } 427 | copy(data[:65], sig[:]) 428 | 429 | …… 430 | 431 | ``` 432 | - 当验证者接收到区块时,[将检查签名者是否为 sequencer 的签名地址。](https://github.com/ethereum-optimism/optimism/blob/c5007bb4be66e08b9e4db51c72096912d69eeb0c/op-node/p2p/gossip.go#L434) 433 | 434 | ```golang 435 | func verifyBlockSignature(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, id peer.ID, signatureBytes []byte, payloadBytes []byte) pubsub.ValidationResult { 436 | signingHash, err := BlockSigningHash(cfg, payloadBytes) 437 | if err != nil { 438 | log.Warn("failed to compute block signing hash", "err", err, "peer", id) 439 | return pubsub.ValidationReject 440 | } 441 | 442 | pub, err := crypto.SigToPub(signingHash[:], signatureBytes) 443 | if err != nil { 444 | log.Warn("invalid block signature", "err", err, "peer", id) 445 | return pubsub.ValidationReject 446 | } 447 | addr := crypto.PubkeyToAddress(*pub) 448 | 449 | // In the future we may load & validate block metadata before checking the signature. 450 | // And then check the signer based on the metadata, to support e.g. multiple p2p signers at the same time. 451 | // For now we only have one signer at a time and thus check the address directly. 452 | // This means we may drop old payloads upon key rotation, 453 | // but this can be recovered from like any other missed unsafe payload. 454 | if expected := runCfg.P2PSequencerAddress(); expected == (common.Address{}) { 455 | log.Warn("no configured p2p sequencer address, ignoring gossiped block", "peer", id, "addr", addr) 456 | return pubsub.ValidationIgnore 457 | } else if addr != expected { 458 | log.Warn("unexpected block author", "err", err, "peer", id, "addr", addr, "expected", expected) 459 | return pubsub.ValidationReject 460 | } 461 | return pubsub.ValidationAccept 462 | } 463 | ``` 464 | 465 | #### 当存在缺失区块,通过p2p快速同步 466 | 467 | 当节点因为特殊情况,比如宕机后重新链接,可能会产生一些没有同步上的区块(gaps),当遇到这种情况时,可以通过p2p网络的反向链的方式快速同步。 468 | 469 | 我们来看一下`op-node/rollup/driver/state.go`中的`checkForGapInUnsafeQueue`函数 470 | 471 | 该代码段定义了一个名为 `checkForGapInUnsafeQueue` 的方法,属于 `Driver` 结构体。它的目的是检查一个名为 "unsafe queue" 的队列中是否存在数据缺口,并尝试通过一个名为 `altSync` 的备用同步方法来检索缺失的负载。这里的关键点是,该方法是为了确保数据的连续性,并在检测到数据缺失时尝试从其他同步方法中检索缺失的数据。以下是函数的主要步骤: 472 | 473 | 1. 函数首先从 `s.derivation` 中获取 `UnsafeL2Head` 和 `UnsafeL2SyncTarget` 作为检查范围的起始和结束点。 474 | 2. 函数检查在 `start` 和 `end` 之间是否存在缺失的数据块,这是通过比较 `end` 和 `start` 的 `Number` 值来完成的。 475 | 3. 如果检测到数据缺口,函数会通过调用 `s.altSync.RequestL2Range(ctx, start, end)` 来请求缺失的数据范围。如果 `end` 是一个空引用(即 `eth.L2BlockRef{}`),函数将请求一个开放结束范围的同步,从 `start` 开始。 476 | 4. 在请求数据时,函数会记录一个调试日志,说明它正在请求哪个范围的数据。 477 | 5. 函数最终返回一个错误值。如果没有错误,它会返回 `nil` 478 | 479 | ```go 480 | // checkForGapInUnsafeQueue checks if there is a gap in the unsafe queue and attempts to retrieve the missing payloads from an alt-sync method. 481 | // WARNING: This is only an outgoing signal, the blocks are not guaranteed to be retrieved. 482 | // Results are received through OnUnsafeL2Payload. 483 | func (s *Driver) checkForGapInUnsafeQueue(ctx context.Context) error { 484 | start := s.derivation.UnsafeL2Head() 485 | end := s.derivation.UnsafeL2SyncTarget() 486 | // Check if we have missing blocks between the start and end. Request them if we do. 487 | if end == (eth.L2BlockRef{}) { 488 | s.log.Debug("requesting sync with open-end range", "start", start) 489 | return s.altSync.RequestL2Range(ctx, start, eth.L2BlockRef{}) 490 | } else if end.Number > start.Number+1 { 491 | s.log.Debug("requesting missing unsafe L2 block range", "start", start, "end", end, "size", end.Number-start.Number) 492 | return s.altSync.RequestL2Range(ctx, start, end) 493 | } 494 | return nil 495 | } 496 | ``` 497 | 498 | `RequestL2Range`函数向`requests`通道里传递请求区块的开始和结束信号。 499 | 500 | 然后通过`onRangeRequest`方法来对请求向`peerRequests`通道分发,`peerRequests`通道会被多个peer开启的loop所等待,即每一次分发都只有一个peer会去处理这个request。 501 | ```go 502 | func (s *SyncClient) onRangeRequest(ctx context.Context, req rangeRequest) { 503 | … 504 | for i := uint64(0); ; i++ { 505 | num := req.end.Number - 1 - i 506 | if num <= req.start { 507 | return 508 | } 509 | // check if we have something in quarantine already 510 | if h, ok := s.quarantineByNum[num]; ok { 511 | if s.trusted.Contains(h) { // if we trust it, try to promote it. 512 | s.tryPromote(h) 513 | } 514 | // Don't fetch things that we have a candidate for already. 515 | // We'll evict it from quarantine by finding a conflict, or if we sync enough other blocks 516 | continue 517 | } 518 | 519 | if _, ok := s.inFlight[num]; ok { 520 | log.Debug("request still in-flight, not rescheduling sync request", "num", num) 521 | continue // request still in flight 522 | } 523 | pr := peerRequest{num: num, complete: new(atomic.Bool)} 524 | 525 | log.Debug("Scheduling P2P block request", "num", num) 526 | // schedule number 527 | select { 528 | case s.peerRequests <- pr: 529 | s.inFlight[num] = pr.complete 530 | case <-ctx.Done(): 531 | log.Info("did not schedule full P2P sync range", "current", num, "err", ctx.Err()) 532 | return 533 | default: // peers may all be busy processing requests already 534 | log.Info("no peers ready to handle block requests for more P2P requests for L2 block history", "current", num) 535 | return 536 | } 537 | } 538 | } 539 | ``` 540 | 541 | 接下来我们看看,当peer收到这个request的时候会怎么处理。 542 | 543 | 首先我们要知道的是,peer和请求节点之间的链接,或者消息传递是通过libp2p的stream来传递的。stream的处理方法由接收peer节点实现,stream的创建由发送节点来开启。 544 | 545 | 我们可以在之前的init函数中看到这样的代码,这里MakeStreamHandler返回了一个处理函数,SetStreamHandler将协议id和这个处理函数绑定,因此,每当发送节点创建并使用这个stream的时候,都会触发返回的处理函数。 546 | 547 | ```go 548 | n.syncSrv = NewReqRespServer(rollupCfg, l2Chain, metrics) 549 | // register the sync protocol with libp2p host 550 | payloadByNumber := MakeStreamHandler(resourcesCtx, log.New("serve", "payloads_by_number"), n.syncSrv.HandleSyncRequest) 551 | n.host.SetStreamHandler(PayloadByNumberProtocolID(rollupCfg.L2ChainID), payloadByNumber) 552 | ``` 553 | 554 | 接下来让我们看看处理函数里面是如何处理的 555 | 函数首先进行全局和个人的速率限制检查,以控制处理请求的速度。然后,它读取并验证了请求的区块号,确保它在合理的范围内。之后,函数从 L2 层获取请求的区块负载,并将其写入到响应流中。在写入响应数据时,它设置了写入截止时间,以避免在写入过程中被慢速的 peer 连接阻塞。最终,函数返回请求的区块号和可能的错误。 556 | 557 | ```go 558 | func (srv *ReqRespServer) handleSyncRequest(ctx context.Context, stream network.Stream) (uint64, error) { 559 | peerId := stream.Conn().RemotePeer() 560 | 561 | // take a token from the global rate-limiter, 562 | // to make sure there's not too much concurrent server work between different peers. 563 | if err := srv.globalRequestsRL.Wait(ctx); err != nil { 564 | return 0, fmt.Errorf("timed out waiting for global sync rate limit: %w", err) 565 | } 566 | 567 | // find rate limiting data of peer, or add otherwise 568 | srv.peerStatsLock.Lock() 569 | ps, _ := srv.peerRateLimits.Get(peerId) 570 | if ps == nil { 571 | ps = &peerStat{ 572 | Requests: rate.NewLimiter(peerServerBlocksRateLimit, peerServerBlocksBurst), 573 | } 574 | srv.peerRateLimits.Add(peerId, ps) 575 | ps.Requests.Reserve() // count the hit, but make it delay the next request rather than immediately waiting 576 | } else { 577 | // Only wait if it's an existing peer, otherwise the instant rate-limit Wait call always errors. 578 | 579 | // If the requester thinks we're taking too long, then it's their problem and they can disconnect. 580 | // We'll disconnect ourselves only when failing to read/write, 581 | // if the work is invalid (range validation), or when individual sub tasks timeout. 582 | if err := ps.Requests.Wait(ctx); err != nil { 583 | return 0, fmt.Errorf("timed out waiting for global sync rate limit: %w", err) 584 | } 585 | } 586 | srv.peerStatsLock.Unlock() 587 | 588 | // Set read deadline, if available 589 | _ = stream.SetReadDeadline(time.Now().Add(serverReadRequestTimeout)) 590 | 591 | // Read the request 592 | var req uint64 593 | if err := binary.Read(stream, binary.LittleEndian, &req); err != nil { 594 | return 0, fmt.Errorf("failed to read requested block number: %w", err) 595 | } 596 | if err := stream.CloseRead(); err != nil { 597 | return req, fmt.Errorf("failed to close reading-side of a P2P sync request call: %w", err) 598 | } 599 | 600 | // Check the request is within the expected range of blocks 601 | if req < srv.cfg.Genesis.L2.Number { 602 | return req, fmt.Errorf("cannot serve request for L2 block %d before genesis %d: %w", req, srv.cfg.Genesis.L2.Number, invalidRequestErr) 603 | } 604 | max, err := srv.cfg.TargetBlockNumber(uint64(time.Now().Unix())) 605 | if err != nil { 606 | return req, fmt.Errorf("cannot determine max target block number to verify request: %w", invalidRequestErr) 607 | } 608 | if req > max { 609 | return req, fmt.Errorf("cannot serve request for L2 block %d after max expected block (%v): %w", req, max, invalidRequestErr) 610 | } 611 | 612 | payload, err := srv.l2.PayloadByNumber(ctx, req) 613 | if err != nil { 614 | if errors.Is(err, ethereum.NotFound) { 615 | return req, fmt.Errorf("peer requested unknown block by number: %w", err) 616 | } else { 617 | return req, fmt.Errorf("failed to retrieve payload to serve to peer: %w", err) 618 | } 619 | } 620 | 621 | // We set write deadline, if available, to safely write without blocking on a throttling peer connection 622 | _ = stream.SetWriteDeadline(time.Now().Add(serverWriteChunkTimeout)) 623 | 624 | // 0 - resultCode: success = 0 625 | // 1:5 - version: 0 626 | var tmp [5]byte 627 | if _, err := stream.Write(tmp[:]); err != nil { 628 | return req, fmt.Errorf("failed to write response header data: %w", err) 629 | } 630 | w := snappy.NewBufferedWriter(stream) 631 | if _, err := payload.MarshalSSZ(w); err != nil { 632 | return req, fmt.Errorf("failed to write payload to sync response: %w", err) 633 | } 634 | if err := w.Close(); err != nil { 635 | return req, fmt.Errorf("failed to finishing writing payload to sync response: %w", err) 636 | } 637 | return req, nil 638 | } 639 | ``` 640 | 641 | 至此,反向链同步请求和处理的大致流程已经讲解完毕 642 | 643 | #### p2p节点中的积分声誉系统 644 | 645 | 为了防止某些节点进行恶意的请求与响应来破坏整个网络的安全性,optimism还使用了一套积分系统。 646 | 647 | 例如在`op-node/p2p/app_scores.go` 中存在一系列函数对peer的分数进行设置 648 | 649 | ```go 650 | func (s *peerApplicationScorer) onValidResponse(id peer.ID) { 651 | _, err := s.scorebook.SetScore(id, store.IncrementValidResponses{Cap: s.params.ValidResponseCap}) 652 | if err != nil { 653 | s.log.Error("Unable to update peer score", "peer", id, "err", err) 654 | return 655 | } 656 | } 657 | 658 | func (s *peerApplicationScorer) onResponseError(id peer.ID) { 659 | _, err := s.scorebook.SetScore(id, store.IncrementErrorResponses{Cap: s.params.ErrorResponseCap}) 660 | if err != nil { 661 | s.log.Error("Unable to update peer score", "peer", id, "err", err) 662 | return 663 | } 664 | } 665 | 666 | func (s *peerApplicationScorer) onRejectedPayload(id peer.ID) { 667 | _, err := s.scorebook.SetScore(id, store.IncrementRejectedPayloads{Cap: s.params.RejectedPayloadCap}) 668 | if err != nil { 669 | s.log.Error("Unable to update peer score", "peer", id, "err", err) 670 | return 671 | } 672 | } 673 | ``` 674 | 675 | 然后在添加新的节点前会检查其积分情况 676 | 677 | ```go 678 | func AddScoring(gater BlockingConnectionGater, scores Scores, minScore float64) *ScoringConnectionGater { 679 | return &ScoringConnectionGater{BlockingConnectionGater: gater, scores: scores, minScore: minScore} 680 | } 681 | 682 | func (g *ScoringConnectionGater) checkScore(p peer.ID) (allow bool) { 683 | score, err := g.scores.GetPeerScore(p) 684 | if err != nil { 685 | return false 686 | } 687 | return score >= g.minScore 688 | } 689 | 690 | func (g *ScoringConnectionGater) InterceptPeerDial(p peer.ID) (allow bool) { 691 | return g.BlockingConnectionGater.InterceptPeerDial(p) && g.checkScore(p) 692 | } 693 | 694 | func (g *ScoringConnectionGater) InterceptAddrDial(id peer.ID, ma multiaddr.Multiaddr) (allow bool) { 695 | return g.BlockingConnectionGater.InterceptAddrDial(id, ma) && g.checkScore(id) 696 | } 697 | 698 | func (g *ScoringConnectionGater) InterceptSecured(dir network.Direction, id peer.ID, mas network.ConnMultiaddrs) (allow bool) { 699 | return g.BlockingConnectionGater.InterceptSecured(dir, id, mas) && g.checkScore(id) 700 | } 701 | ``` 702 | 703 | ### 总结 704 | libp2p的高度可配置性使得整个项目的p2p具有高度的可自定义化和模块话,以上是optimsim对libp2p进行个性化实现的主要逻辑,还有其他细节可以在p2p目录下通过阅读源码的方式来详细学习。 705 | -------------------------------------------------------------------------------- /sequencer/03-how-batcher-works.md: -------------------------------------------------------------------------------- 1 | # batcher工作原理 2 | 在这一章节中,我们将探讨到底什么是`batcher` ⚙️ 3 | 官方specs中有batcher的介绍([source](https://github.com/ethereum-optimism/optimism/blob/develop/specs/batcher.md)) 4 | 5 | 在进行之前,我们先提出几个问题,通过这两个问题来真正理解`batcher`的作用以及工作原理 6 | - `batcher`是什么?它为什么叫做`batcher` 7 | - `batcher`在代码中到底是怎么运行的? 8 | 9 | ## 前置知识 10 | - 在rollup机制中,要想做到的去中心化特性,例如抗审查等。我们必须要把layer2上发生的数据(transactions)全部发送到layer1当中。这样就可以在利用layer1的安全性的同时,又可以完全从layer1中构建出来整个layer2的数据,使得layer2才真正的具有有效性。 11 | - [Epochs and the Sequencing Window](https://github.com/ethereum-optimism/optimism/blob/develop/specs/overview.md#epochs-and-the-sequencing-window):`Epoch`可以简单理解为L1新的一个`区块(N+1)`生成的这段时间。`epoch`的编号等于L1`区块N`的编号,在L1区块`N -> N+1` 这段时间内产生的所有L2区块都属于`epoch N`。在上个概念中我们提到必须上传L2的数据到L1中,那么我们应该在什么范围内上传数据才是有效的呢,`Sequencing Window`的size给了我们答案,即区块N/epoch N的相关数据,必须在L1的第`N + size`之前已经上传到L1了。 12 | - Batch/Batcher Transaction: `Batch`可以简单理解为每一个L2区块构建所需要的交易。`Batcher Transaction`为多个batch组合起来经过加工后发送到L1的那笔交易 13 | - [Channe](https://github.com/ethereum-optimism/optimism/blob/develop/specs/glossary.md#channel): `channel`可以简单理解为是`batch`的组合,组合是为了获得更好的压缩率,从而降低数据可用性成本,以使`batcher`上传的成本进一步降低。 14 | - [Frame](https://github.com/ethereum-optimism/optimism/blob/develop/specs/glossary.md#channel-frame): `frame`可以理解为,有时候为了更好的压缩率,可能会导致`channel`数据过大而不能直接被`batcher`将整个`channel`发送给L1,因此需要对`channel`进行切割,分多次进行发送。 15 | 16 | ## 什么是batcher 17 | 在rollup中,需要一个角色来传递L2信息到L1当中,同时每当有新的交易就马上发送是昂贵且不方便管理的。这时候我们将需要制定一种合理的批量上传策略。因此,为了解决这个问题,batcher出现了。batcher是唯一存在(sequencer当前掌管私钥),且和特定地址发送`Batcher Transaction`来传递L2信息的组件。 18 | 19 | batcher通过对unsafe区块数据进行收集,来获取多个batch,在这里每个区块都对应一个batch。当收集足够的batch进行高效压缩后生成channel,并以frame的形式发送到L1来完成L2的信息上传。 20 | 21 | ## 代码实现 22 | 在这部分我们会从代码层来进行深度的机制和实现原理的讲解 23 | 24 | ### 程序起点 25 | `op-batcher/batcher/driver.go` 26 | 27 | 通过调用`Start`函数来启动`loop`循环,在`loop`的循环中,主要处理三件事 28 | - 当定时器触发时,将所有新的还未加载的`L2block`加载进来,然后触发`publishStateToL1`函数向L1进行`state`发布 29 | - 处理`receipts`,记录成功或者失败状态 30 | - 处理关闭请求 31 | 32 | ```go 33 | func (l *BatchSubmitter) Start() error { 34 | l.log.Info("Starting Batch Submitter") 35 | 36 | l.mutex.Lock() 37 | defer l.mutex.Unlock() 38 | 39 | if l.running { 40 | return errors.New("batcher is already running") 41 | } 42 | l.running = true 43 | 44 | l.shutdownCtx, l.cancelShutdownCtx = context.WithCancel(context.Background()) 45 | l.killCtx, l.cancelKillCtx = context.WithCancel(context.Background()) 46 | l.state.Clear() 47 | l.lastStoredBlock = eth.BlockID{} 48 | 49 | l.wg.Add(1) 50 | go l.loop() 51 | 52 | l.log.Info("Batch Submitter started") 53 | 54 | return nil 55 | } 56 | ``` 57 | 58 | ```go 59 | func (l *BatchSubmitter) loop() { 60 | defer l.wg.Done() 61 | 62 | ticker := time.NewTicker(l.PollInterval) 63 | defer ticker.Stop() 64 | 65 | receiptsCh := make(chan txmgr.TxReceipt[txData]) 66 | queue := txmgr.NewQueue[txData](l.killCtx, l.txMgr, l.MaxPendingTransactions) 67 | 68 | for { 69 | select { 70 | case <-ticker.C: 71 | if err := l.loadBlocksIntoState(l.shutdownCtx); errors.Is(err, ErrReorg) { 72 | err := l.state.Close() 73 | if err != nil { 74 | l.log.Error("error closing the channel manager to handle a L2 reorg", "err", err) 75 | } 76 | l.publishStateToL1(queue, receiptsCh, true) 77 | l.state.Clear() 78 | continue 79 | } 80 | l.publishStateToL1(queue, receiptsCh, false) 81 | case r := <-receiptsCh: 82 | l.handleReceipt(r) 83 | case <-l.shutdownCtx.Done(): 84 | err := l.state.Close() 85 | if err != nil { 86 | l.log.Error("error closing the channel manager", "err", err) 87 | } 88 | l.publishStateToL1(queue, receiptsCh, true) 89 | return 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ### 加载最新区块数据 96 | `op-batcher/batcher/driver.go` 97 | 98 | `loadBlocksIntoState`函数调用`calculateL2BlockRangeToStore`来获取自上次发送`batch transaction`而派生的最新`safeblock`后新生成的`unsafeblock`范围。然后循环将这个范围中的每一个`unsafe`块调用`loadBlockIntoState`函数从L2里获取并通过`AddL2Block`函数加载到内部的`block队列`里。等待进一步处理。 99 | 100 | ```go 101 | func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context) error { 102 | start, end, err := l.calculateL2BlockRangeToStore(ctx) 103 | …… 104 | var latestBlock *types.Block 105 | // Add all blocks to "state" 106 | for i := start.Number + 1; i < end.Number+1; i++ { 107 | block, err := l.loadBlockIntoState(ctx, i) 108 | if errors.Is(err, ErrReorg) { 109 | l.log.Warn("Found L2 reorg", "block_number", i) 110 | l.lastStoredBlock = eth.BlockID{} 111 | return err 112 | } else if err != nil { 113 | l.log.Warn("failed to load block into state", "err", err) 114 | return err 115 | } 116 | l.lastStoredBlock = eth.ToBlockID(block) 117 | latestBlock = block 118 | } 119 | …… 120 | } 121 | ``` 122 | 123 | ```go 124 | func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uint64) (*types.Block, error) { 125 | …… 126 | block, err := l.L2Client.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber)) 127 | …… 128 | if err := l.state.AddL2Block(block); err != nil { 129 | return nil, fmt.Errorf("adding L2 block to state: %w", err) 130 | } 131 | …… 132 | return block, nil 133 | } 134 | ``` 135 | ### 将加载的block数据处理,并发送到layer1 136 | `op-batcher/batcher/driver.go` 137 | 138 | `publishTxToL1`函数使用`TxData`函数对之前加载到数据进行处理,并调用`sendTransaction`函数发送到L1 139 | 140 | ```go 141 | func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData]) error { 142 | // send all available transactions 143 | l1tip, err := l.l1Tip(ctx) 144 | if err != nil { 145 | l.log.Error("Failed to query L1 tip", "error", err) 146 | return err 147 | } 148 | l.recordL1Tip(l1tip) 149 | 150 | // Collect next transaction data 151 | txdata, err := l.state.TxData(l1tip.ID()) 152 | if err == io.EOF { 153 | l.log.Trace("no transaction data available") 154 | return err 155 | } else if err != nil { 156 | l.log.Error("unable to get tx data", "err", err) 157 | return err 158 | } 159 | 160 | l.sendTransaction(txdata, queue, receiptsCh) 161 | return nil 162 | } 163 | ``` 164 | 165 | #### TxData详解 166 | `op-batcher/batcher/channel_manager.go` 167 | 168 | `TxData`函数主要负责两件事务 169 | - 查找第一个含有`frame`的的`channel`,如果存在且通过检查后使用`nextTxData`获取数据并返回 170 | - 如果没有这样的`channel`,我们需要现调用`ensureChannelWithSpace`检查`channel`还有剩余的空间,再使用`processBlocks`将之前加载到`block队列`中的数据构造到 `outchannel的composer`当中压缩 171 | - `outputFrames`将`outchannel composer`当中的数据切割成适合大小的`frame` 172 | - 最后再把刚构造到数据通过`nextTxData`函数返回出去。 173 | 174 | 175 | `EnsureChannelWithSpace` 确保 `currentChannel` 填充有可容纳更多数据的空间的`channel`(即,`channel.IsFull` 返回 `false`)。 如果 `currentChannel` 为零或已满,则会创建一个新`channel`。 176 | 177 | ```go 178 | func (s *channelManager) TxData(l1Head eth.BlockID) (txData, error) { 179 | s.mu.Lock() 180 | defer s.mu.Unlock() 181 | var firstWithFrame *channel 182 | for _, ch := range s.channelQueue { 183 | if ch.HasFrame() { 184 | firstWithFrame = ch 185 | break 186 | } 187 | } 188 | 189 | dataPending := firstWithFrame != nil && firstWithFrame.HasFrame() 190 | s.log.Debug("Requested tx data", "l1Head", l1Head, "data_pending", dataPending, "blocks_pending", len(s.blocks)) 191 | 192 | // Short circuit if there is a pending frame or the channel manager is closed. 193 | if dataPending || s.closed { 194 | return s.nextTxData(firstWithFrame) 195 | } 196 | 197 | // No pending frame, so we have to add new blocks to the channel 198 | 199 | // If we have no saved blocks, we will not be able to create valid frames 200 | if len(s.blocks) == 0 { 201 | return txData{}, io.EOF 202 | } 203 | 204 | if err := s.ensureChannelWithSpace(l1Head); err != nil { 205 | return txData{}, err 206 | } 207 | 208 | if err := s.processBlocks(); err != nil { 209 | return txData{}, err 210 | } 211 | 212 | // Register current L1 head only after all pending blocks have been 213 | // processed. Even if a timeout will be triggered now, it is better to have 214 | // all pending blocks be included in this channel for submission. 215 | s.registerL1Block(l1Head) 216 | 217 | if err := s.outputFrames(); err != nil { 218 | return txData{}, err 219 | } 220 | 221 | return s.nextTxData(s.currentChannel) 222 | } 223 | 224 | ``` 225 | 226 | `processBlocks`函数在内部通过`AddBlock`把`block队列`里的`block`加入到当前的`channel`当中 227 | 228 | ```go 229 | func (s *channelManager) processBlocks() error { 230 | var ( 231 | blocksAdded int 232 | _chFullErr *ChannelFullError // throw away, just for type checking 233 | latestL2ref eth.L2BlockRef 234 | ) 235 | for i, block := range s.blocks { 236 | l1info, err := s.currentChannel.AddBlock(block) 237 | if errors.As(err, &_chFullErr) { 238 | // current block didn't get added because channel is already full 239 | break 240 | } else if err != nil { 241 | return fmt.Errorf("adding block[%d] to channel builder: %w", i, err) 242 | } 243 | s.log.Debug("Added block to channel", "channel", s.currentChannel.ID(), "block", block) 244 | 245 | blocksAdded += 1 246 | latestL2ref = l2BlockRefFromBlockAndL1Info(block, l1info) 247 | s.metr.RecordL2BlockInChannel(block) 248 | // current block got added but channel is now full 249 | if s.currentChannel.IsFull() { 250 | break 251 | } 252 | } 253 | ``` 254 | 255 | `AddBlock` 首先通过`BlockToBatch`把`batch`从`blcok`中获取出来,再通过`AddBatch`函数对数据进行压缩并存储。 256 | 257 | ```go 258 | func (c *channelBuilder) AddBlock(block *types.Block) (derive.L1BlockInfo, error) { 259 | if c.IsFull() { 260 | return derive.L1BlockInfo{}, c.FullErr() 261 | } 262 | 263 | batch, l1info, err := derive.BlockToBatch(block) 264 | if err != nil { 265 | return l1info, fmt.Errorf("converting block to batch: %w", err) 266 | } 267 | 268 | if _, err = c.co.AddBatch(batch); errors.Is(err, derive.ErrTooManyRLPBytes) || errors.Is(err, derive.CompressorFullErr) { 269 | c.setFullErr(err) 270 | return l1info, c.FullErr() 271 | } else if err != nil { 272 | return l1info, fmt.Errorf("adding block to channel out: %w", err) 273 | } 274 | c.blocks = append(c.blocks, block) 275 | c.updateSwTimeout(batch) 276 | 277 | if err = c.co.FullErr(); err != nil { 278 | c.setFullErr(err) 279 | // Adding this block still worked, so don't return error, just mark as full 280 | } 281 | 282 | return l1info, nil 283 | } 284 | ``` 285 | 286 | 在`txdata`获取后,使用`sendTransaction`将整个数据发送到L1当中。 287 | 288 | ## 总结 289 | 在这一章节中,我们了解了什么是`batcher`并且了解了`batcher`的运行原理,你可以在这个 [address](https://etherscan.io/address/0x6887246668a3b87f54deb3b94ba47a6f63f32985)中查看当前`batcher`的行为。 -------------------------------------------------------------------------------- /sequencer/04-how-derivation-works.md: -------------------------------------------------------------------------------- 1 | # opstack是如何从Layer1中派生出来Layer2的 2 | 在阅读本文章之前,我强烈建议你先阅读一下来自`optimism/specs`中有关派生部分的介绍([source](https://github.com/ethereum-optimism/optimism/blob/develop/specs/derivation.md#deriving-payload-attributes)) 3 | 如果你看完这篇文章,感到迷茫,这是正常的。但是还是请记住这份感觉,因为在看完我们这篇文章的分析之后,请你回过来头再看一遍,你就会发现这篇官方的文章真的很凝练,把所有要点和细节都精炼的阐述了一遍。 4 | 5 | 接下来让我们进入文章正题。我们都知道layer2的运行节点,是可以从DA层(layer1)中获取数据,并且构建出完整的区块数据的。今天我们就来讲解一下这个过程中是如何在`codebase`中实现的。 6 | 7 | ## 你需要有的问题 8 | 9 | 如果现在让你设计这样一套系统,你会怎么设计呢?你会有哪些问题?在这里我列出来了一些问题,带着这些问题去思考会帮助你更好的理解整篇文章 10 | - 当你启动一个新节点的时候,整个系统是如何运行的? 11 | - 你需要一个个去查询所有l1的区块数据吗?如何触发查询? 12 | - 当拿到l1区块的数据后,你需要哪些数据? 13 | - 派生过程中,区块的状态是怎么变化的?如何从`unsafe`变成`safe`再变成`finalized`? 14 | - 官方specs中晦涩的数据结构 `batch/channel/frame` 这些到底是干嘛的?(可以在上一章`03-how-batcher-works`章节中详细理解) 15 | 16 | ## 什么是派生(derivation)? 17 | 18 | 在理解`derivation`前,我们先来聊一聊optimism的基本rollup机制,这里我们简单以一笔l2上的transfer交易为例。 19 | 20 | 当你在optimism网络上发出一笔转账交易,这笔交易会被"转发"给`sequencer`节点,由`sequencer`进行排序,然后进行区块的封装并进行区块的广播,这里可以理解为出块。我们把这个包含你交易的区块称为`区块A`。这时的`区块A`状态为`unsafe`。接下来等`sequencer`达到一定的时间间隔了(比如4分钟),会由`sequencer`中的`batcher`的模块把这四分钟内所有收集到的交易(包括你这笔转账交易)通过一笔交易发送到l1上,并由l1产出区块X。这时的区块A状态仍然为`unsafe`。当任何一个节点执行`derivation`部分的程序后,此节点从l1中获取区块X的数据,并对`本地l2的unsafe区块A`进行更新。这时的`区块A`状态为`safe`。在经过`l1两个epoch(64个区块)`后,由l2节点将区块A标记为`finalized`区块。 21 | 22 | 而派生就是把角色带入到上述例子的l2节点当中,通过不断的并行执行`derivation`程序将获取的`unsafe`区块逐步变成`safe`区块,同时把已经是`safe`的区块逐步变成`finalized`状态的一个过程。 23 | 24 | ## 代码层深潜 25 | 26 | hoho 船长,让我们深潜🤿 27 | 28 | ### 获取batcher发送的batch transactions的data 29 | 30 | 我们先来看看当我们知道一个新的l1的区块时,如何查看区块里面是否有`batch transactions`的数据 31 | 在这里我们先梳理一下所需要的模块,再针对这些模块进行查看 32 | - 首先要确定下一个l1的区块块号是多少 33 | - 将下一个区块的数据解析出来 34 | 35 | #### 确定下一个区块的块号 36 | `op-node/rollup/derive/l1_traversal.go` 37 | 38 | 通过查询当前`origin.Number + 1`的块高来获取最新的l1块,如果此块不存在,即`error`和`ethereum.NotFound`匹配,那么就代表当前块高即为最新的区块,下一个区块还未在l1上产生。如果获取成功,将最新的区块号记录在`l1t.block`中 39 | ```go 40 | func (l1t *L1Traversal) AdvanceL1Block(ctx context.Context) error { 41 | origin := l1t.block 42 | nextL1Origin, err := l1t.l1Blocks.L1BlockRefByNumber(ctx, origin.Number+1) 43 | if errors.Is(err, ethereum.NotFound) { 44 | l1t.log.Debug("can't find next L1 block info (yet)", "number", origin.Number+1, "origin", origin) 45 | return io.EOF 46 | } else if err != nil { 47 | return NewTemporaryError(fmt.Errorf("failed to find L1 block info by number, at origin %s next %d: %w", origin, origin.Number+1, err)) 48 | } 49 | if l1t.block.Hash != nextL1Origin.ParentHash { 50 | return NewResetError(fmt.Errorf("detected L1 reorg from %s to %s with conflicting parent %s", l1t.block, nextL1Origin, nextL1Origin.ParentID())) 51 | } 52 | 53 | …… 54 | 55 | l1t.block = nextL1Origin 56 | l1t.done = false 57 | return nil 58 | } 59 | ``` 60 | 61 | #### 将区块的data解析出来 62 | `op-node/rollup/derive/calldata_source.go` 63 | 64 | 首先先通过`InfoAndTxsByHash`将刚才获取的区块的所有`transactions`拿到,然后将`transactions`和我们的batcherAddr还有我们的config传入到`DataFromEVMTransactions`函数中, 65 | 为什么要传这些参数呢?因为我们在过滤这些交易的时候,需要保证`batcher`地址和接收地址的准确性(权威性)。在`DataFromEVMTransactions`接收到这些参数后,通过循环对每个交易进行地址的准确性过滤,找到正确的`batch transactions`。 66 | 67 | ```go 68 | func NewDataSource(ctx context.Context, log log.Logger, cfg *rollup.Config, fetcher L1TransactionFetcher, block eth.BlockID, batcherAddr common.Address) DataIter { 69 | _, txs, err := fetcher.InfoAndTxsByHash(ctx, block.Hash) 70 | if err != nil { 71 | return &DataSource{ 72 | open: false, 73 | id: block, 74 | cfg: cfg, 75 | fetcher: fetcher, 76 | log: log, 77 | batcherAddr: batcherAddr, 78 | } 79 | } else { 80 | return &DataSource{ 81 | open: true, 82 | data: DataFromEVMTransactions(cfg, batcherAddr, txs, log.New("origin", block)), 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | ```go 89 | func DataFromEVMTransactions(config *rollup.Config, batcherAddr common.Address, txs types.Transactions, log log.Logger) []eth.Data { 90 | var out []eth.Data 91 | l1Signer := config.L1Signer() 92 | for j, tx := range txs { 93 | if to := tx.To(); to != nil && *to == config.BatchInboxAddress { 94 | seqDataSubmitter, err := l1Signer.Sender(tx) // optimization: only derive sender if To is correct 95 | if err != nil { 96 | log.Warn("tx in inbox with invalid signature", "index", j, "err", err) 97 | continue // bad signature, ignore 98 | } 99 | // some random L1 user might have sent a transaction to our batch inbox, ignore them 100 | if seqDataSubmitter != batcherAddr { 101 | log.Warn("tx in inbox with unauthorized submitter", "index", j, "err", err) 102 | continue // not an authorized batch submitter, ignore 103 | } 104 | out = append(out, tx.Data()) 105 | } 106 | } 107 | return out 108 | } 109 | ``` 110 | 111 | ### 从data到safeAttribute,使unsafe的区块safe化 112 | 113 | 在这一部分,首先会将上一步我们解析出来的`data`解析成`frame`并添加到`FrameQueue`的`frames`数组里面。然后从`frames`数组中提取一个`frame`,并将`frame`初始化进一个`channel`并添加到`channelbank`当中,等待该`channel`中的`frames`添加完毕后,从`channel`中提取`batch`信息,把`batch`添加到`BatchQueue`中,将`BatchQueue`中的`batch`添加到`AttributesQueue`中,用来构造`safeAttributes`,并把`enginequeue`里面的`safeblcok`更新,最终通过`ForkchoiceUpdate`函数的调用来完成EL层`safeblock`的更新 114 | 115 | #### data -> frame 116 | `op-node/rollup/derive/frame_queue.go` 117 | 118 | 此函数通过`NextData`函数获取上一步的data,然后将此data解析后添加到`FrameQueue`的`frames`数组里面,并返回在数组中第一个`frame`。 119 | 120 | ```go 121 | func (fq *FrameQueue) NextFrame(ctx context.Context) (Frame, error) { 122 | // Find more frames if we need to 123 | if len(fq.frames) == 0 { 124 | if data, err := fq.prev.NextData(ctx); err != nil { 125 | return Frame{}, err 126 | } else { 127 | if new, err := ParseFrames(data); err == nil { 128 | fq.frames = append(fq.frames, new...) 129 | } else { 130 | fq.log.Warn("Failed to parse frames", "origin", fq.prev.Origin(), "err", err) 131 | } 132 | } 133 | } 134 | // If we did not add more frames but still have more data, retry this function. 135 | if len(fq.frames) == 0 { 136 | return Frame{}, NotEnoughData 137 | } 138 | 139 | ret := fq.frames[0] 140 | fq.frames = fq.frames[1:] 141 | return ret, nil 142 | } 143 | ``` 144 | 145 | #### frame -> channel 146 | `op-node/rollup/derive/channel_bank.go` 147 | 148 | `NextData`函数负责从当前`channel bank`中读出第一个`channel`中的`raw data`并返回,同时负责调用`NextFrame`获取`frame`并装载到`channel`中 149 | 150 | ```go 151 | func (cb *ChannelBank) NextData(ctx context.Context) ([]byte, error) { 152 | // Do the read from the channel bank first 153 | data, err := cb.Read() 154 | if err == io.EOF { 155 | // continue - We will attempt to load data into the channel bank 156 | } else if err != nil { 157 | return nil, err 158 | } else { 159 | return data, nil 160 | } 161 | 162 | // Then load data into the channel bank 163 | if frame, err := cb.prev.NextFrame(ctx); err == io.EOF { 164 | return nil, io.EOF 165 | } else if err != nil { 166 | return nil, err 167 | } else { 168 | cb.IngestFrame(frame) 169 | return nil, NotEnoughData 170 | } 171 | } 172 | ``` 173 | 174 | #### channel -> batch 175 | `op-node/rollup/derive/channel_in_reader.go` 176 | 177 | `NextBatch`函数主要负责将刚才到`raw data` 解码成具有`batch`结构的数据并返回。其中`WriteChannel`函数的作用是提供一个函数并赋值给`nextBatchFn`,这个函数的目的是创建一个读取器,从读取器中解码`batch`结构的数据并返回。 178 | 179 | ```go 180 | func (cr *ChannelInReader) NextBatch(ctx context.Context) (*BatchData, error) { 181 | if cr.nextBatchFn == nil { 182 | if data, err := cr.prev.NextData(ctx); err == io.EOF { 183 | return nil, io.EOF 184 | } else if err != nil { 185 | return nil, err 186 | } else { 187 | if err := cr.WriteChannel(data); err != nil { 188 | return nil, NewTemporaryError(err) 189 | } 190 | } 191 | } 192 | 193 | // TODO: can batch be non nil while err == io.EOF 194 | // This depends on the behavior of rlp.Stream 195 | batch, err := cr.nextBatchFn() 196 | if err == io.EOF { 197 | cr.NextChannel() 198 | return nil, NotEnoughData 199 | } else if err != nil { 200 | cr.log.Warn("failed to read batch from channel reader, skipping to next channel now", "err", err) 201 | cr.NextChannel() 202 | return nil, NotEnoughData 203 | } 204 | return batch.Batch, nil 205 | } 206 | ``` 207 | 208 | **注意❗️在这里NextBatch函数产生的batch并没有被直接使用,而是先加入了batchQueue当中,再统一管理和使用,并且这里的NextBatch实际由 op-node/rollup/derive/batch_queue.go 目录下的func (bq *BatchQueue) NextBatch()函数调用** 209 | #### batch -> safeAttributes 210 | **补充信息:** 211 | 1.在layer2区块中,区块中的交易中的第一个永远都是一个`锚定交易`,可以简单理解为包含了一些l1的信息,如果这个layer2区块同时还是epoch中第一个区块的话,那么还会包含来自layer1的`deposit`交易([epoch中第一个区块示例](https://optimistic.etherscan.io/txs?block=110721915])。 212 | 2.这里的batch不能理解为batcher发送的batch交易。例如,我们在这里将batcher发送的batch交易命名为batchA,而在我们这里使用和讨论的命名为batchB,batchA和batchB的关系为包含关系,即batchA中可能包含非常巨量的交易,这些交易可以构造为batchB,batchBB,batchBBB等。batchB对应一个layer2中区块的交易,而batchA对应大量layer2中区块的交易。 213 | 214 | `op-node/rollup/derive/attributes_queue.go` 215 | - `NextAttributes`函数传入当前l2的safe区块头后,将块头和我们上一步获取的batch传递到`createNextAttributes`函数中,构造`safeAttributes`。 216 | - `createNextAttributes`中我们要注意的是,`createNextAttributes`函数内部调用的`PreparePayloadAttributes`函数,`PreparePayloadAttributes`函数主要负责,锚定交易和`deposit`交易的。最后再把`batch`的交易和`PreparePayloadAttributes`函数返回的交易拼接起来后返回 217 | 218 | `createNextAttributes`函数在内部调用`PreparePayloadAttributes` 219 | 220 | ```go 221 | func (aq *AttributesQueue) NextAttributes(ctx context.Context, l2SafeHead eth.L2BlockRef) (*eth.PayloadAttributes, error) { 222 | // Get a batch if we need it 223 | if aq.batch == nil { 224 | batch, err := aq.prev.NextBatch(ctx, l2SafeHead) 225 | if err != nil { 226 | return nil, err 227 | } 228 | aq.batch = batch 229 | } 230 | 231 | // Actually generate the next attributes 232 | if attrs, err := aq.createNextAttributes(ctx, aq.batch, l2SafeHead); err != nil { 233 | return nil, err 234 | } else { 235 | // Clear out the local state once we will succeed 236 | aq.batch = nil 237 | return attrs, nil 238 | } 239 | 240 | } 241 | ``` 242 | 243 | ```go 244 | func (aq *AttributesQueue) createNextAttributes(ctx context.Context, batch *BatchData, l2SafeHead eth.L2BlockRef) (*eth.PayloadAttributes, error) { 245 | 246 | …… 247 | attrs, err := aq.builder.PreparePayloadAttributes(fetchCtx, l2SafeHead, batch.Epoch()) 248 | …… 249 | 250 | return attrs, nil 251 | } 252 | ``` 253 | 254 | ```go 255 | func (aq *AttributesQueue) createNextAttributes(ctx context.Context, batch *BatchData, l2SafeHead eth.L2BlockRef) (*eth.PayloadAttributes, error) { 256 | // sanity check parent hash 257 | if batch.ParentHash != l2SafeHead.Hash { 258 | return nil, NewResetError(fmt.Errorf("valid batch has bad parent hash %s, expected %s", batch.ParentHash, l2SafeHead.Hash)) 259 | } 260 | // sanity check timestamp 261 | if expected := l2SafeHead.Time + aq.config.BlockTime; expected != batch.Timestamp { 262 | return nil, NewResetError(fmt.Errorf("valid batch has bad timestamp %d, expected %d", batch.Timestamp, expected)) 263 | } 264 | fetchCtx, cancel := context.WithTimeout(ctx, 20*time.Second) 265 | defer cancel() 266 | attrs, err := aq.builder.PreparePayloadAttributes(fetchCtx, l2SafeHead, batch.Epoch()) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | // we are verifying, not sequencing, we've got all transactions and do not pull from the tx-pool 272 | // (that would make the block derivation non-deterministic) 273 | attrs.NoTxPool = true 274 | attrs.Transactions = append(attrs.Transactions, batch.Transactions...) 275 | 276 | aq.log.Info("generated attributes in payload queue", "txs", len(attrs.Transactions), "timestamp", batch.Timestamp) 277 | 278 | return attrs, nil 279 | } 280 | ``` 281 | 282 | #### safeAttributes -> safe block 283 | 在这一步,会先`engine queue`中的`safehead`设置为`safe`,但是这并不代表这个区块是`safe`的了,还必须通过`ForkchoiceUpdat`在EL中更新 284 | 285 | `op-node/rollup/derive/engine_queue.go` 286 | 287 | `tryNextSafeAttributes`函数在内部判断是否当前`safehead`和`unsafehead`的关系,如果一切正常,则触发`consolidateNextSafeAttributes`函数来把`engine queue`中的`safeHead` 设置为我们上一步拿到的`safeAttributes`构造出来的`safe`区块,并将`needForkchoiceUpdate`设置为`true`,触发后续的`ForkchoiceUpdate`来把EL中的区块状态改成`safe`而真正将`unsafe`区块转化成`safe`区块。最后的`postProcessSafeL2`函数是将`safehead`加入到`finalizedL1`队列中,以供后续`finalied`使用。 288 | 289 | ```go 290 | func (eq *EngineQueue) tryNextSafeAttributes(ctx context.Context) error { 291 | …… 292 | if eq.safeHead.Number < eq.unsafeHead.Number { 293 | return eq.consolidateNextSafeAttributes(ctx) 294 | } 295 | …… 296 | } 297 | 298 | func (eq *EngineQueue) consolidateNextSafeAttributes(ctx context.Context) error { 299 | …… 300 | payload, err := eq.engine.PayloadByNumber(ctx, eq.safeHead.Number+1) 301 | …… 302 | ref, err := PayloadToBlockRef(payload, &eq.cfg.Genesis) 303 | …… 304 | eq.safeHead = ref 305 | eq.needForkchoiceUpdate = true 306 | eq.postProcessSafeL2() 307 | …… 308 | return nil 309 | } 310 | ``` 311 | 312 | ### 将safe区块finalized化 313 | safe区块并不是真的牢固安全的区块,他还需要进行进一步的最终化确定,即`finalized`化。当一个区块的状态转变为`safe`时,从此区块派生的来源`L1(batcher transaction)`开始计算,经过两个`L1 epoch(64个区块`后,此`safe`区块可以被更新成`finalzied`状态。 314 | 315 | `op-node/rollup/derive/engine_queue.go` 316 | 317 | `tryFinalizePastL2Blocks`函数在内部对`finalized队列`中区块进行64个区块的校验,如果通过校验,调用`tryFinalizeL2`来完成`engine queue`当中`finalized`的设置和标记`needForkchoiceUpdate`的更新。 318 | 319 | ```go 320 | func (eq *EngineQueue) tryFinalizePastL2Blocks(ctx context.Context) error { 321 | …… 322 | eq.log.Info("processing L1 finality information", "l1_finalized", eq.finalizedL1, "l1_origin", eq.origin, "previous", eq.triedFinalizeAt) //const finalityDelay untyped int = 64 323 | 324 | // Sanity check we are indeed on the finalizing chain, and not stuck on something else. 325 | // We assume that the block-by-number query is consistent with the previously received finalized chain signal 326 | ref, err := eq.l1Fetcher.L1BlockRefByNumber(ctx, eq.origin.Number) 327 | if err != nil { 328 | return NewTemporaryError(fmt.Errorf("failed to check if on finalizing L1 chain: %w", err)) 329 | } 330 | if ref.Hash != eq.origin.Hash { 331 | return NewResetError(fmt.Errorf("need to reset, we are on %s, not on the finalizing L1 chain %s (towards %s)", eq.origin, ref, eq.finalizedL1)) 332 | } 333 | eq.tryFinalizeL2() 334 | return nil 335 | } 336 | ``` 337 | 338 | ```go 339 | func (eq *EngineQueue) tryFinalizeL2() { 340 | if eq.finalizedL1 == (eth.L1BlockRef{}) { 341 | return // if no L1 information is finalized yet, then skip this 342 | } 343 | eq.triedFinalizeAt = eq.origin 344 | // default to keep the same finalized block 345 | finalizedL2 := eq.finalized 346 | // go through the latest inclusion data, and find the last L2 block that was derived from a finalized L1 block 347 | for _, fd := range eq.finalityData { 348 | if fd.L2Block.Number > finalizedL2.Number && fd.L1Block.Number <= eq.finalizedL1.Number { 349 | finalizedL2 = fd.L2Block 350 | eq.needForkchoiceUpdate = true 351 | } 352 | } 353 | eq.finalized = finalizedL2 354 | eq.metrics.RecordL2Ref("l2_finalized", finalizedL2) 355 | } 356 | ``` 357 | ### 循环触发 358 | 在`op-node/rollup/driver/state.go`中的`eventLoop`函数中负责触发整个循环过程中的执行入口。主要是间接执行了了`op-node/rollup/derive/engine_queue.go`中`Step`函数 359 | 360 | ```go 361 | func (eq *EngineQueue) Step(ctx context.Context) error { 362 | if eq.needForkchoiceUpdate { 363 | return eq.tryUpdateEngine(ctx) 364 | } 365 | // Trying unsafe payload should be done before safe attributes 366 | // It allows the unsafe head can move forward while the long-range consolidation is in progress. 367 | if eq.unsafePayloads.Len() > 0 { 368 | if err := eq.tryNextUnsafePayload(ctx); err != io.EOF { 369 | return err 370 | } 371 | // EOF error means we can't process the next unsafe payload. Then we should process next safe attributes. 372 | } 373 | if eq.isEngineSyncing() { 374 | // Make pipeline first focus to sync unsafe blocks to engineSyncTarget 375 | return EngineP2PSyncing 376 | } 377 | if eq.safeAttributes != nil { 378 | return eq.tryNextSafeAttributes(ctx) 379 | } 380 | outOfData := false 381 | newOrigin := eq.prev.Origin() 382 | // Check if the L2 unsafe head origin is consistent with the new origin 383 | if err := eq.verifyNewL1Origin(ctx, newOrigin); err != nil { 384 | return err 385 | } 386 | eq.origin = newOrigin 387 | eq.postProcessSafeL2() // make sure we track the last L2 safe head for every new L1 block 388 | // try to finalize the L2 blocks we have synced so far (no-op if L1 finality is behind) 389 | if err := eq.tryFinalizePastL2Blocks(ctx); err != nil { 390 | return err 391 | } 392 | if next, err := eq.prev.NextAttributes(ctx, eq.safeHead); err == io.EOF { 393 | outOfData = true 394 | } else if err != nil { 395 | return err 396 | } else { 397 | eq.safeAttributes = &attributesWithParent{ 398 | attributes: next, 399 | parent: eq.safeHead, 400 | } 401 | eq.log.Debug("Adding next safe attributes", "safe_head", eq.safeHead, "next", next) 402 | return NotEnoughData 403 | } 404 | 405 | if outOfData { 406 | return io.EOF 407 | } else { 408 | return nil 409 | } 410 | } 411 | ``` 412 | ## 总结 413 | 整个`derivation`功能看似非常复杂,但是你如果将每个环节都拆解开的话,还是能够很好的掌握理解的,官方的那篇specs不好理解的原因在于,他的`batch`,`frame`,`channel`等概念很容易让人迷茫,因此,如果你在看完这篇文章后,仍然觉得还很迷惑,建议可以回过头去再看看我们的`03-how-batcher-works`。 -------------------------------------------------------------------------------- /sequencer/05-how-proposer-works.md: -------------------------------------------------------------------------------- 1 | # op-proposer介绍 2 | 3 | 在这一章节中,我们将探讨到底什么是`op-proposer` 🌊 4 | 5 | 首先分享下来自官方specs中的资源([source](https://github.com/ethereum-optimism/optimism/blob/develop/specs/proposals.md)) 6 | 7 | 一句话概括性的描述`proposer`的作用:定期的将来自layer2上的状态根(state root)发送到layer1上,以便可以无需信任地直接在layer1层面上执行一些来自layer2的交易,如提款或者message通信。 8 | 9 | 在本章节中,将会以处理来自layer2的一笔layer1的提现交易为例子,讲解`op-proposer`在整个流程中的作用。 10 | 11 | ## 提现流程 12 | 13 | 在Optimism中,提现是从L2(如 OP Mainnet, OP Goerli)到L1(如 Ethereum mainnet, Goerli)的交易,可能附带或不附带资产。可以粗略的分为四笔交易: 14 | - 用户在L2提交的提现发起交易; 15 | - `proposer`将L2中的`state root` 通过发送交易的方式上传到L1当中,以供接下来步骤中用户中L1中使用 16 | - 用户在L1提交的提现证明交易,基于`Merkle Patricia Trie`,证明提现的合法性; 17 | - 错误挑战期过后,用户在L1提交的提现最终交易,实际运行L1交易,认领任何附加资产等; 18 | 19 | 具体详情可以查看官方对于这部分的描述([source](https://community.optimism.io/docs/protocol/withdrawal-flow/#)) 20 | 21 | ## 什么是proposer 22 | `proposer`是服务于在L1中需要用到L2部分数据时的连通器,通过`proposer`将这一部分L2的数据(state root)发送到L1的合约当中。L1的合约就可以通过合约调用的方式直接使用了。 23 | 24 | 注意⚠️:很多人认为`proposer`发送的`state root`后**才**代表这些设计的区块是`finalized`。这种理解是`错误`的。`safe`的区块在L1中经过**两个**`epoch(64个区块)`后即可认定为`finalized`。 25 | `proposer`是将`finalized`的区块数据上传,而不是上传后才`finalized`。 26 | 27 | ### proposer和batcher的区别 28 | 在之前我们讲解了`batcher`部分,`batcher`也是负责把L2的数据发送到L1中。你可能会有疑问,`batcher`不都已经把数据搬运到L1当中了,为什么还需要一个`proposer`再进行一次搬运呢? 29 | 30 | #### 区块状态不一致 31 | `batcher`发送数据时,区块的状态还是`unsafe`状态,不能直接使用,且无法根据`batcher`的交易来判断区块的状态何时变成`finalized`状态。 32 | `proposer`发送数据时,代表了相关区块已经达到了`finalized`阶段,可以最大程度的去相信并使用相关数据。 33 | 34 | #### 传递的数据格式和大小不同 35 | `batcher`是将几乎完整的交易信息,包括`gasprice,data`等详细信息存储在`layer1`当中。 36 | `proposer`只是将区块的`state root`发送到l1当中。`state root`后续配合[merkle-tree](https://medium.com/techskill-brew/merkle-tree-in-blockchain-part-5-blockchain-basics-4e25b61179a2)的设计使用 37 | `batcher`传递的数据是巨量,`proposer`是少量的。因此`batcher`的数据更适合放置在`calldate`中,便宜,但是不能直接被合约使用。`proposer`的数据存储在`合约的storage`当中,数据量少,成本不会很高,并且可以在合约交互中使用。 38 | 39 | #### 在以太坊中,数据存储calldata当中和存储在合约的storage当中的区别 40 | 在以太坊中,`calldata`和`storage`的区别主要有三方面: 41 | 42 | 1. **持久性**: 43 | - `storage`:持久存储,数据永久保存。 44 | - `calldata`:临时存储,函数执行完毕后数据消失。 45 | 46 | 2. **成本**: 47 | - `storage`:较贵,需永久存储数据。 48 | - `calldata`:较便宜,临时存储。 49 | 50 | 3. **可访问性**: 51 | - `storage`:多个函数或事务中可访问。 52 | - `calldata`:仅当前函数执行期间可访问。 53 | 54 | ## 代码实现 55 | 在这部分我们会从代码层来进行深度的机制和实现原理的讲解 56 | 57 | ### 程序起点 58 | `op-proposer/proposer/l2_output_submitter.go` 59 | 60 | 通过调用`Start`函数来启动`loop`循环,在`loop`的循环中,主要通过函数`FetchNextOutputInfo`负责查看下一个区块是否该发送`proposal`交易,如果需要发送,则直接调用`sendTransaction`函数发送到L1当作,如不需要发送,则进行下一次循环。 61 | 62 | ```go 63 | func (l *L2OutputSubmitter) loop() { 64 | defer l.wg.Done() 65 | 66 | ctx := l.ctx 67 | 68 | ticker := time.NewTicker(l.pollInterval) 69 | defer ticker.Stop() 70 | for { 71 | select { 72 | case <-ticker.C: 73 | output, shouldPropose, err := l.FetchNextOutputInfo(ctx) 74 | if err != nil { 75 | break 76 | } 77 | if !shouldPropose { 78 | break 79 | } 80 | cCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) 81 | if err := l.sendTransaction(cCtx, output); err != nil { 82 | l.log.Error("Failed to send proposal transaction", 83 | "err", err, 84 | "l1blocknum", output.Status.CurrentL1.Number, 85 | "l1blockhash", output.Status.CurrentL1.Hash, 86 | "l1head", output.Status.HeadL1.Number) 87 | cancel() 88 | break 89 | } 90 | l.metr.RecordL2BlocksProposed(output.BlockRef) 91 | cancel() 92 | 93 | case <-l.done: 94 | return 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ### 获取output 101 | `op-proposer/proposer/l2_output_submitter.go` 102 | 103 | `FetchNextOutputInfo`函数通过调用`l2ooContract`合约来获取下一次该发送`proposal`的区块数,再将该区块块号和当前L2度区块块号进行比较,来判断是否应该发送`proposal`交易。如果需要发送,则调用`fetchOutput`函数来生成`output` 104 | 105 | ```go 106 | func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) { 107 | cCtx, cancel := context.WithTimeout(ctx, l.networkTimeout) 108 | defer cancel() 109 | callOpts := &bind.CallOpts{ 110 | From: l.txMgr.From(), 111 | Context: cCtx, 112 | } 113 | nextCheckpointBlock, err := l.l2ooContract.NextBlockNumber(callOpts) 114 | if err != nil { 115 | l.log.Error("proposer unable to get next block number", "err", err) 116 | return nil, false, err 117 | } 118 | // Fetch the current L2 heads 119 | cCtx, cancel = context.WithTimeout(ctx, l.networkTimeout) 120 | defer cancel() 121 | status, err := l.rollupClient.SyncStatus(cCtx) 122 | if err != nil { 123 | l.log.Error("proposer unable to get sync status", "err", err) 124 | return nil, false, err 125 | } 126 | 127 | // Use either the finalized or safe head depending on the config. Finalized head is default & safer. 128 | var currentBlockNumber *big.Int 129 | if l.allowNonFinalized { 130 | currentBlockNumber = new(big.Int).SetUint64(status.SafeL2.Number) 131 | } else { 132 | currentBlockNumber = new(big.Int).SetUint64(status.FinalizedL2.Number) 133 | } 134 | // Ensure that we do not submit a block in the future 135 | if currentBlockNumber.Cmp(nextCheckpointBlock) < 0 { 136 | l.log.Debug("proposer submission interval has not elapsed", "currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextCheckpointBlock) 137 | return nil, false, nil 138 | } 139 | 140 | return l.fetchOutput(ctx, nextCheckpointBlock) 141 | } 142 | ``` 143 | 144 | `fetchOutput`函数在内部间接通过`OutputV0AtBlock`函数来获取并处理`output`返回体 145 | 146 | `op-service/sources/l2_client.go` 147 | 148 | `OutputV0AtBlock`函数获取之前检索出来需要传递`proposal`的区块哈希来拿到区块头,再根据这个区块头派生`OutputV0`所需要的数据。其中通过`GetProof`函数获取的的`proof`中的`StorageHash(withdrawal_storage_root)`的作用是,如果只需要`L2ToL1MessagePasserAddr`相关的`state`的数据的话,`withdrawal_storage_root`可以大幅度减小整个默克尔树证明过程的大小。 149 | 150 | ```go 151 | func (s *L2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) { 152 | head, err := s.InfoByHash(ctx, blockHash) 153 | if err != nil { 154 | return nil, fmt.Errorf("failed to get L2 block by hash: %w", err) 155 | } 156 | if head == nil { 157 | return nil, ethereum.NotFound 158 | } 159 | 160 | proof, err := s.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, blockHash.String()) 161 | if err != nil { 162 | return nil, fmt.Errorf("failed to get contract proof at block %s: %w", blockHash, err) 163 | } 164 | if proof == nil { 165 | return nil, fmt.Errorf("proof %w", ethereum.NotFound) 166 | } 167 | // make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root 168 | if err := proof.Verify(head.Root()); err != nil { 169 | return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err) 170 | } 171 | stateRoot := head.Root() 172 | return ð.OutputV0{ 173 | StateRoot: eth.Bytes32(stateRoot), 174 | MessagePasserStorageRoot: eth.Bytes32(proof.StorageHash), 175 | BlockHash: blockHash, 176 | }, nil 177 | } 178 | ``` 179 | 180 | ### 发送output 181 | 182 | `op-proposer/proposer/l2_output_submitter.go` 183 | 184 | 在`sendTransaction`函数中会间接调用`proposeL2OutputTxData`函数去使用`L1链上合约的ABI`来将我们的`output`与合约函数的`入参格式`进行匹配。随后`sendTransaction`函数将包装好的数据发送到L1上,与`L2OutputOracle合约`交互。 185 | 186 | ```go 187 | func proposeL2OutputTxData(abi *abi.ABI, output *eth.OutputResponse) ([]byte, error) { 188 | return abi.Pack( 189 | "proposeL2Output", 190 | output.OutputRoot, 191 | new(big.Int).SetUint64(output.BlockRef.Number), 192 | output.Status.CurrentL1.Hash, 193 | new(big.Int).SetUint64(output.Status.CurrentL1.Number)) 194 | } 195 | ``` 196 | 197 | `packages/contracts-bedrock/src/L1/L2OutputOracle.sol` 198 | 199 | `L2OutputOracle合约`通过将此来自`L2区块的state root`进行校验,并存入`合约的storage`当中。 200 | 201 | ```solidity 202 | /// @notice Accepts an outputRoot and the timestamp of the corresponding L2 block. 203 | /// The timestamp must be equal to the current value returned by `nextTimestamp()` in 204 | /// order to be accepted. This function may only be called by the Proposer. 205 | /// @param _outputRoot The L2 output of the checkpoint block. 206 | /// @param _l2BlockNumber The L2 block number that resulted in _outputRoot. 207 | /// @param _l1BlockHash A block hash which must be included in the current chain. 208 | /// @param _l1BlockNumber The block number with the specified block hash. 209 | function proposeL2Output( 210 | bytes32 _outputRoot, 211 | uint256 _l2BlockNumber, 212 | bytes32 _l1BlockHash, 213 | uint256 _l1BlockNumber 214 | ) 215 | external 216 | payable 217 | { 218 | require(msg.sender == proposer, "L2OutputOracle: only the proposer address can propose new outputs"); 219 | 220 | require( 221 | _l2BlockNumber == nextBlockNumber(), 222 | "L2OutputOracle: block number must be equal to next expected block number" 223 | ); 224 | 225 | require( 226 | computeL2Timestamp(_l2BlockNumber) < block.timestamp, 227 | "L2OutputOracle: cannot propose L2 output in the future" 228 | ); 229 | 230 | require(_outputRoot != bytes32(0), "L2OutputOracle: L2 output proposal cannot be the zero hash"); 231 | 232 | if (_l1BlockHash != bytes32(0)) { 233 | // This check allows the proposer to propose an output based on a given L1 block, 234 | // without fear that it will be reorged out. 235 | // It will also revert if the blockheight provided is more than 256 blocks behind the 236 | // chain tip (as the hash will return as zero). This does open the door to a griefing 237 | // attack in which the proposer's submission is censored until the block is no longer 238 | // retrievable, if the proposer is experiencing this attack it can simply leave out the 239 | // blockhash value, and delay submission until it is confident that the L1 block is 240 | // finalized. 241 | require( 242 | blockhash(_l1BlockNumber) == _l1BlockHash, 243 | "L2OutputOracle: block hash does not match the hash at the expected height" 244 | ); 245 | } 246 | 247 | emit OutputProposed(_outputRoot, nextOutputIndex(), _l2BlockNumber, block.timestamp); 248 | 249 | l2Outputs.push( 250 | Types.OutputProposal({ 251 | outputRoot: _outputRoot, 252 | timestamp: uint128(block.timestamp), 253 | l2BlockNumber: uint128(_l2BlockNumber) 254 | }) 255 | ); 256 | } 257 | ``` 258 | 259 | ## 总结 260 | `proposer`的总体实现思路与逻辑相对简单,即定期循环从L1中读取下次需要发送`proposal`的L2区块并与本地L2区块比较,并负责将数据处理并发送到L1当中。其他在提款过程中的其他交易流程大部分由`SDK`负责,可以详细阅读我们之前推送的官方对于提款过程部分的描述([source](https://community.optimism.io/docs/protocol/withdrawal-flow/#))。 261 | 如果想要查看在主网中`proposer`的实际行为,可以查看此[proposer address](https://etherscan.io/address/0x473300df21d047806a082244b417f96b32f13a33) -------------------------------------------------------------------------------- /sequencer/06-Upgrade-of-OPStack-in-EIP-4844.md: -------------------------------------------------------------------------------- 1 | # OPStack在EIP-4844中的升级 2 | 3 | Ethereum的EIP-4844对layer2来说是一次巨大的变革,它显著降低了layer2在使用L1作为DA(数据可用性)的费用。本文不详细解析EIP-4844的具体内容,只简要介绍,作为我们了解OP-Stack更新的背景。 4 | 5 | ## EIP-4844 6 | 7 | TL;DR: 8 | EIP-4844引入了一种称为“blob”的数据格式,这种格式的数据不参与EVM执行,而是存储在共识层,每个数据块的生命周期为4096个epoch(约18天)。blob存在于l1主网上,由新的type3 transaction携带,每个区块最多能容纳6个blob,每个transaction最多可以携带6个blob。 9 | 10 | 欲了解更多详情,请参考以下文章: 11 | 12 | [Ethereum Evolved: Dencun Upgrade Part 5, EIP-4844](https://consensys.io/blog/ethereum-evolved-dencun-upgrade-part-5-eip-4844) 13 | 14 | [EIP-4844, Blobs, and Blob Gas: What you need to know](https://www.blocknative.com/blog/eip-4844-blobs-and-blob-gas-what-you-need-to-know) 15 | 16 | [Proto-Danksharding](https://notes.ethereum.org/@vbuterin/proto_danksharding_faq#Proto-Danksharding-FAQ) 17 | 18 | ## OP-Stack的应用 19 | 20 | OP-Stack在采用BLOB替换之前的CALLDATA作为rollup的数据存储方式后,费率直线下降 21 | ![image](https://hackmd.io/_uploads/BJCSEyngA.png) 22 | 23 | **在OP-Stack的此次更新中,主要的业务逻辑变更涉及将原先通过calldata发送的数据转换为blob格式,并通过blob类型的交易发送到L1。此外,还涉及到从L1获取发送到rollup的数据时对blob的解析,以下是参与此次升级的主要组件:** 24 | 25 | 1. **submitter** —— 负责将rollup数据发送到L1的组件 26 | 2. **fetcher** —— 将L1的数据(旧rollup数据/deposit交易等)同步到L2中 27 | 3. **blob相关定义与实现** —— 如何获取和结构blob数据等内容等 28 | 4. **其他相关设计部分** —— 如客户端支持blob类型交易的签名、与fault proof相关的设计等 29 | 30 | --- 31 | 32 | ⚠️⚠️⚠️请注意,本文中所有涉及的代码均基于最初的PR设计,可能与实际生产环境中运行的代码存在差异。 33 | 34 | --- 35 | 36 | ### Blob相关定义与编解码实现 37 | 38 | [Pull Request(8131) blob 定义](https://github.com/ethereum-optimism/optimism/pull/8131/files#diff-30107b16d72d6e958093d83b5d736522a7994cab064187562605c82174400cd5) 39 | 40 | [Pull Request(8767) encoding & decoding](https://github.com/ethereum-optimism/optimism/commit/78ecdf523026d0afa45c519524a15b83cbe162c8#diff-30107b16d72d6e958093d83b5d736522a7994cab064187562605c82174400cd5R86) 41 | 42 | #### 定义blob 43 | 44 | ```go 45 | BlobSize = 4096 * 32 46 | 47 | type Blob [BlobSize]byte 48 | ``` 49 | 50 | #### blob encoding 51 | 52 | [**official specs about blob encoding**](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/derivation.md#blob-encoding) 53 | 54 | 需要注意的是,此specs对应的是最新版本的代码,而下方PR截取的代码则为最初的简化版本。主要区别在于:Blob类型被分为4096个字段元素,每个字段元素的最大大小受限于特定模的大小,即math.log2(BLS_MODULUS) = 254.8570894...,这意味着每个字段元素的大小不会超过254位,即31.75字节。最初的演示代码只使用了31字节,放弃了0.75字节的空间。而在最新版本的代码中,通过四个字段元素的联合作用,充分利用了每个字段元素的0.75字节空间,从而提高了数据的使用效率。 55 | 以下为Pull Request(8767)的部分截取代码 56 | 通过4096次循环,它读取总共31*4096字节的数据,这些数据随后被加入到blob中。 57 | 58 | ```go 59 | func (b *Blob) FromData(data Data) error { 60 | if len(data) > MaxBlobDataSize { 61 | return fmt.Errorf("data is too large for blob. len=%v", len(data)) 62 | } 63 | b.Clear() 64 | // encode 4-byte little-endian length value into topmost 4 bytes (out of 31) of first field 65 | // element 66 | binary.LittleEndian.PutUint32(b[1:5], uint32(len(data))) 67 | // encode first 27 bytes of input data into remaining bytes of first field element 68 | offset := copy(b[5:32], data) 69 | // encode (up to) 31 bytes of remaining input data at a time into the subsequent field element 70 | for i := 1; i < 4096; i++ { 71 | offset += copy(b[i*32+1:i*32+32], data[offset:]) 72 | if offset == len(data) { 73 | break 74 | } 75 | } 76 | if offset < len(data) { 77 | return fmt.Errorf("failed to fit all data into blob. bytes remaining: %v", len(data)-offset) 78 | } 79 | return nil 80 | } 81 | ``` 82 | 83 | #### blob decoding 84 | 85 | blob数据的解码,原理同上述的数据编码 86 | 87 | ```go 88 | func (b *Blob) ToData() (Data, error) { 89 | data := make(Data, 4096*32) 90 | for i := 0; i < 4096; i++ { 91 | if b[i*32] != 0 { 92 | return nil, fmt.Errorf("invalid blob, found non-zero high order byte %x of field element %d", b[i*32], i) 93 | } 94 | copy(data[i*31:i*31+31], b[i*32+1:i*32+32]) 95 | } 96 | // extract the length prefix & trim the output accordingly 97 | dataLen := binary.LittleEndian.Uint32(data[:4]) 98 | data = data[4:] 99 | if dataLen > uint32(len(data)) { 100 | return nil, fmt.Errorf("invalid blob, length prefix out of range: %d", dataLen) 101 | } 102 | data = data[:dataLen] 103 | return data, nil 104 | } 105 | ``` 106 | 107 | ### Submiter 108 | 109 | [Pull Request(8769)](https://github.com/ethereum-optimism/optimism/pull/8769) 110 | 111 | #### flag配置 112 | 113 | ```go 114 | switch c.DataAvailabilityType { 115 | case flags.CalldataType: 116 | case flags.BlobsType: 117 | default: 118 | return fmt.Errorf("unknown data availability type: %v", c.DataAvailabilityType) 119 | } 120 | ``` 121 | 122 | #### BatchSubmitter 123 | 124 | BatchSubmitter的功能从之前仅发送calldata数据扩展为根据情况发送calldata或blob类型的数据。Blob类型的数据通过之前提到的FromData(blob-encode)函数在blobTxCandidate内部进行编码 125 | 126 | ```go 127 | func (l *BatchSubmitter) sendTransaction(txdata txData, queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData]) error { 128 | // Do the gas estimation offline. A value of 0 will cause the [txmgr] to estimate the gas limit. 129 | data := txdata.Bytes() 130 | 131 | var candidate *txmgr.TxCandidate 132 | if l.Config.UseBlobs { 133 | var err error 134 | if candidate, err = l.blobTxCandidate(data); err != nil { 135 | // We could potentially fall through and try a calldata tx instead, but this would 136 | // likely result in the chain spending more in gas fees than it is tuned for, so best 137 | // to just fail. We do not expect this error to trigger unless there is a serious bug 138 | // or configuration issue. 139 | return fmt.Errorf("could not create blob tx candidate: %w", err) 140 | } 141 | } else { 142 | candidate = l.calldataTxCandidate(data) 143 | } 144 | 145 | intrinsicGas, err := core.IntrinsicGas(candidate.TxData, nil, false, true, true, false) 146 | if err != nil { 147 | // we log instead of return an error here because txmgr can do its own gas estimation 148 | l.Log.Error("Failed to calculate intrinsic gas", "err", err) 149 | } else { 150 | candidate.GasLimit = intrinsicGas 151 | } 152 | 153 | queue.Send(txdata, *candidate, receiptsCh) 154 | return nil 155 | } 156 | 157 | func (l *BatchSubmitter) blobTxCandidate(data []byte) (*txmgr.TxCandidate, error) { 158 | var b eth.Blob 159 | if err := b.FromData(data); err != nil { 160 | return nil, fmt.Errorf("data could not be converted to blob: %w", err) 161 | } 162 | return &txmgr.TxCandidate{ 163 | To: &l.RollupConfig.BatchInboxAddress, 164 | Blobs: []*eth.Blob{&b}, 165 | }, nil 166 | } 167 | ``` 168 | 169 | ### Fetcher 170 | 171 | [Pull Request(9098)](https://github.com/ethereum-optimism/optimism/pull/9098/files#diff-1fd8727490dbd2214b6d0c247eb222f2ac4098d259b4a45e9c0caea7fb2d3e08) 172 | 173 | #### GetBlob 174 | 175 | GetBlob负责获取blob数据,其主要逻辑包括利用4096个字段元素构建完整的blob,并通过commitment验证构建的blob的正确性。 176 | 同时,GetBlob也参与了上层[L1Retrieval中的逻辑流程](https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN/blob/main/sequencer/04-how-derivation-works.md)。 177 | 178 | ```go 179 | func (p *PreimageOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *eth.Blob { 180 | // Send a hint for the blob commitment & blob field elements. 181 | blobReqMeta := make([]byte, 16) 182 | binary.BigEndian.PutUint64(blobReqMeta[0:8], blobHash.Index) 183 | binary.BigEndian.PutUint64(blobReqMeta[8:16], ref.Time) 184 | p.hint.Hint(BlobHint(append(blobHash.Hash[:], blobReqMeta...))) 185 | 186 | commitment := p.oracle.Get(preimage.Sha256Key(blobHash.Hash)) 187 | 188 | // Reconstruct the full blob from the 4096 field elements. 189 | blob := eth.Blob{} 190 | fieldElemKey := make([]byte, 80) 191 | copy(fieldElemKey[:48], commitment) 192 | for i := 0; i < params.BlobTxFieldElementsPerBlob; i++ { 193 | binary.BigEndian.PutUint64(fieldElemKey[72:], uint64(i)) 194 | fieldElement := p.oracle.Get(preimage.BlobKey(crypto.Keccak256(fieldElemKey))) 195 | 196 | copy(blob[i<<5:(i+1)<<5], fieldElement[:]) 197 | } 198 | 199 | blobCommitment, err := blob.ComputeKZGCommitment() 200 | if err != nil || !bytes.Equal(blobCommitment[:], commitment[:]) { 201 | panic(fmt.Errorf("invalid blob commitment: %w", err)) 202 | } 203 | 204 | return &blob 205 | } 206 | ``` 207 | 208 | ### 其他杂项 209 | 210 | 除了以上几个主要模块外,还包含例如负责签署type3类型transaction的client sign模块,和fault proof相关涉及的模块,fault proof会在下一章节进行详细描述,这里就不过多赘述了 211 | 212 | [Pull Request(5452), fault proof相关](https://github.com/ethereum-optimism/optimism/commit/4739b0f8bfe2f3848af3f1a5661a038c5d602b2f#diff-790daa91002e5c07497fdc2d7c2149b551d77ccec1b1906cc70f575b7c7bad65) 213 | 214 | [Pull Request(9182), client sign相关](https://github.com/ethereum-optimism/optimism/pull/9185/files#diff-8046655b02fcced5322724e2cd61ece649a9d79ba09405093f9ed70b2087e47d) 215 | 216 | --------------------------------------------------------------------------------