本文档是L2链推导规范,描述了如何从L1数据中推导出L2区块,包括批量提交、架构设计以及有效负载属性推导等关键步骤。主要内容包括:从L1读取数据、构建通道、解码批次、重新排序交易,最终形成L2区块。文档还涵盖了L1重组的处理以及如何同步L1和L2状态。
<!-- 本文件中所有的词汇表引用。-->
<!-- 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 链推导 — 从 L1 数据推导 L2 区块 — 是 rollup 节点 的主要职责之一,无论是在验证器模式下还是在排序器模式下(其中推导充当排序的完整性检查,并能检测 L1 链重组)。
L2 链是从 L1 链推导出来的。特别是,每个 L1 区块都映射到一个 L2 排序周期,该周期包含多个 L2 区块。周期数定义为等于相应的 L1 区块数。
为了推导周期 E 中的 L2 区块,我们需要以下输入:
E 的 L1 排序窗口:范围为 [E, E + SWS) 的 L1 区块,其中 SWS 是排序窗口大小(请注意,这意味着周期是重叠的)。特别是,我们需要:
E - 1 的最后一个 L2 区块之后的 L2 链状态,或者 — 如果周期 E - 1 不存在 — L2 创世状态。
E <= L2CI,则周期 E 不存在,其中 L2CI 是 L2 链初始状态。要从头开始推导整个 L2 链,我们只需从 L2 创世状态 开始,并将 L2 链初始状态 作为第一个周期,然后按顺序处理所有排序窗口。有关我们如何在实践中实现这一点的更多信息,请参阅 架构部分。 L2 链可能包含 Bedrock 之前的历史记录,但此处的 L2 创世指的是第一个 Bedrock L2 区块。
每个周期可能包含可变数量的 L2 区块(每 l2_block_time 一个,在 Optimism 上为 2 秒),由 排序器 决定,但每个区块都受以下约束:
min_l2_timestamp <= block.timestamp <= max_l2_timestamp,其中
min_l2_timestamp = l1_timestampblock.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 秒的固定 区块时间(尽管某些槽位可以跳过)。因此,预计使用 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 将继续执行。
有关更多详细信息,请参阅 [批量队列]。
在实践中,通常没有必要等待完整的 L1 区块排序窗口,才能开始推导周期中的 L2 区块。实际上,只要我们能够重建连续批量,我们就可以开始推导相应的 L2 区块。我们称之为积极区块推导。
但是,在最坏的情况下,我们只能通过读取排序窗口的最后一个 L1 区块来重建周期中第一个 L2 区块的批量。当该批量的一些数据包含在窗口的最后一个 L1 区块中时,就会发生这种情况。在这种情况下,我们不仅无法推导出周期中的第一个 L2 区块,而且在此之前我们也无法推导出该周期中的任何其他 L2 区块,因为它们需要应用该周期的第一个 L2 区块后产生的状态。 (请注意,这仅适用于区块推导。批量仍然可以被推导并尝试排队,我们只是无法从中创建区块。)
排序器 接受来自用户的 L2 交易。它负责根据这些交易构建区块。对于每个这样的区块,它还会创建一个相应的 排序器批量。它还负责将每个批量提交给 数据可用性提供商(例如,以太坊 calldata),它通过其 批量器 组件来完成。
L2 区块和批量之间的区别是微妙但重要的:区块包括 L2 状态根,而批量仅在给定的 L2 时间戳(相当于:L2 区块号)提交到交易。区块还包括对前一个区块的引用 (*)。
(*) 这在某些极端情况下很重要,在这些情况下,会发生 L1 重组并且批量将重新发布到 L1 链,但不会发布到前面的批量,而 L2 区块的前身不可能改变。
这意味着即使排序器错误地应用了状态转换,批量中的交易仍将被视为规范 L2 链的一部分。批量仍然需要经过有效性检查(即,必须正确编码),批量中的各个交易也是如此(例如,签名必须有效)。无效批量和有效批量中的无效单个交易会被正确的节点丢弃。
如果排序器错误地应用了状态转换并发布了一个 输出根,那么这个输出根将是不正确的。不正确的输出根将受到 错误证明 的质疑,然后被现有排序器批量的正确输出根替换。
有关更多信息,请参阅 批量提交规范。
批量提交与 L2 链推导密切相关,因为推导过程必须解码已编码用于批量提交目的的批量。
批量器 将 批量器交易 提交给 数据可用性提供商。这些交易包含一个或多个 通道帧,这些帧是属于 通道 的数据块。
通道 是 排序器批量(对于任何 L2 区块)压缩在一起的序列。将多个批量分组在一起的原因仅仅是为了获得更好的压缩率,从而降低数据可用性成本。
通道可能太大而无法容纳在单个 批量器交易 中,因此我们需要将其拆分为称为 通道帧 的块。单个批量器交易还可以携带多个帧(属于相同或不同的通道)。
此设计使我们能够以最大的灵活性将批量聚合到通道中,并将通道拆分到批量器交易中。值得注意的是,它使我们能够最大程度地利用批量器交易中的数据使用率:例如,它使我们能够将窗口的最后一个(小)帧与来自下一个窗口的大帧打包在一起。
将来,此通道识别功能还允许 批量器 采用多个签名者(私钥)来并行提交一个或多个通道 (1)。
(1) 这有助于缓解以下问题:由于影响 L2 tx-pool 并因此影响包含的交易 nonce 值:同一签名者进行的多个交易被卡住等待先前交易的包含。
另请注意,我们使用流式压缩方案,当我们启动通道时,我们不需要知道通道最终将包含多少个区块,甚至在通道中发送第一个帧时也不知道。
通过跨多个数据交易拆分通道,L2 可以具有比数据可用性层可能支持的更大的区块数据。
所有这些都在下图中说明。说明如下。
第一行表示 L1 区块及其编号。L1 区块下方的框表示包含在区块中的 批量器交易。L1 区块下方的波浪线表示 存款(更具体地说是,由 存款合约 发出的事件)。
框中的每个彩色块表示一个 通道帧。因此 A 和 B 是 通道,而 A0、A1、B0、B1、B2 是帧。请注意:
在下一行中,圆角框表示从通道中提取的各个 排序器批量。四个蓝色/紫色/粉色是从通道 A 推导出来的,而其他是从通道 B 推导出来的。这些批量在这里按从批量中解码的顺序表示(在本例中,B 首先被解码)。
注意 此处的标题说“首先看到通道 B,并将首先解码为批量”,但这不是一个要求。例如,实现同样可以接受窥视通道并首先解码包含最旧批量的通道。
图的其余部分在概念上与第一部分不同,并说明了在重新排序通道后 L2 链的推导。
第一行显示了批量器交易。请注意,在这种情况下,存在批量的排序,使通道内的所有帧看起来都是连续的。但这通常不是真的。例如,在第二笔交易中,A1 和 B0 的位置可以颠倒,结果完全相同 — 无需更改图中的其余部分。
第二行显示了按正确顺序重建的通道。第三行显示了从通道中提取的批量。因为通道是有序的,并且通道中的批量是按顺序排列的,这意味着批量也是有序的。第四行显示了从每个批量推导出的 L2 区块。请注意,这里我们有一个 1-1 的批量到区块的映射,但正如我们稍后将看到的,在 L1 上发布的批量中存在“差距”的情况下,可以插入不映射到批量的空区块。
第五行显示了 L1 属性存款交易,它在每个 L2 区块中记录有关与 L2 区块周期匹配的 L1 区块的信息。第一个数字表示周期/L1x 数字,而第二个数字(“序列号”)表示周期内的位置。
最后,第六行显示了从前面提到的 存款合约 事件中推导出的 用户存款交易。
请注意图右下角的 101-0 L1 属性交易。只有在以下情况下才有可能在那里存在:帧 B2 指示它是通道中的最后一个帧,并且 (2) 不能插入空区块。
该图未指定正在使用的排序窗口大小,但由此我们可以推断出它必须至少为 4 个区块,因为通道 A 的最后一个帧出现在区块 102 中,但属于周期 99。
至于关于“安全类型”的注释,它解释了在 L1 和 L2 上使用的区块分类。
这些安全级别映射到与 执行引擎 API 交互时传输的 headBlockHash、safeBlockHash 和 finalizedBlockHash 值。
批量器交易被编码为 version_byte ++ rollup_payload(其中 ++ 表示连接)。
version_byte |
rollup_payload |
|---|---|
| 0 | frame ... (一个或多个帧,连接) |
未知版本使批量器交易无效(rollup 节点必须忽略它)。 批量器交易中的所有帧都必须可解析。如果任何一个帧无法解析,则会拒绝交易中的所有帧。
批量交易通过验证交易的 to 地址是否与批量收件箱地址匹配,以及 from 地址是否与 系统配置 中数据读取的 L1 区块的批量发送者地址匹配来验证。
通道帧 被编码为:
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_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 编码的批量执行流式压缩和解压缩。这意味着我们有可能在知道通道将包含多少个批量(以及多少帧)之前,就开始在 批量器交易 中包含通道帧。
回想一下,批量包含要包含在特定 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 区块的 排序周期 对应的 L1 区块的编号和哈希timestamp 是 L2 区块的时间戳transaction_list 是 EIP-2718 编码交易的 RLP 编码列表。未知版本以及格式错误的内容会使批量无效(rollup 节点必须忽略它)。
epoch_number 和 timestamp 还必须遵守 批量队列 部分中列出的约束,否则该批量将被视为无效并将被忽略。
以上主要描述了 L2 链推导中使用的常规编码, 主要是批量如何在 批量器交易 中编码。
本节介绍如何使用管道架构从 L1 批量生成 L2 链。
验证器可能会以不同的方式实现此操作,但必须在语义上等效,才能不偏离 L2 链。
我们的架构将推导过程分解为由以下阶段组成的管道:
数据从管道的开始(外部)流向结束(内部)。 从最内层阶段拉取最外层阶段的数据。
但是,数据是按相反的顺序处理的。这意味着如果最后一个阶段中有任何要处理的数据,它将首先被处理。处理按每个阶段都可以采取的“步骤”进行。在内部阶段中处理数据之前,我们尝试在最后一个(最内部)阶段中尽可能多地执行步骤,依此类推。
这确保了我们在提取更多数据之前使用我们已经拥有的数据,并最大限度地减少数据遍历推导管道的延迟。
每个阶段都可以根据需要维护其自己的内部状态。特别是,每个阶段都维护一个 L1 区块引用(编号 + 哈希),该引用指向最新的 L1 区块,以便完全处理来自先前区块的所有数据,并且正在处理或已处理来自该区块的数据。这使得最内层阶段能够考虑到用于生成 L2 链的 L1 数据可用性的最终确定,从而在 L2 链输入变得不可逆时反映在 L2 链分叉选择中。
让我们简要描述一下管道的每个阶段。
在L1 遍历阶段,我们只需读取下一个 L1 区块的标头。在正常操作中,这些将是创建时的新的 L1 区块,尽管我们也可以在同步时或在 L1 重组 的情况下读取旧区块。
在遍历 L1 区块后,会更新 L1 检索阶段使用的 系统配置 副本,以便批量发送者身份验证始终准确地对应于该阶段读取数据的 L1 区块。
在L1 检索阶段,我们读取从外部阶段(L1 遍历)获取的区块,并从中提取数据。 默认情况下,rollup 在从区块中的 批量器交易 检索的 calldata 上运行,对于每个交易:
每个数据交易都经过版本控制,并包含一系列 通道帧,以供帧队列读取,请参阅 批量提交线路格式。
帧队列一次缓冲一个数据交易, 解码为 通道帧,以供下一阶段使用。 请参阅 批量器交易格式 和 帧格式 规范。
通道库阶段负责管理从 L1 检索阶段写入的通道库的缓冲。通道库阶段中的一个步骤尝试从“就绪”的通道读取数据。
当前通道已完全缓冲,直到被读取或删除, 未来的 ChannelBank 版本可能会支持流式通道。
为了限制资源使用,通道库会根据通道大小进行剪枝,并使旧通道超时。
通道按 FIFO 顺序记录在称为通道队列的结构中。通道第一次被看到属于该通道的帧时,会被添加到通道队列中。
成功插入新帧后,通道库会被剪枝:
通道按 FIFO 顺序删除,直到 total_size <= MAX_CHANNEL_BANK_SIZE,其中:
total_size 是每个通道大小的总和,该大小是通道的所有缓冲帧数据的总和,
每个帧还有额外的 200 字节的帧开销。MAX_CHANNEL_BANK_SIZE 是一个协议常量,为 100,000,000 字节。通道打开时所在的 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 区块数表示。超时通道的新帧将被删除,而不是缓冲。
通道库只能输出从第一个打开的通道获取的数据。
读取时,如果第一个打开的通道超时,则将其从通道库中删除。
一旦第一个打开的通道(如果有)未超时并且已准备就绪,则可以读取它并将其从通道库中删除。
如果满足以下条件,则通道已准备就绪:
如果没有通道准备就绪,则读取下一个帧并将其提取到通道库中。
当帧引用的通道 ID 尚未出现在通道库中时, 会打开一个新通道,标记为当前 L1 区块,并附加到通道队列中。
帧插入条件:
is_last == 1,但通道已经看到一个关闭帧,并且尚未从通道库中删除)将被丢弃。如果一个帧正在关闭(is_last == 1),则任何现有的编号较高的帧都将从通道中删除。
请注意,虽然这允许通道 ID 在从通道库中删除后被重用,但建议 batcher 的实现使用唯一的通道 ID。
在这个阶段,我们解压缩从上一个阶段拉取的通道,然后从解压缩的字节流中解析批次。
有关解压缩和解码规范,请参见批次格式。
在 批次缓冲 阶段,我们按时间戳重新排序批次。如果某些时间槽缺少批次,并且存在具有更高时间戳的有效批次, 则此阶段还会生成空批次以填补空白。
只要有一个紧随当前安全 L2 头(可以从规范 L1 链派生的最后一个区块)的时间戳的连续批次, 批次就会被推送到下一阶段。批次的父哈希也必须与当前安全 L2 头的哈希匹配。
请注意,从 L1 派生的批次中存在任何空白,这意味着此阶段需要缓冲整个排序窗口, 然后才能生成空批次(因为在最坏的情况下,缺失的批次可能在该窗口的最后一个 L1 区块中包含数据)。
一个批次可以有 4 种不同的有效性形式:
drop:批次无效,并且始终在未来,除非我们进行重组(reorg)。它可以从缓冲区中删除。accept:批次有效,应该被处理。undecided:我们缺乏 L1 信息,直到我们可以继续进行批次过滤。future:批次可能有效,但尚未处理,应该稍后再次检查。批次按照包含在 L1 上的顺序进行处理:如果可以 accept-ed 多个批次,则应用第一个。
实现可以推迟 future 批次到稍后的派生步骤,以减少验证工作。
批次的有效性派生如下:
定义:
batch 如 批次格式部分 中定义。epoch = safe_l2_head.l1_origin 是一个与批次相关的L1 来源,具有以下属性:
number(L1 区块号),hash(L1 区块哈希)和 timestamp(L1 区块时间戳)。inclusion_block_number 是 batch 首次 完全 派生时的 L1 区块号,
即由前一阶段解码和输出。next_timestamp = safe_l2_head.timestamp + block_time 是下一个批次应具有的预期 L2 时间戳,
请参见 区块时间信息。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_epochbatch.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:
如果无法 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.hashepoch_num = epoch.numberepoch_hash = epoch.hashepoch_num = next_epoch.numberepoch_hash = next_epoch.hash在 有效载荷属性派生 阶段,我们会将从上一阶段获得的批次转换为
PayloadAttributes 结构的实例。这样的结构编码了需要包含在区块中的交易,
以及其他区块输入(时间戳,费用接收者等)。有效载荷属性派生的详细信息在下面的
派生有效载荷属性部分中。
此阶段维护其自己的系统配置副本,独立于 L1 检索阶段。每当批次输入引用的 L1 epoch 更改时,系统配置都会使用 L1 日志事件进行更新。
在 引擎队列 阶段,先前派生的 PayloadAttributes 结构被缓冲并发送到
执行引擎,以执行并转换为正确的 L2 区块。
该阶段维护对三个 L2 区块的引用:
此外,它还会缓冲最近处理的安全 L2 区块的引用的简短历史记录,以及每个区块的派生来源的 L1 区块的引用。 此历史记录不必完整,但可以将以后的 L1 终结性信号转换为 L2 终结性。
为了与引擎交互,使用了执行引擎 API,其中包含以下 JSON-RPC 方法:
engine_forkchoiceUpdatedV1 — 如果不同,则将 forkchoice(即链头)更新为 headBlockHash,
如果有效载荷属性参数不为 null,则指示引擎开始构建执行有效载荷。engine_getPayloadV1 — 检索先前请求的执行有效载荷构建。engine_newPayloadV1 — 执行执行有效载荷以创建区块。执行有效载荷是 ExecutionPayloadV1 类型的对象。
如果有任何要应用的 forkchoice 更新,在派生或处理其他输入之前,这些更新将首先应用于引擎。
以下情况下可能会发生此同步:
新的 forkchoice 状态将使用 engine_forkchoiceUpdatedV1 应用。
在 forkchoice 状态有效性错误时,必须重置派生管道以恢复到一致状态。
如果不安全头领先于安全头,则会尝试整合,验证现有的不安全 L2 链是否与从规范 L1 数据派生的 L2 输入匹配。
在整合期间,我们会考虑最旧的不安全 L2 区块,即紧随安全头之后的不安全 L2 区块。如果有效载荷属性与此最旧的不安全 L2 区块匹配,则可以将该区块视为“安全”,并成为新的安全头。
派生的 L2 有效载荷属性的以下字段将检查与 L2 区块的相等性:
parent_hashtimestamprandaofee_recipienttransactions_list(首先是长度,然后是每个编码交易的相等性,包括存款)如果整合成功,forkchoice 变更将按照上述部分中的描述进行同步。
如果整合失败,L2 有效载荷属性将立即处理,如下所述。 选择有效载荷属性是为了支持先前的安全 L2 区块,从而在当前安全区块之上创建 L2 链重组。立即处理新的替代属性使 go-ethereum 等执行引擎能够实施该变更,因为可能不支持对链的顶端进行线性回滚。
如果安全和不安全 L2 头相同(无论是由于整合失败与否),我们会将 L2 有效载荷属性发送到执行引擎,以构建为正确的 L2 区块。然后,此 L2 区块将成为新的 L2 安全头和不安全头。
如果由于验证错误(即该区块中存在无效交易或状态转换),无法将从批次创建的有效载荷属性插入到链中,则应删除该批次,并且不应提前安全头。引擎队列将尝试使用批次队列中的下一个批次用于该时间戳。如果找不到有效的批次,则汇总节点将创建一个仅存款批次,该批次应始终通过验证,因为存款始终有效。
通过执行引擎 API 与执行引擎的交互在与执行引擎的通信部分中进行了详细说明。
然后,使用以下顺序处理有效载荷属性:
engine_forkchoiceUpdatedV1,其中包含该阶段的当前 forkchoice 状态,以及用于启动区块构建的属性。
engine_getPayload,以通过上一步结果中的有效载荷 ID 检索有效载荷。engine_newPayload,以将新有效载荷导入到执行引擎中。engine_forkchoiceUpdatedV1,以使新有效载荷成为规范的有效载荷,
现在 safe 和 unsafe 字段都更改为引用有效载荷,并且没有有效载荷属性。引擎 API 错误处理:
如果没有 forkchoice 更新或 L1 数据要处理,并且如果下一个可能的 L2 区块已通过不安全的来源(例如 sequencer 通过 p2p 网络发布它)可用, 则会乐观地将其处理为“不安全”区块。在理想情况下,这会将以后的派生工作减少到仅与 L1 进行整合,并使 用户能够比 L1 确认 L2 数据包更快地看到 L2 链的头。
要处理不安全的有效负载,有效负载必须:
然后,按以下顺序处理有效负载:
engine_newPayloadV1:处理有效负载。它尚未成为规范。engine_forkchoiceUpdatedV1:使有效负载成为规范的不安全 L2 块,并保留安全/已完成的 L2 块。引擎 API 错误处理:
可以重置管道,例如,如果我们检测到 L1 重组(reorganization)。 这使汇总节点能够处理 L1 链重组事件。
重置会将管道恢复为一种状态,该状态产生的输出与完整的 L2 派生过程相同, 而是从现有的 L2 链开始,该链的回溯程度足以与当前的 L1 链协调一致。
请注意,此算法涵盖了几个重要的用例:
处理这些情况也意味着可以将节点配置为急切地同步具有 0 个确认的 L1 数据, 因为如果 L1 以后确实将数据识别为规范,则它可以撤消更改,从而实现安全的低延迟使用。
首先重置引擎队列,以确定要从其中继续派生的 L1 和 L2 起始点。 此后,其他阶段将彼此重置。
要查找起始点,相对于链的回溯方向,有几个步骤:
finalized 区块,则从 Bedrock genesis 区块开始。safe 区块,则回退到 finalized 区块。unsafe 区块应始终可用并与上述一致
(在罕见的引擎损坏恢复情况下可能并非如此,正在审查中)。unsafe 起始点,
从先前的 unsafe 返回到 finalized,不再进一步。
safe 起始点,
从上面的合理的 unsafe 头返回到 finalized,不再进一步。
unsafe 头将修订为当前的父级。highest。0 表示 L1 源更改,如果不更改则递增 1)n 的 L1 源比 highest 的 L1 源大一个序列窗口,
并且 n.sequence_number == 0,则 n 的父 L2 区块将是 safe 起始点。finalized L2 区块作为 finalized 起始点持续存在。l2base)将成为 L2 管道派生的 base:
通过从这里开始,各个阶段可以缓冲任何必要的数据,同时删除不完整的派生输出,直到
L1 遍历赶上实际的 L2 安全头。在回溯 L2 链时,实现可以健全性检查,即与现有的 forkchoice 状态相比,起始点从未设置得太远, 以避免由于错误配置而导致的密集重组。
实施者请注意:步骤 1-4 称为 FindL2Heads。步骤 5 当前是引擎队列重置的一部分。
这可能会更改为将起始点搜索与裸重置逻辑隔离。
base 开始,作为下一个阶段要拉取的第一个区块。base L1 数据,或者将获取工作推迟到以后的管道步骤。base 作为初始 L1 参考点。finalized/safe/unsafe)base。在必要时,从 base 开始的阶段可以从 l2base 区块中编码的数据初始化其系统配置。
请注意,在 merge 后,重组的深度将受到 L1 终结延迟 的限制 (2 个 L1 信标 epoch,或大约 13 分钟,除非超过 1/3 的网络持续不同意)。 新的 L1 区块可以每 L1 信标 epoch(大约 6.4 分钟)完成,并且根据这些 终结性信号和批次包含,派生的 L2 链也将变为不可逆的。
请注意,这种形式的终结仅影响输入,并且节点可以主观地说链是不可逆的, 通过从这些不可逆的输入以及设置的协议规则和参数中重现链。
但这与发布在 L1 上的输出完全无关,发布在 L1 上的输出需要一种证明形式,例如故障证明或 zk 证明才能完成。乐观rollup输出(例如 L1 上的提款)仅在经过一周未经争议(故障证明挑战窗口)后被标记为“已完成”, 这与权益证明完成发生名称冲突。
对于从 L1 数据派生的每个 L2 区块,我们需要构建有效载荷属性,
由 PayloadAttributesV1 对象的扩展版本表示,
其中包括其他 transactions 和 noTxPool 字段。
此过程发生在验证器节点运行的有效载荷属性队列中,以及 sequencer 节点运行的区块生产过程中(如果批量提交交易,则 sequencer 可以启用 tx-pool 使用)。
对于要由 sequencer 创建的每个 L2 区块,我们从与 目标 L2 区块号匹配的Sequencer 批次开始。如果 L1 链未包含目标 L2 区块号的批次,则这可能是空的自动生成的批次。 记住,该批次包括排序 epoch号、L2 时间戳和交易列表。
此区块是排序 epoch的一部分, 其数字与 L1 区块的数字匹配(其 L1 origin)。 此 L1 区块用于派生 L1 属性以及(对于 epoch 中的第一个 L2 区块)用户存款。
因此,PayloadAttributesV1 对象必须包括以下交易:
交易 必须 按此顺序出现在有效载荷属性中。
L1 属性从 L1 区块头读取,而存款从 L1 区块的收据中读取。 有关如何将存款编码为日志条目的详细信息,请参阅 存款合约规范。
在派生出交易列表后,汇总节点按如下方式构造 PayloadAttributesV1:
timestamp 设置为批次的时间戳。random 设置为 prev_randao L1 区块属性。suggestedFeeRecipient 设置为 Sequencer Fee Vault 地址。参见 Fee Vaults 规范。transactions 是派生的交易数组:已存款交易和排序的交易,所有交易都使用 EIP-2718 进行编码。noTxPool 设置为 true,以在构造块时使用上述确切的 transactions 列表。gasLimit 设置为此有效载荷的系统配置中的当前 gasLimit 值。
- 原文链接: github.com/ethereum-opti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!