L2链推导规范
本文档是L2链推导规范,描述了如何从L1数据中推导出L2区块,包括批量提交、架构设计以及有效负载属性推导等关键步骤。主要内容包括:从L1读取数据、构建通道、解码批次、重新排序交易,最终形成L2区块。文档还涵盖了L1重组的处理以及如何同步L1和L2状态。
L2 链推导规范
<!-- 本文件中所有的词汇表引用。--> [g-derivation]: glossary.md#L2-chain-derivation [g-payload-attr]: glossary.md#payload-attributes [g-block]: glossary.md#block [g-exec-engine]: glossary.md#execution-engine [g-reorg]: glossary.md#chain-re-organization [g-receipts]: glossary.md#receipt [g-inception]: glossary.md#L2-chain-inception [g-deposit-contract]: glossary.md#deposit-contract [g-deposited]: glossary.md#deposited-transaction [g-l1-attr-deposit]: glossary.md#l1-attributes-deposited-transaction [g-user-deposited]: glossary.md#user-deposited-transaction [g-deposits]: glossary.md#deposits [g-deposit-contract]: glossary.md#deposit-contract [g-l1-attr-predeploy]: glossary.md#l1-attributes-predeployed-contract [g-depositing-call]: glossary.md#depositing-call [g-depositing-transaction]: glossary.md#depositing-transaction [g-sequencing]: glossary.md#sequencing [g-sequencer]: glossary.md#sequencer [g-sequencing-epoch]: glossary.md#sequencing-epoch [g-sequencing-window]: glossary.md#sequencing-window [g-sequencer-batch]: glossary.md#sequencer-batch [g-l2-genesis]: glossary.md#l2-genesis-block [g-l2-chain-inception]: glossary.md#L2-chain-inception [g-batcher-transaction]: glossary.md#batcher-transaction [g-avail-provider]: glossary.md#data-availability-provider [g-batcher]: glossary.md#batcher [g-l2-output]: glossary.md#l2-output-root [g-fault-proof]: glossary.md#fault-proof [g-channel]: glossary.md#channel [g-channel-frame]: glossary.md#channel-frame [g-rollup-node]: glossary.md#rollup-node [g-channel-timeout]: glossary.md#channel-timeout [g-block-time]: glossary.md#block-time [g-time-slot]: glossary.md#time-slot [g-consolidation]: glossary.md#unsafe-block-consolidation [g-safe-l2-head]: glossary.md#safe-l2-head [g-safe-l2-block]: glossary.md#safe-l2-block [g-unsafe-l2-head]: glossary.md#unsafe-l2-head [g-unsafe-l2-block]: glossary.md#unsafe-l2-block [g-unsafe-sync]: glossary.md#unsafe-sync [g-l1-origin]: glossary.md#l1-origin [g-deposit-tx-type]: glossary.md#deposited-transaction-type [g-finalized-l2-head]: glossary.md#finalized-l2-head [g-system-config]: glossary.md#system-configuration
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> 目录
- 概述
- Batch Submission(批量提交)
- 架构
- Deriving Payload Attributes(推导 Payload 属性)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
概述
注意 以下假设使用单个排序器和批量器。将来,该设计将被调整以适应多个此类实体。
[L2 链推导][g-derivation] — 从 L1 数据推导 L2 [区块][g-block] — 是 [rollup 节点][g-rollup-node] 的主要职责之一,无论是在验证器模式下还是在排序器模式下(其中推导充当排序的完整性检查,并能检测 L1 链[重组][g-reorg])。
L2 链是从 L1 链推导出来的。特别是,每个 L1 区块都映射到一个 L2 [排序周期][g-sequencing-epoch],该周期包含多个 L2 区块。周期数定义为等于相应的 L1 区块数。
为了推导周期 E 中的 L2 区块,我们需要以下输入:
- 周期
E的 L1 [排序窗口][g-sequencing-window]:范围为[E, E + SWS)的 L1 区块,其中SWS是排序窗口大小(请注意,这意味着周期是重叠的)。特别是,我们需要:- 包含在排序窗口中的 [批量器交易][g-batcher-transaction]。这些允许我们重建包含要包含在 L2 区块中的交易的 [排序器批量][g-sequencer-batch](每个批量包含 L2 区块的列表)。
- 请注意,不可能在 L1 区块
E上有一个包含相对于周期E的批量的批量器交易,因为该批量必须包含 L1 区块E的哈希值。
- 请注意,不可能在 L1 区块
- 在 L1 区块
E中进行的 [存款][g-deposits](以 [存款合约][g-deposit-contract] 发出的事件的形式)。 - 来自 L1 区块
E的 L1 区块属性(以推导 [L1 属性存款交易][g-l1-attr-deposit])。
- 包含在排序窗口中的 [批量器交易][g-batcher-transaction]。这些允许我们重建包含要包含在 L2 区块中的交易的 [排序器批量][g-sequencer-batch](每个批量包含 L2 区块的列表)。
- 周期
E - 1的最后一个 L2 区块之后的 L2 链状态,或者 — 如果周期E - 1不存在 — [L2 创世状态][g-l2-genesis]。- 如果
E <= L2CI,则周期E不存在,其中L2CI是 [L2 链初始状态][g-l2-chain-inception]。
- 如果
要从头开始推导整个 L2 链,我们只需从 [L2 创世状态][g-l2-genesis] 开始,并将 [L2 链初始状态][g-l2-chain-inception] 作为第一个周期,然后按顺序处理所有排序窗口。有关我们如何在实践中实现这一点的更多信息,请参阅 架构部分。 L2 链可能包含 Bedrock 之前的历史记录,但此处的 L2 创世指的是第一个 Bedrock L2 区块。
每个周期可能包含可变数量的 L2 区块(每 l2_block_time 一个,在 Optimism 上为 2 秒),由 [排序器][g-sequencer] 决定,但每个区块都受以下约束:
min_l2_timestamp <= block.timestamp <= max_l2_timestamp,其中- 所有这些值都以秒为单位
min_l2_timestamp = l1_timestamp- 这确保了 L2 时间戳不落后于 L1 来源时间戳。
block.timestamp = prev_l2_timestamp + l2_block_timeprev_l2_timestamp是上一个周期的最后一个 L2 区块的时间戳l2_block_time是 L2 区块之间时间的可配置参数(在 Optimism 上为 2 秒)
max_l2_timestamp = max(l1_timestamp + max_sequencer_drift, min_l2_timestamp + l2_block_time)l1_timestamp是与 L2 区块周期关联的 L1 区块的时间戳max_sequencer_drift是允许排序器提前于 L1 的最大值
总而言之,这些约束意味着必须每 l2_block_time 秒有一个 L2 区块,并且一个周期的第一个 L2 区块的时间戳绝不能落后于与该 L2 区块周期匹配的 L1 区块的时间戳。
合并后,以太坊具有 12 秒的固定 [区块时间][g-block-time](尽管某些槽位可以跳过)。因此,预计使用 2 秒的 L2 区块时间,大多数情况下,每个周期将包含 12/2 = 6 个 L2 区块。
然而,排序器可以延长或缩短周期(受以上约束)。
这样做的理由是在 L1 上跳过一个槽位或与 L1 的连接暂时中断的情况下保持活跃性 — 这需要更长的周期。
然后需要更短的周期,以避免 L2 时间戳越来越超前于 L1。
请注意,min_l2_timestamp + l2_block_time 确保始终可以处理新的 L2 批量,即使超过了 max_sequencer_drift。但是,当超过 max_sequencer_drift 时,会强制进行到下一个 L1 来源,但有一个例外,以确保可以在下一个 L2 批量中满足最小时间戳边界(基于此下一个 L1 来源),并且在超过 max_sequencer_drift 时,len(batch.transactions) == 0 将继续执行。
有关更多详细信息,请参阅 [批量队列]。
Eager Block Derivation(积极区块推导)
在实践中,通常没有必要等待完整的 L1 区块排序窗口,才能开始推导周期中的 L2 区块。实际上,只要我们能够重建连续批量,我们就可以开始推导相应的 L2 区块。我们称之为积极区块推导。
但是,在最坏的情况下,我们只能通过读取排序窗口的最后一个 L1 区块来重建周期中第一个 L2 区块的批量。当该批量的一些数据包含在窗口的最后一个 L1 区块中时,就会发生这种情况。在这种情况下,我们不仅无法推导出周期中的第一个 L2 区块,而且在此之前我们也无法推导出该周期中的任何其他 L2 区块,因为它们需要应用该周期的第一个 L2 区块后产生的状态。 (请注意,这仅适用于区块推导。批量仍然可以被推导并尝试排队,我们只是无法从中创建区块。)
Batch Submission(批量提交)
Sequencing & Batch Submission Overview(排序与批量提交概述)
[排序器][g-sequencer] 接受来自用户的 L2 交易。它负责根据这些交易构建区块。对于每个这样的区块,它还会创建一个相应的 [排序器批量][g-sequencer-batch]。它还负责将每个批量提交给 [数据可用性提供商][g-avail-provider](例如,以太坊 calldata),它通过其 [批量器][g-batcher] 组件来完成。
L2 区块和批量之间的区别是微妙但重要的:区块包括 L2 状态根,而批量仅在给定的 L2 时间戳(相当于:L2 区块号)提交到交易。区块还包括对前一个区块的引用 (*)。
(*) 这在某些极端情况下很重要,在这些情况下,会发生 L1 重组并且批量将重新发布到 L1 链,但不会发布到前面的批量,而 L2 区块的前身不可能改变。
这意味着即使排序器错误地应用了状态转换,批量中的交易仍将被视为规范 L2 链的一部分。批量仍然需要经过有效性检查(即,必须正确编码),批量中的各个交易也是如此(例如,签名必须有效)。无效批量和有效批量中的无效单个交易会被正确的节点丢弃。
如果排序器错误地应用了状态转换并发布了一个 [输出根][g-l2-output],那么这个输出根将是不正确的。不正确的输出根将受到 [错误证明][g-fault-proof] 的质疑,然后被现有排序器批量的正确输出根替换。
有关更多信息,请参阅 批量提交规范。
Batch Submission Wire Format(批量提交线路格式)
批量提交与 L2 链推导密切相关,因为推导过程必须解码已编码用于批量提交目的的批量。
[批量器][g-batcher] 将 [批量器交易][g-batcher-transaction] 提交给 [数据可用性提供商][g-avail-provider]。这些交易包含一个或多个 [通道帧][g-channel-frame],这些帧是属于 [通道][g-channel] 的数据块。
[通道][g-channel] 是 [排序器批量][g-sequencer-batch](对于任何 L2 区块)压缩在一起的序列。将多个批量分组在一起的原因仅仅是为了获得更好的压缩率,从而降低数据可用性成本。
通道可能太大而无法容纳在单个 [批量器交易][g-batcher-transaction] 中,因此我们需要将其拆分为称为 [通道帧][g-channel-frame] 的块。单个批量器交易还可以携带多个帧(属于相同或不同的通道)。
此设计使我们能够以最大的灵活性将批量聚合到通道中,并将通道拆分到批量器交易中。值得注意的是,它使我们能够最大程度地利用批量器交易中的数据使用率:例如,它使我们能够将窗口的最后一个(小)帧与来自下一个窗口的大帧打包在一起。
将来,此通道识别功能还允许 [批量器][g-batcher] 采用多个签名者(私钥)来并行提交一个或多个通道 (1)。
(1) 这有助于缓解以下问题:由于影响 L2 tx-pool 并因此影响包含的交易 nonce 值:同一签名者进行的多个交易被卡住等待先前交易的包含。
另请注意,我们使用流式压缩方案,当我们启动通道时,我们不需要知道通道最终将包含多少个区块,甚至在通道中发送第一个帧时也不知道。
通过跨多个数据交易拆分通道,L2 可以具有比数据可用性层可能支持的更大的区块数据。
所有这些都在下图中说明。说明如下。
第一行表示 L1 区块及其编号。L1 区块下方的框表示包含在区块中的 [批量器交易][g-batcher-transaction]。L1 区块下方的波浪线表示 [存款][g-deposits](更具体地说是,由 [存款合约][g-deposit-contract] 发出的事件)。
框中的每个彩色块表示一个 [通道帧][g-channel-frame]。因此 A 和 B 是 [通道][g-channel],而 A0、A1、B0、B1、B2 是帧。请注意:
- 多个通道被交错
- 帧不需要按顺序传输
- 单个批量器交易可以携带来自多个通道的帧
在下一行中,圆角框表示从通道中提取的各个 [排序器批量][g-sequencer-batch]。四个蓝色/紫色/粉色是从通道 A 推导出来的,而其他是从通道 B 推导出来的。这些批量在这里按从批量中解码的顺序表示(在本例中,B 首先被解码)。
注意 此处的标题说“首先看到通道 B,并将首先解码为批量”,但这不是一个要求。例如,实现同样可以接受窥视通道并首先解码包含最旧批量的通道。
图的其余部分在概念上与第一部分不同,并说明了在重新排序通道后 L2 链的推导。
第一行显示了批量器交易。请注意,在这种情况下,存在批量的排序,使通道内的所有帧看起来都是连续的。但这通常不是真的。例如,在第二笔交易中,A1 和 B0 的位置可以颠倒,结果完全相同 — 无需更改图中的其余部分。
第二行显示了按正确顺序重建的通道。第三行显示了从通道中提取的批量。因为通道是有序的,并且通道中的批量是按顺序排列的,这意味着批量也是有序的。第四行显示了从每个批量推导出的 [L2 区块][g-block]。请注意,这里我们有一个 1-1 的批量到区块的映射,但正如我们稍后将看到的,在 L1 上发布的批量中存在“差距”的情况下,可以插入不映射到批量的空区块。
第五行显示了 [L1 属性存款交易][g-l1-attr-deposit],它在每个 L2 区块中记录有关与 L2 区块周期匹配的 L1 区块的信息。第一个数字表示周期/L1x 数字,而第二个数字(“序列号”)表示周期内的位置。
最后,第六行显示了从前面提到的 [存款合约][g-deposit-contract] 事件中推导出的 [用户存款交易][g-user-deposited]。
请注意图右下角的 101-0 L1 属性交易。只有在以下情况下才有可能在那里存在:帧 B2 指示它是通道中的最后一个帧,并且 (2) 不能插入空区块。
该图未指定正在使用的排序窗口大小,但由此我们可以推断出它必须至少为 4 个区块,因为通道 A 的最后一个帧出现在区块 102 中,但属于周期 99。
至于关于“安全类型”的注释,它解释了在 L1 和 L2 上使用的区块分类。
- [不安全 L2 区块][g-unsafe-l2-block]:
- [安全 L2 区块][g-safe-l2-block]:
- 最终 L2 区块:请参阅从 [最终确定][g-finalized-l2-head] L1 数据推导出的区块。
这些安全级别映射到与 执行引擎 API 交互时传输的 headBlockHash、safeBlockHash 和 finalizedBlockHash 值。
Batcher Transaction Format(批量器交易格式)
批量器交易被编码为 version_byte ++ rollup_payload(其中 ++ 表示连接)。
version_byte |
rollup_payload |
|---|---|
| 0 | frame ... (一个或多个帧,连接) |
未知版本使批量器交易无效(rollup 节点必须忽略它)。 批量器交易中的所有帧都必须可解析。如果任何一个帧无法解析,则会拒绝交易中的所有帧。
批量交易通过验证交易的 to 地址是否与批量收件箱地址匹配,以及 from 地址是否与 [系统配置][g-system-config] 中数据读取的 L1 区块的批量发送者地址匹配来验证。
Frame Format(帧格式)
[通道帧][g-channel-frame] 被编码为:
frame = channel_id ++ frame_number ++ frame_data_length ++ frame_data ++ is_last
channel_id = bytes16
frame_number = uint16
frame_data_length = uint32
frame_data = bytes
is_last = bool
其中 uint32 和 uint16 都是大端无符号整数。类型名称应根据 [Solidity ABI] 进行解释和编码。
帧中的所有数据都是固定大小的,除了 frame_data。固定开销为 16 + 2 + 4 + 1 = 23 字节。
固定大小的帧元数据避免了与目标总数据长度的循环依赖,
从而简化了具有不同内容长度的帧的打包。
其中:
channel_id是通道的不透明标识符。不应重复使用,建议使用随机标识符;但是,在超时规则之外,不会检查其有效性frame_number标识帧在通道中的索引frame_data_length是frame_data的长度(以字节为单位)。上限为 1,000,000 字节。frame_data是属于通道的字节序列,在逻辑上位于来自前一个帧的字节之后is_last是一个单字节,如果帧是通道中的最后一个,则值为 1,如果通道中有帧,则为 0。 任何其他值都会使帧无效(rollup 节点必须忽略它)。
Channel Format(通道格式)
通道被编码为 channel_encoding,定义为:
rlp_batches = []
for batch in batches:
rlp_batches.append(batch)
channel_encoding = compress(rlp_batches)
其中:
batches是输入,它是按下一节(“批量编码”)中每个批量字节编码的批量序列rlp_batches是 RLP 编码批量的连接compress是执行压缩的函数,使用 ZLIB 算法(如 RFC-1950 中指定),不带字典channel_encoding是rlp_batches的压缩版本
解压缩通道时,我们将解压缩数据的量限制为 MAX_RLP_BYTES_PER_CHANNEL(当前为 10,000,000 字节),以避免“zip-bomb”类型的攻击(其中一个小的压缩输入解压缩为大量数据)。如果解压缩的数据超过了限制,则进行的处理就好像通道仅包含第一个 MAX_RLP_BYTES_PER_CHANNEL 解压缩字节一样。该限制设置在 RLP 解码上,因此即使通道的大小大于 MAX_RLP_BYTES_PER_CHANNEL,所有可以在 MAX_RLP_BYTES_PER_CHANNEL 中解码的批量都将被接受。确切的要求是 length(input) <= MAX_RLP_BYTES_PER_CHANNEL。
虽然上面的伪代码暗示了所有批量都是预先知道的,但可以对 RLP 编码的批量执行流式压缩和解压缩。这意味着我们有可能在知道通道将包含多少个批量(以及多少帧)之前,就开始在 [批量器交易][g-batcher-transaction] 中包含通道帧。
Batch Format(批量格式)
回想一下,批量包含要包含在特定 L2 区块中的交易列表。
批量被编码为 batch_version ++ content,其中 content 取决于 batch_version:
batch_version |
content |
|---|---|
| 0 | rlp_encode([parent_hash, epoch_number, epoch_hash, timestamp, transaction_list]) |
其中:
batch_version是单个字节,类似于交易类型,位于 RLP 内容之前。rlp_encode是根据 RLP 格式 编码批量的函数,[x, y, z]表示包含项目x、y和z的列表parent_hash是上一个 L2 区块的区块哈希epoch_number和epoch_hash是与 L2 区块的 [排序周期][g-sequencing-epoch] 对应的 L1 区块的编号和哈希timestamp是 L2 区块的时间戳transaction_list是 EIP-2718 编码交易的 RLP 编码列表。
未知版本以及格式错误的内容会使批量无效(rollup 节点必须忽略它)。
epoch_number 和 timestamp 还必须遵守 批量队列 部分中列出的约束,否则该批量将被视为无效并将被忽略。
架构
以上主要描述了 L2 链推导中使用的常规编码, 主要是批量如何在 [批量器交易][g-batcher-transaction] 中编码。
本节介绍如何使用管道架构从 L1 批量生成 L2 链。
验证器可能会以不同的方式实现此操作,但必须在语义上等效,才能不偏离 L2 链。
L2 Chain Derivation Pipeline(L2 链推导管线)
我们的架构将推导过程分解为由以下阶段组成的管道:
- L1 Traversal(L1 遍历)
- L1 Retrieval(L1 检索)
- Frame Queue(帧队列)
- Channel Bank(通道库)
- Channel Reader (Batch Decoding)(通道读取器(批量解码))
- Batch Queue(批量队列)
- Payload Attributes Derivation(Payload 属性推导)
- Engine Queue(引擎队列)
数据从管道的开始(外部)流向结束(内部)。 从最内层阶段拉取最外层阶段的数据。
但是,数据是按相反的顺序处理的。这意味着如果最后一个阶段中有任何要处理的数据,它将首先被处理。处理按每个阶段都可以采取的“步骤”进行。在内部阶段中处理数据之前,我们尝试在最后一个(最内部)阶段中尽可能多地执行步骤,依此类推。
这确保了我们在提取更多数据之前使用我们已经拥有的数据,并最大限度地减少数据遍历推导管道的延迟。
每个阶段都可以根据需要维护其自己的内部状态。特别是,每个阶段都维护一个 L1 区块引用(编号 + 哈希),该引用指向最新的 L1 区块,以便完全处理来自先前区块的所有数据,并且正在处理或已处理来自该区块的数据。这使得最内层阶段能够考虑到用于生成 L2 链的 L1 数据可用性的最终确定,从而在 L2 链输入变得不可逆时反映在 L2 链分叉选择中。
让我们简要描述一下管道的每个阶段。
L1 Traversal(L1 遍历)
在L1 遍历阶段,我们只需读取下一个 L1 区块的标头。在正常操作中,这些将是创建时的新的 L1 区块,尽管我们也可以在同步时或在 L1 [重组][g-reorg] 的情况下读取旧区块。
在遍历 L1 区块后,会更新 L1 检索阶段使用的 [系统配置][g-system-config] 副本,以便批量发送者身份验证始终准确地对应于该阶段读取数据的 L1 区块。
L1 Retrieval(L1 检索)
在L1 检索阶段,我们读取从外部阶段(L1 遍历)获取的区块,并从中提取数据。 默认情况下,rollup 在从区块中的 [批量器交易][g-batcher-transaction] 检索的 calldata 上运行,对于每个交易:
- 接收者必须是配置的批量收件箱地址。
- 发送者必须与从与数据 L1 区块匹配的系统配置中加载的批量地址匹配。
每个数据交易都经过版本控制,并包含一系列 [通道帧][g-channel-frame],以供帧队列读取,请参阅 批量提交线路格式。
Frame Queue(帧队列)
帧队列一次缓冲一个数据交易, 解码为 [通道帧][g-channel-frame],以供下一阶段使用。 请参阅 批量器交易格式 和 帧格式 规范。
Channel Bank(通道库)
通道库阶段负责管理从 L1 检索阶段写入的通道库的缓冲。通道库阶段中的一个步骤尝试从“就绪”的通道读取数据。
当前通道已完全缓冲,直到被读取或删除, 未来的 ChannelBank 版本可能会支持流式通道。
为了限制资源使用,通道库会根据通道大小进行剪枝,并使旧通道超时。
通道按 FIFO 顺序记录在称为通道队列的结构中。通道第一次被看到属于该通道的帧时,会被添加到通道队列中。
Pruning(剪枝)
成功插入新帧后,通道库会被剪枝:
通道按 FIFO 顺序删除,直到 total_size <= MAX_CHANNEL_BANK_SIZE,其中:
total_size是每个通道大小的总和,该大小是通道的所有缓冲帧数据的总和, 每个帧还有额外的200字节的帧开销。MAX_CHANNEL_BANK_SIZE是一个协议常量,为 100,000,000 字节。
Timeouts(超时)
通道打开时所在的 L1 来源会与通道一起作为 channel.open_l1_block 进行跟踪,
并确定通道数据保留的最大 L1 区块跨度(然后被剪枝)。
如果:current_l1_block.number > channel.open_l1_block.number + CHANNEL_TIMEOUT,则通道超时,其中:
current_l1_block是该阶段当前遍历的 L1 来源。CHANNEL_TIMEOUT是一个可 rollup 配置的参数,以 L1 区块数表示。
超时通道的新帧将被删除,而不是缓冲。
Reading(读取)
通道库只能输出从第一个打开的通道获取的数据。
读取时,如果第一个打开的通道超时,则将其从通道库中删除。
一旦第一个打开的通道(如果有)未超时并且已准备就绪,则可以读取它并将其从通道库中删除。
如果满足以下条件,则通道已准备就绪:
- 通道已关闭
- 通道具有连续的帧序列,直到关闭帧
如果没有通道准备就绪,则读取下一个帧并将其提取到通道库中。
Loading frames(加载帧)
当帧引用的通道 ID 尚未出现在通道库中时, 会打开一个新通道,标记为当前 L1 区块,并附加到通道队列中。
帧插入条件:
- 与已超时但尚未从通道库中删除的通道匹配的新帧将被丢弃。
- 对于尚未从通道库中删除的帧,重复的帧(按帧号)将被丢弃。
- 重复的关闭帧(新帧
is_last == 1,但通道已经看到一个关闭帧,并且尚未从通道库中删除)将被丢弃。
如果一个帧正在关闭(is_last == 1),则任何现有的编号较高的帧都将从通道中删除。
请注意,虽然这允许通道 ID 在从通道库中删除后被重用,但建议 batcher 的实现使用唯一的通道 ID。
通道读取器(批量解码)
在这个阶段,我们解压缩从上一个阶段拉取的通道,然后从解压缩的字节流中解析[批次][g-sequencer-batch]。
有关解压缩和解码规范,请参见批次格式。
批次队列
在 批次缓冲 阶段,我们按时间戳重新排序批次。如果某些[时间槽][g-time-slot]缺少批次,并且存在具有更高时间戳的有效批次, 则此阶段还会生成空批次以填补空白。
只要有一个紧随当前[安全 L2 头][g-safe-l2-head](可以从规范 L1 链派生的最后一个区块)的时间戳的连续批次, 批次就会被推送到下一阶段。批次的父哈希也必须与当前安全 L2 头的哈希匹配。
请注意,从 L1 派生的批次中存在任何空白,这意味着此阶段需要缓冲整个[排序窗口][g-sequencing-window], 然后才能生成空批次(因为在最坏的情况下,缺失的批次可能在该窗口的最后一个 L1 区块中包含数据)。
一个批次可以有 4 种不同的有效性形式:
drop:批次无效,并且始终在未来,除非我们进行重组(reorg)。它可以从缓冲区中删除。accept:批次有效,应该被处理。undecided:我们缺乏 L1 信息,直到我们可以继续进行批次过滤。future:批次可能有效,但尚未处理,应该稍后再次检查。
批次按照包含在 L1 上的顺序进行处理:如果可以 accept-ed 多个批次,则应用第一个。
实现可以推迟 future 批次到稍后的派生步骤,以减少验证工作。
批次的有效性派生如下:
定义:
batch如 批次格式部分 中定义。epoch = safe_l2_head.l1_origin是一个与批次相关的[L1 来源][g-l1-origin],具有以下属性:number(L1 区块号),hash(L1 区块哈希)和timestamp(L1 区块时间戳)。inclusion_block_number是batch首次 完全 派生时的 L1 区块号, 即由前一阶段解码和输出。next_timestamp = safe_l2_head.timestamp + block_time是下一个批次应具有的预期 L2 时间戳, 请参见 [区块时间信息][g-block-time]。next_epoch可能尚未知,但如果可用,它将是epoch之后的 L1 区块。batch_origin是epoch或next_epoch,具体取决于验证。
请注意,可以推迟批次的处理,直到 batch.timestamp <= next_timestamp,
因为无论如何都必须保留 future 批次。
规则,按验证顺序:
batch.timestamp > next_timestamp->future:即批次必须准备好处理。batch.timestamp < next_timestamp->drop:即批次不能太旧。batch.parent_hash != safe_l2_head.hash->drop:即父哈希必须等于 L2 安全头区块哈希。batch.epoch_num + sequence_window_size < inclusion_block_number->drop:即批次必须及时包含。batch.epoch_num < epoch.number->drop:即批次来源不应早于 L2 安全头的来源。batch.epoch_num == epoch.number:将batch_origin定义为epoch。batch.epoch_num == epoch.number+1:- 如果
next_epoch未知 ->undecided: 即,在获得 L1 来源数据之前,无法处理更改 L1 来源的批次。 - 如果已知,则将
batch_origin定义为next_epoch
- 如果
batch.epoch_num > epoch.number+1->drop:即,每个 L2 区块,L1 来源的变化不能超过一个 L1 区块。batch.epoch_hash != batch_origin.hash->drop:即,批次必须引用规范的 L1 来源, 以防止批次被重放到意外的 L1 链上。batch.timestamp < batch_origin.time->drop:强制执行最小 L2 时间戳规则。batch.timestamp > batch_origin.time + max_sequencer_drift:强制执行 L2 时间戳漂移规则, 但例外情况是保留上述最小 L2 时间戳不变式:len(batch.transactions) == 0:epoch.number == batch.epoch_num: 这意味着批次尚未提前 L1 来源,因此必须对照next_epoch进行检查。- 如果
next_epoch未知 ->undecided: 没有下一个 L1 来源,我们尚无法确定是否可以保持时间不变性。 - 如果
batch.timestamp >= next_epoch.time->drop: 批次本来可以采用下一个 L1 来源,而不会破坏L2 time >= L1 time不变性。
- 如果
len(batch.transactions) > 0->drop: 当超过 sequencer 时间漂移时,永远不允许 sequencer 包含交易。
batch.transactions:如果batch.transactions列表包含无效或仅通过其他方式派生的交易,则drop:- 任何为空的交易(零长度字节字符串)
- 任何[已存款交易][g-deposit-tx-type](由交易类型前缀字节标识)
如果无法 accept-ed 任何批次,并且该阶段已完成缓冲可以从高度为 epoch.number + sequence_window_size 的 L1
区块中完全读取的所有批次,并且 next_epoch 可用,
则可以使用以下属性派生一个空批次:
parent_hash = safe_l2_head.hashtimestamp = next_timestamptransactions为空,即没有 sequencer 交易。可以在下一阶段添加已存款交易。- 如果
next_timestamp < next_epoch.time:则重复当前的 L1 来源,以保持 L2 时间不变性。epoch_num = epoch.numberepoch_hash = epoch.hash
- 如果批次是 epoch 的第一个批次,则使用该 epoch 而不是提前 epoch,以确保
每个 epoch 至少有一个 L2 区块。
epoch_num = epoch.numberepoch_hash = epoch.hash
- 否则,
epoch_num = next_epoch.numberepoch_hash = next_epoch.hash
有效载荷属性派生
在 有效载荷属性派生 阶段,我们会将从上一阶段获得的批次转换为
[PayloadAttributes][g-payload-attr] 结构的实例。这样的结构编码了需要包含在区块中的交易,
以及其他区块输入(时间戳,费用接收者等)。有效载荷属性派生的详细信息在下面的
派生有效载荷属性部分中。
此阶段维护其自己的[系统配置][g-system-config]副本,独立于 L1 检索阶段。每当批次输入引用的 L1 epoch 更改时,系统配置都会使用 L1 日志事件进行更新。
引擎队列
在 引擎队列 阶段,先前派生的 PayloadAttributes 结构被缓冲并发送到
[执行引擎][g-exec-engine],以执行并转换为正确的 L2 区块。
该阶段维护对三个 L2 区块的引用:
- [最终确定的 L2 头][g-finalized-l2-head]:直到包括此区块的所有内容都可以从 最终确定的(即规范且永久不可逆)的 L1 链部分完全派生。
- [安全 L2 头][g-safe-l2-head]:直到包括此区块的所有内容都可以从 当前规范的 L1 链中完全派生。
- [不安全 L2 头][g-unsafe-l2-head]:安全头和不安全头之间的区块是[不安全区块][g-unsafe-l2-block], 这些区块不是从 L1 派生的。这些区块要么来自排序(在排序器模式下),要么来自与排序器的 [不安全同步][g-unsafe-sync](在验证器模式下)。 这也称为“最新”头。
此外,它还会缓冲最近处理的安全 L2 区块的引用的简短历史记录,以及每个区块的派生来源的 L1 区块的引用。 此历史记录不必完整,但可以将以后的 L1 终结性信号转换为 L2 终结性。
引擎 API 用法
为了与引擎交互,使用了执行引擎 API,其中包含以下 JSON-RPC 方法:
engine_forkchoiceUpdatedV1— 如果不同,则将 forkchoice(即链头)更新为headBlockHash, 如果有效载荷属性参数不为null,则指示引擎开始构建执行有效载荷。engine_getPayloadV1— 检索先前请求的执行有效载荷构建。engine_newPayloadV1— 执行执行有效载荷以创建区块。
执行有效载荷是 ExecutionPayloadV1 类型的对象。
Forkchoice 同步
如果有任何要应用的 forkchoice 更新,在派生或处理其他输入之前,这些更新将首先应用于引擎。
以下情况下可能会发生此同步:
- L1 终结性信号最终确定一个或多个 L2 区块:更新“已最终确定”的 L2 区块。
- 成功合并不安全 L2 区块:更新“安全”的 L2 区块。
- 派生管道重置后的第一件事,以确保一致的执行引擎 forkchoice 状态。
新的 forkchoice 状态将使用 engine_forkchoiceUpdatedV1 应用。
在 forkchoice 状态有效性错误时,必须重置派生管道以恢复到一致状态。
L1-整合:有效载荷属性匹配
如果不安全头领先于安全头,则会尝试[整合][g-consolidation],验证现有的不安全 L2 链是否与从规范 L1 数据派生的 L2 输入匹配。
在整合期间,我们会考虑最旧的不安全 L2 区块,即紧随安全头之后的不安全 L2 区块。如果有效载荷属性与此最旧的不安全 L2 区块匹配,则可以将该区块视为“安全”,并成为新的安全头。
派生的 L2 有效载荷属性的以下字段将检查与 L2 区块的相等性:
parent_hashtimestamprandaofee_recipienttransactions_list(首先是长度,然后是每个编码交易的相等性,包括存款)
如果整合成功,forkchoice 变更将按照上述部分中的描述进行同步。
如果整合失败,L2 有效载荷属性将立即处理,如下所述。 选择有效载荷属性是为了支持先前的安全 L2 区块,从而在当前安全区块之上创建 L2 链重组。立即处理新的替代属性使 go-ethereum 等执行引擎能够实施该变更,因为可能不支持对链的顶端进行线性回滚。
L1-同步:有效载荷属性处理
如果安全和不安全 L2 头相同(无论是由于整合失败与否),我们会将 L2 有效载荷属性发送到执行引擎,以构建为正确的 L2 区块。然后,此 L2 区块将成为新的 L2 安全头和不安全头。
如果由于验证错误(即该区块中存在无效交易或状态转换),无法将从批次创建的有效载荷属性插入到链中,则应删除该批次,并且不应提前安全头。引擎队列将尝试使用批次队列中的下一个批次用于该时间戳。如果找不到有效的批次,则汇总节点将创建一个仅存款批次,该批次应始终通过验证,因为存款始终有效。
通过执行引擎 API 与执行引擎的交互在与执行引擎的通信部分中进行了详细说明。
然后,使用以下顺序处理有效载荷属性:
engine_forkchoiceUpdatedV1,其中包含该阶段的当前 forkchoice 状态,以及用于启动区块构建的属性。- 必须禁用非确定性来源(如 tx-pool)以重新构建预期的区块。
engine_getPayload,以通过上一步结果中的有效载荷 ID 检索有效载荷。engine_newPayload,以将新有效载荷导入到执行引擎中。engine_forkchoiceUpdatedV1,以使新有效载荷成为规范的有效载荷, 现在safe和unsafe字段都更改为引用有效载荷,并且没有有效载荷属性。
引擎 API 错误处理:
- 在 RPC 类型错误时,应在将来的步骤中重新尝试有效载荷属性处理。
- 在有效载荷处理错误时,必须删除属性,并且不得更改 forkchoice 状态。
- 最终,派生管道将生成备用有效载荷属性,无论是否有批次。
- 如果有效载荷属性仅包含存款,那么如果这些属性无效,则这是一个严重的派生错误。
- 在 forkchoice 状态VALIDITY错误时,必须重置派生管道以恢复到一致状态。
处理不安全的有效负载属性
如果没有 forkchoice 更新或 L1 数据要处理,并且如果下一个可能的 L2 区块已通过不安全的来源(例如 sequencer 通过 p2p 网络发布它)可用, 则会乐观地将其处理为“不安全”区块。在理想情况下,这会将以后的派生工作减少到仅与 L1 进行整合,并使 用户能够比 L1 确认 L2 数据包更快地看到 L2 链的头。
要处理不安全的有效负载,有效负载必须:
- 具有大于当前安全 L2 块编号的块编号。
- 仅由于 L1 重组才能重新组织安全 L2 块。
- 具有与当前不安全 L2 块匹配的父级 blockhash。
- 可以防止执行引擎单独同步不安全 L2 链中更大的间隙。
- 避免不安全 L2 块重新组织其他先前验证的 L2 块。
- 此检查可能会在将来的版本中更改,以采用例如 L1 快照同步协议。
然后,按以下顺序处理有效负载:
engine_newPayloadV1:处理有效负载。它尚未成为规范。engine_forkchoiceUpdatedV1:使有效负载成为规范的不安全 L2 块,并保留安全/已完成的 L2 块。
引擎 API 错误处理:
- 在 RPC 类型错误时,应在将来的步骤中重新尝试有效负载处理。
- 在有效负载処理错误时,必须删除有效负载,并且不能将其标记为规范。
- 在 forkchoice 状态VALIDITY错误时,必须重置派生管道以恢复到一致状态。
重置管道
可以重置管道,例如,如果我们检测到 L1 [重组(reorganization)][g-reorg]。 这使汇总节点能够处理 L1 链重组事件。
重置会将管道恢复为一种状态,该状态产生的输出与完整的 L2 派生过程相同, 而是从现有的 L2 链开始,该链的回溯程度足以与当前的 L1 链协调一致。
请注意,此算法涵盖了几个重要的用例:
- 在不从 0 开始的情况下初始化管道,例如,当汇总节点使用现有的引擎实例重新启动时。
- 如果管道与执行引擎链变得不一致,则恢复管道,例如,当引擎同步/更改时。
- 当 L1 链重组时恢复管道,例如,延迟的 L1 区块被孤立,或较大的认证失败。
- 初始化管道,以在故障证明程序内部使用先前的 L1 和 L2 历史记录来派生有争议的 L2 区块。
处理这些情况也意味着可以将节点配置为急切地同步具有 0 个确认的 L1 数据, 因为如果 L1 以后确实将数据识别为规范,则它可以撤消更改,从而实现安全的低延迟使用。
首先重置引擎队列,以确定要从其中继续派生的 L1 和 L2 起始点。 此后,其他阶段将彼此重置。
查找同步起始点
要查找起始点,相对于链的回溯方向,有几个步骤:
- 找到当前的 L2 forkchoice 状态
- 如果找不到
finalized区块,则从 Bedrock genesis 区块开始。 - 如果找不到
safe区块,则回退到finalized区块。 unsafe区块应始终可用并与上述一致 (在罕见的引擎损坏恢复情况下可能并非如此,正在审查中)。
- 如果找不到
- 找到第一个具有合理的 L1 参考的 L2 区块,作为新的
unsafe起始点, 从先前的unsafe返回到finalized,不再进一步。- 合理的条件是:L2 区块的 L1 源已知且规范,或者未知且具有领先于 L1 的块号。
- 找到第一个具有比排序窗口更旧的 L1 参考的 L2 区块,作为新的
safe起始点, 从上面的合理的unsafe头返回到finalized,不再进一步。- 如果在任何时候 L1 源已知但不是规范,则
unsafe头将修订为当前的父级。 - 具有已知规范 L1 源的最高 L2 区块将被记住为
highest。 - 如果在任何时候,区块中的 L1 源相对于派生规则是损坏的,则会发生错误。损坏包括:
- L1 源块号或父哈希与父 L1 源不一致
- L1 序列号不一致(始终更改为
0表示 L1 源更改,如果不更改则递增1)
- 如果 L2 区块
n的 L1 源比highest的 L1 源大一个序列窗口, 并且n.sequence_number == 0,则n的父 L2 区块将是safe起始点。
- 如果在任何时候 L1 源已知但不是规范,则
finalizedL2 区块作为finalized起始点持续存在。- 找到第一个具有比通道超时旧的 L1 参考的 L2 区块
- 此区块引用的 L1 源(我们称之为
l2base)将成为 L2 管道派生的base: 通过从这里开始,各个阶段可以缓冲任何必要的数据,同时删除不完整的派生输出,直到 L1 遍历赶上实际的 L2 安全头。
- 此区块引用的 L1 源(我们称之为
在回溯 L2 链时,实现可以健全性检查,即与现有的 forkchoice 状态相比,起始点从未设置得太远, 以避免由于错误配置而导致的密集重组。
实施者请注意:步骤 1-4 称为 FindL2Heads。步骤 5 当前是引擎队列重置的一部分。
这可能会更改为将起始点搜索与裸重置逻辑隔离。
重置派生阶段
- L1 遍历:从 L1
base开始,作为下一个阶段要拉取的第一个区块。 - L1 检索:清空先前的数据,并获取
baseL1 数据,或者将获取工作推迟到以后的管道步骤。 - 帧队列:清空队列。
- 通道库:清空通道库。
- 通道读取器:重置任何批次解码状态。
- 批次队列:清空批次队列,使用
base作为初始 L1 参考点。 - 有效载荷属性派生:清空任何批次/属性状态。
- 引擎队列:
- 使用同步起始点状态初始化 L2 forkchoice 状态。(
finalized/safe/unsafe) - 将该阶段的 L1 参考点初始化为
base。 - 需要 forkchoice 更新作为第一个任务
- 重置任何终结性数据
- 使用同步起始点状态初始化 L2 forkchoice 状态。(
在必要时,从 base 开始的阶段可以从 l2base 区块中编码的数据初始化其系统配置。
关于合并后的重组
请注意,在 merge 后,重组的深度将受到 L1 终结延迟 的限制 (2 个 L1 信标 epoch,或大约 13 分钟,除非超过 1/3 的网络持续不同意)。 新的 L1 区块可以每 L1 信标 epoch(大约 6.4 分钟)完成,并且根据这些 终结性信号和批次包含,派生的 L2 链也将变为不可逆的。
请注意,这种形式的终结仅影响输入,并且节点可以主观地说链是不可逆的, 通过从这些不可逆的输入以及设置的协议规则和参数中重现链。
但这与发布在 L1 上的输出完全无关,发布在 L1 上的输出需要一种证明形式,例如故障证明或 zk 证明才能完成。乐观rollup输出(例如 L1 上的提款)仅在经过一周未经争议(故障证明挑战窗口)后被标记为“已完成”, 这与权益证明完成发生名称冲突。
派生有效载荷属性
对于从 L1 数据派生的每个 L2 区块,我们需要构建[有效载荷属性][g-payload-attr],
由 PayloadAttributesV1 对象的扩展版本表示,
其中包括其他 transactions 和 noTxPool 字段。
此过程发生在验证器节点运行的有效载荷属性队列中,以及 sequencer 节点运行的区块生产过程中(如果批量提交交易,则 sequencer 可以启用 tx-pool 使用)。
派生交易列表
对于要由 sequencer 创建的每个 L2 区块,我们从与 目标 L2 区块号匹配的[Sequencer 批次][g-sequencer-batch]开始。如果 L1 链未包含目标 L2 区块号的批次,则这可能是空的自动生成的批次。 记住,该批次包括[排序 epoch][g-sequencing-epoch]号、L2 时间戳和交易列表。
此区块是[排序 epoch][g-sequencing-epoch]的一部分, 其数字与 L1 区块的数字匹配(其 [L1 origin][g-l1-origin])。 此 L1 区块用于派生 L1 属性以及(对于 epoch 中的第一个 L2 区块)用户存款。
因此,PayloadAttributesV1 对象必须包括以下交易:
- 一个或多个[已存款交易][g-deposited],分为两种:
- 从 L1 origin 派生的单个 [L1 属性已存款交易][g-l1-attr-deposit]。
- 对于epoch中的第一 L2 个区块,从 L1 origin 的[收据][g-receipts]派生的零个或多个 [用户已存款交易][g-user-deposited]。
- 零个或多个 [排序交易][g-sequencing]:由 L2 用户签名的常规交易,包含在 sequencer 批次中。
交易 必须 按此顺序出现在有效载荷属性中。
L1 属性从 L1 区块头读取,而存款从 L1 区块的[收据][g-receipts]中读取。 有关如何将存款编码为日志条目的详细信息,请参阅 存款合约规范。
构建单个有效载荷属性
在派生出交易列表后,汇总节点按如下方式构造 PayloadAttributesV1:
timestamp设置为批次的时间戳。random设置为prev_randaoL1 区块属性。suggestedFeeRecipient设置为 Sequencer Fee Vault 地址。参见 Fee Vaults 规范。transactions是派生的交易数组:已存款交易和排序的交易,所有交易都使用 EIP-2718 进行编码。noTxPool设置为true,以在构造块时使用上述确切的transactions列表。gasLimit设置为此有效载荷的[系统配置][g-system-config]中的当前gasLimit值。
- 原文链接: github.com/ethereum-opti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~