L2链推导规范

ethereum-optimism 发布于 2024-01-23 阅读 1083

本文档是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 --> 目录

<!-- 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 区块 E 中进行的 [存款][g-deposits](以 [存款合约][g-deposit-contract] 发出的事件的形式)。
    • 来自 L1 区块 E 的 L1 区块属性(以推导 [L1 属性存款交易][g-l1-attr-deposit])。
  • 周期 E - 1 的最后一个 L2 区块之后的 L2 链状态,或者 — 如果周期 E - 1 不存在 — [L2 创世状态][g-l2-genesis]。
    • 如果 E &lt;= 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 &lt;= block.timestamp &lt;= max_l2_timestamp,其中
    • 所有这些值都以秒为单位
    • min_l2_timestamp = l1_timestamp
      • 这确保了 L2 时间戳不落后于 L1 来源时间戳。
    • block.timestamp = prev_l2_timestamp + l2_block_time
      • prev_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]。因此 AB 是 [通道][g-channel],而 A0A1B0B1B2 是帧。请注意:

  • 多个通道被交错
  • 帧不需要按顺序传输
  • 单个批量器交易可以携带来自多个通道的帧

在下一行中,圆角框表示从通道中提取的各个 [排序器批量][g-sequencer-batch]。四个蓝色/紫色/粉色是从通道 A 推导出来的,而其他是从通道 B 推导出来的。这些批量在这里按从批量中解码的顺序表示(在本例中,B 首先被解码)。

注意 此处的标题说“首先看到通道 B,并将首先解码为批量”,但这不是一个要求。例如,实现同样可以接受窥视通道并首先解码包含最旧批量的通道。

图的其余部分在概念上与第一部分不同,并说明了在重新排序通道后 L2 链的推导。

第一行显示了批量器交易。请注意,在这种情况下,存在批量的排序,使通道内的所有帧看起来都是连续的。但这通常不是真的。例如,在第二笔交易中,A1B0 的位置可以颠倒,结果完全相同 — 无需更改图中的其余部分。

第二行显示了按正确顺序重建的通道。第三行显示了从通道中提取的批量。因为通道是有序的,并且通道中的批量是按顺序排列的,这意味着批量也是有序的。第四行显示了从每个批量推导出的 [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 交互时传输的 headBlockHashsafeBlockHashfinalizedBlockHash 值。

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

其中 uint32uint16 都是大端无符号整数。类型名称应根据 [Solidity ABI] 进行解释和编码。

帧中的所有数据都是固定大小的,除了 frame_data。固定开销为 16 + 2 + 4 + 1 = 23 字节。 固定大小的帧元数据避免了与目标总数据长度的循环依赖, 从而简化了具有不同内容长度的帧的打包。

其中:

  • channel_id 是通道的不透明标识符。不应重复使用,建议使用随机标识符;但是,在超时规则之外,不会检查其有效性
  • frame_number 标识帧在通道中的索引
  • frame_data_lengthframe_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_encodingrlp_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) &lt;= 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] 表示包含项目 xyz 的列表
  • parent_hash 是上一个 L2 区块的区块哈希
  • epoch_numberepoch_hash 是与 L2 区块的 [排序周期][g-sequencing-epoch] 对应的 L1 区块的编号和哈希
  • timestamp 是 L2 区块的时间戳
  • transaction_listEIP-2718 编码交易的 RLP 编码列表。

未知版本以及格式错误的内容会使批量无效(rollup 节点必须忽略它)。

epoch_numbertimestamp 还必须遵守 批量队列 部分中列出的约束,否则该批量将被视为无效并将被忽略。


架构

以上主要描述了 L2 链推导中使用的常规编码, 主要是批量如何在 [批量器交易][g-batcher-transaction] 中编码。

本节介绍如何使用管道架构从 L1 批量生成 L2 链。

验证器可能会以不同的方式实现此操作,但必须在语义上等效,才能不偏离 L2 链。

L2 Chain Derivation Pipeline(L2 链推导管线)

我们的架构将推导过程分解为由以下阶段组成的管道:

  1. L1 Traversal(L1 遍历)
  2. L1 Retrieval(L1 检索)
  3. Frame Queue(帧队列)
  4. Channel Bank(通道库)
  5. Channel Reader (Batch Decoding)(通道读取器(批量解码))
  6. Batch Queue(批量队列)
  7. Payload Attributes Derivation(Payload 属性推导)
  8. 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 &lt;= 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_numberbatch 首次 完全 派生时的 L1 区块号, 即由前一阶段解码和输出。
  • next_timestamp = safe_l2_head.timestamp + block_time 是下一个批次应具有的预期 L2 时间戳, 请参见 [区块时间信息][g-block-time]。
  • next_epoch 可能尚未知,但如果可用,它将是 epoch 之后的 L1 区块。
  • batch_originepochnext_epoch,具体取决于验证。

请注意,可以推迟批次的处理,直到 batch.timestamp &lt;= next_timestamp, 因为无论如何都必须保留 future 批次。

规则,按验证顺序:

  • batch.timestamp > next_timestamp -> future:即批次必须准备好处理。
  • batch.timestamp &lt; next_timestamp -> drop:即批次不能太旧。
  • batch.parent_hash != safe_l2_head.hash -> drop:即父哈希必须等于 L2 安全头区块哈希。
  • batch.epoch_num + sequence_window_size &lt; inclusion_block_number -> drop:即批次必须及时包含。
  • batch.epoch_num &lt; 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 &lt; 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.hash
  • timestamp = next_timestamp
  • transactions 为空,即没有 sequencer 交易。可以在下一阶段添加已存款交易。
  • 如果 next_timestamp &lt; next_epoch.time:则重复当前的 L1 来源,以保持 L2 时间不变性。
    • epoch_num = epoch.number
    • epoch_hash = epoch.hash
  • 如果批次是 epoch 的第一个批次,则使用该 epoch 而不是提前 epoch,以确保 每个 epoch 至少有一个 L2 区块。
    • epoch_num = epoch.number
    • epoch_hash = epoch.hash
  • 否则,
    • epoch_num = next_epoch.number
    • epoch_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_hash
  • timestamp
  • randao
  • fee_recipient
  • transactions_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,以使新有效载荷成为规范的有效载荷, 现在 safeunsafe 字段都更改为引用有效载荷,并且没有有效载荷属性。

引擎 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 起始点。 此后,其他阶段将彼此重置。

查找同步起始点

要查找起始点,相对于链的回溯方向,有几个步骤:

  1. 找到当前的 L2 forkchoice 状态
    • 如果找不到 finalized 区块,则从 Bedrock genesis 区块开始。
    • 如果找不到 safe 区块,则回退到 finalized 区块。
    • unsafe 区块应始终可用并与上述一致 (在罕见的引擎损坏恢复情况下可能并非如此,正在审查中)。
  2. 找到第一个具有合理的 L1 参考的 L2 区块,作为新的 unsafe 起始点, 从先前的 unsafe 返回到 finalized,不再进一步。
    • 合理的条件是:L2 区块的 L1 源已知且规范,或者未知且具有领先于 L1 的块号。
  3. 找到第一个具有比排序窗口更旧的 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 起始点。
  4. finalized L2 区块作为 finalized 起始点持续存在。
  5. 找到第一个具有比通道超时旧的 L1 参考的 L2 区块
    • 此区块引用的 L1 源(我们称之为 l2base)将成为 L2 管道派生的 base: 通过从这里开始,各个阶段可以缓冲任何必要的数据,同时删除不完整的派生输出,直到 L1 遍历赶上实际的 L2 安全头。

在回溯 L2 链时,实现可以健全性检查,即与现有的 forkchoice 状态相比,起始点从未设置得太远, 以避免由于错误配置而导致的密集重组。

实施者请注意:步骤 1-4 称为 FindL2Heads。步骤 5 当前是引擎队列重置的一部分。 这可能会更改为将起始点搜索与裸重置逻辑隔离。

重置派生阶段
  1. L1 遍历:从 L1 base 开始,作为下一个阶段要拉取的第一个区块。
  2. L1 检索:清空先前的数据,并获取 base L1 数据,或者将获取工作推迟到以后的管道步骤。
  3. 帧队列:清空队列。
  4. 通道库:清空通道库。
  5. 通道读取器:重置任何批次解码状态。
  6. 批次队列:清空批次队列,使用 base 作为初始 L1 参考点。
  7. 有效载荷属性派生:清空任何批次/属性状态。
  8. 引擎队列:
    • 使用同步起始点状态初始化 L2 forkchoice 状态。(finalized/safe/unsafe
    • 将该阶段的 L1 参考点初始化为 base
    • 需要 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 对象的扩展版本表示, 其中包括其他 transactionsnoTxPool 字段。

此过程发生在验证器节点运行的有效载荷属性队列中,以及 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_randao L1 区块属性。
  • suggestedFeeRecipient 设置为 Sequencer Fee Vault 地址。参见 Fee Vaults 规范。
  • transactions 是派生的交易数组:已存款交易和排序的交易,所有交易都使用 EIP-2718 进行编码。
  • noTxPool 设置为 true,以在构造块时使用上述确切的 transactions 列表。
  • gasLimit 设置为此有效载荷的[系统配置][g-system-config]中的当前 gasLimit 值。
  • 原文链接: github.com/ethereum-opti...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论