本文档详细阐述了如何从L1数据中推导出L2链,这是rollup节点的核心任务之一。内容涵盖批量提交的各个方面,包括排序、批处理、线格式、架构、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 -->
注意:以下假设只有一个排序器和 batcher。未来,该设计将被修改以适应多个此类实体。
L2 链推导 — 从 L1 数据推导 L2 区块 — 是 rollup 节点 的主要职责之一,无论是在验证器模式下,还是在排序器模式下(其中推导充当排序的完整性检查,并能够检测 L1 链 re-organizations)。
L2 链是从 L1 链推导出来的。特别是,每个 L1 区块被映射到一个包含多个 L2 区块的 L2 排序 epoch。epoch 编号被定义为等于相应的 L1 区块编号。
为了推导 epoch E
中的 L2 区块,我们需要以下输入:
E
的 L1 排序窗口:范围 [E, E + SWS)
中的 L1 区块,其中 SWS
是排序窗口大小(请注意,这意味着 epoch 是重叠的)。特别是,我们需要:
E
上拥有一个包含与 epoch E
相关的批次的 batcher 事务,因为该批次必须包含 L1 区块 E
的哈希值。E
中进行的 deposits(以 deposit contract 发出的事件的形式)。E
的 L1 区块属性(以推导 L1 attributes deposited transaction)。E - 1
的最后一个 L2 区块之后的 L2 链的状态,或者 — 如果 epoch E - 1
不存在 — L2 genesis state。
E <= L2CI
(其中 L2CI
是 L2 chain inception),则 epoch E
不存在。为了从头开始推导整个 L2 链,我们只需从 L2 genesis state 开始,并将 L2 chain inception 作为第一个 epoch,然后按顺序处理所有排序窗口。有关我们如何在实践中实现此操作的更多信息,请参阅 Architecture section。 L2 链可能包含 pre-Bedrock 历史,但此处的 L2 genesis 指的是第一个 Bedrock L2 区块。
每个 epoch 可能包含可变数量的 L2 区块(每 l2_block_time
一个,Optimism 上为 2 秒),由 sequencer 决定,但每个区块须遵守以下约束:
min_l2_timestamp <= block.timestamp <= max_l2_timestamp
,其中
min_l2_timestamp = l1_timestamp
block.timestamp = prev_l2_timestamp + l2_block_time
prev_l2_timestamp
是前一个 epoch 的最后一个 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 区块的 epoch 关联的 L1 区块的时间戳max_sequencer_drift
是 sequencer 允许领先于 L1 的最大值总而言之,这些约束意味着必须每 l2_block_time
秒有一个 L2 区块,并且 epoch 的第一个 L2 区块的时间戳绝不能落后于与该 epoch 匹配的 L1 区块的时间戳。
合并后,以太坊的固定 block time 为 12 秒(尽管可以跳过某些Slot)。因此,预计对于 2 秒的 L2 区块时间,大部分时间里,每个 epoch 将包含 12/2 = 6
个 L2 区块。
但是,sequencer 可以延长或缩短 epoch(受上述约束的约束)。
这样做的理由是为了在 L1 上跳过Slot或与 L1 的连接暂时丢失的情况下保持活跃性 — 这需要更长的 epoch。
然后需要较短的 epoch,以避免 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
继续强制执行。
请参阅 [Batch Queue] 了解更多详细信息。
实际上,通常没有必要等待完整的 L1 区块排序窗口才能开始推导 epoch 中的 L2 区块。实际上,只要我们能够重建顺序批次,我们就可以开始推导相应的 L2 区块。我们将此称为积极区块推导。
但是,在最坏的情况下,我们只能通过读取排序窗口的最后一个 L1 区块来重建 epoch 中第一个 L2 区块的批次。当该批次的某些数据包含在窗口的最后一个 L1 区块中时,就会发生这种情况。在这种情况下,我们不仅无法推导 epoch 中的第一个 L2 区块,而且在那之前也无法推导 epoch 中的任何其他 L2 区块,因为它们需要应用 epoch 的第一个 L2 区块后产生的状态。 (请注意,这仅适用于区块推导。批次仍然可以被推导并暂时排队,我们只是无法从中创建区块。)
Sequencer 接受来自用户的 L2 交易。它负责构建由这些交易组成的区块。对于每个这样的区块,它还会创建一个相应的 sequencer batch。它还负责将每个批次提交给 data availability provider(例如,以太坊 calldata),这是通过它的 batcher 组件完成的。
L2 区块和批次之间的区别是微妙但重要的:区块包括 L2 状态根,而批次仅提交到给定 L2 时间戳(等效地:L2 区块号)的交易。区块还包括对前一个区块的引用 (*)。
(*) 这在某些极端情况下很重要,在这些情况下,可能会发生 L1 重组,并且批次将被重新发布到 L1 链,但不会重新发布到前面的批次,而 L2 区块的前置区块不可能更改。
这意味着即使 sequencer 错误地应用了状态转换,该批次中的交易仍然将被视为规范 L2 链的一部分。批次仍然受有效性检查的约束(即,它们必须被正确编码),并且批次内的单个交易也是如此(例如,签名必须有效)。无效的批次和无效的单个交易(在其他方面有效的批次中)会被正确的节点丢弃。
如果 sequencer 错误地应用了状态转换并发布了 output root,则此输出根将不正确。不正确的输出根将受到 fault proof 的质疑,然后被现有 sequencer 批次的正确输出根替换。
有关更多信息,请参阅 Batch Submission specification。
批量提交与 L2 链推导紧密相关,因为推导过程必须解码为批量提交而编码的批次。
Batcher 将 batcher transactions 提交给 data availability provider。这些交易包含一个或多个 channel frames,它们是属于 channel 的数据块。
Channel 是一系列压缩在一起的 sequencer batches(对于任何 L2 区块)。将多个批次分组在一起的原因仅仅是为了获得更好的压缩率,从而降低数据可用性成本。
通道可能太大而无法容纳在单个 batcher transaction 中,因此我们需要将其拆分为称为 channel frames 的块。单个 batcher 交易还可以携带多个帧(属于同一通道或不同通道)。
这种设计使我们在如何将批次聚合到通道中以及如何在 batcher 交易中拆分通道方面具有最大的灵活性。值得注意的是,它使我们能够最大化 batcher 交易中的数据利用率:例如,它使我们能够将窗口的最后一个(小)帧与下一个窗口的大帧打包在一起。
将来,此通道识别功能还允许 batcher 采用多个签名者(私钥)来并行提交一个或多个通道 (1)。
(1) 这有助于缓解以下问题:由于影响 L2 tx-pool 并因此包含的交易 nonce 值:同一签名者进行的多个交易都卡在等待先前交易的包含中。
另请注意,我们使用流压缩方案,并且当我们启动通道时,我们不需要知道通道最终将包含多少个区块,甚至在通道中发送第一个帧时也不知道。
并且通过在多个数据交易中拆分通道,L2 可以具有比数据可用性层支持的更大的区块数据。
所有这些都在下图中进行了说明。解释如下。
第一行表示 L1 区块及其编号。L1 区块下方的框表示包含在区块中的 batcher transactions。L1 区块下方的曲线表示 deposits(更具体地说,是由 deposit contract 发出的事件)。
框中的每个彩色块表示一个 channel frame。因此,A
和 B
是 channels,而 A0
、A1
、B0
、B1
、B2
是帧。请注意:
在下一行中,圆角框表示从通道中提取的各个 sequencer batches。四个蓝色/紫色/粉色来自通道 A
,而其他来自通道 B
。这些批次在此处按从批次解码的顺序表示(在本例中,首先解码 B
)。
注意 此处的标题说“首先看到通道 B,将首先将其解码为批次”,但这不是必需的。例如,对于实现来说,窥视通道并首先解码包含最旧批次的通道同样是可以接受的。
该图的其余部分在概念上与第一部分不同,并且说明了在重新排序通道之后进行的 L2 链推导。
第一行显示 batcher 交易。请注意,在这种情况下,批次存在一种排序,使得通道内的所有帧都连续出现。通常情况并非如此。例如,在第二个交易中,A1
和 B0
的位置可以颠倒,以获得完全相同的结果 — 无需更改图的其余部分。
第二行显示按正确顺序重建的通道。第三行显示从通道中提取的批次。因为通道已排序,并且通道内的批次是顺序的,所以这意味着批次也已排序。第四行显示从每个批次推导出的 L2 block。请注意,我们在此处具有 1-1 的 batch 到 block 映射,但是,正如我们稍后将看到的,在 L1 上发布的批次中存在“间隙”的情况下,可以插入不映射到批次的空区块。
第五行显示 L1 attributes deposited transaction,它在每个 L2 区块中记录有关与 L2 区块的 epoch 匹配的 L1 区块的信息。第一个数字表示 epoch/L1x 编号,而第二个数字(“序列号”)表示 epoch 中的位置。
最后,第六行显示从 deposit contract 事件派生的 user-deposited transactions,该事件先前已提及。
请注意图右下角的 101-0
L1 属性交易。只有在以下情况下,它才有可能出现在那里:(1) 帧 B2
指示它是通道中的最后一个帧,并且 (2) 不得插入空区块。
该图未指定使用的排序窗口大小,但由此我们可以推断出它必须至少为 4 个区块,因为通道 A
的最后一个帧出现在区块 102 中,但属于 epoch 99。
至于对“安全类型”的评论,它解释了 L1 和 L2 上使用的区块分类。
这些安全级别映射到与 execution-engine API 交互时传输的 headBlockHash
、safeBlockHash
和 finalizedBlockHash
值。
Batcher 交易被编码为 version_byte ++ rollup_payload
(其中 ++
表示串联)。
version_byte |
rollup_payload |
---|---|
0 | frame ... (一个或多个帧,串联) |
未知版本会使 batcher 交易无效(rollup 节点必须忽略它)。 batcher 交易中的所有帧都必须可解析。如果任何一个帧无法解析,则拒绝事务中的所有帧。
通过验证交易的 to
地址是否与批处理收件箱地址匹配,以及 from
地址是否与读取交易数据时 system configuration 中的批处理发送方地址匹配来对批处理交易进行身份验证(from
地址对应于读取交易数据时 L1 区块)。
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
都是大端无符号整数。类型名称应根据 the 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 编码的批处理执行流式压缩和解压缩。这意味着我们可以在知道通道将包含多少批处理(以及多少帧)之前,开始在 batcher transaction 中包含通道帧。
回想一下,批处理包含要包含在特定 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 format 编码批处理的函数,[x, y, z]
表示包含项目 x
、y
和 z
的列表parent_hash
是上一个 L2 区块的区块哈希epoch_number
和 epoch_hash
是与 L2 区块的 sequencing epoch 相对应的 L1 区块的编号和哈希timestamp
是 L2 区块的时间戳transaction_list
是 EIP-2718 编码交易的 RLP 编码列表。未知版本会使批处理无效(rollup 节点必须忽略它),格式错误的内容也是如此。
epoch_number
和 timestamp
还必须遵守 Batch Queue 部分中列出的约束,否则该批处理将被视为无效,并将被忽略。
以上主要描述了 L2 链推导中使用的通用编码,主要是在 batcher transactions 中如何编码批处理。
本节介绍如何使用流水线架构从 L1 批处理生成 L2 链。
验证器可以以不同的方式实现此目的,但必须在语义上等效,以免与 L2 链有所不同。
我们的架构将推导过程分解为由以下阶段组成的流水线:
数据从流水线的开头(外部)流向结尾(内部)。 从最内层阶段,数据从最外层阶段拉取。
但是,数据以相反的顺序处理。这意味着如果最后一个阶段有任何要处理的数据,它将首先被处理。处理以可以在每个阶段执行的“步骤”进行。在对其外部阶段执行任何步骤之前,我们尝试在最后一个(最内部)阶段执行尽可能多的步骤,依此类推。
这确保了我们在拉取更多数据之前使用我们已经拥有的数据,并最大程度地减少了数据遍历推导流水线的延迟。
每个阶段都可以根据需要维护自己的内部状态。特别是,每个阶段都维护一个 L1 区块引用(编号 + 哈希)到最新的 L1 区块,这样来自先前区块的所有数据都已完全处理,并且正在处理或已经处理来自该区块的数据。这使最内层阶段能够考虑用于生成 L2 链的 L1 数据可用性的完成情况,以在 L2 链输入变得不可逆时反映在 L2 链 forkchoice 中。
让我们简要描述流水线的每个阶段。
在 L1 遍历 阶段,我们只需读取下一个 L1 区块的标头。在正常操作中,这些将是创建时的新 L1 区块,尽管我们也可以在同步时或在 L1 re-org 的情况下读取旧区块。
在遍历 L1 区块时,会更新 L1 检索阶段使用的 system configuration 副本,这样,批处理发送方认证始终准确地对应于该阶段读取的数据的 L1 区块。
在 L1 检索 阶段,我们读取从外部阶段(L1 遍历)获取的区块,并从中提取数据。 默认情况下,rollup 在从区块中的 batcher transactions 检索的 calldata 上运行,对于每个交易:
每个数据交易都有版本控制,并包含一系列 channel frames,以供 Frame Queue 读取,请参阅 Batch Submission Wire Format。
Frame Queue 一次缓冲一个数据交易,解码为 channel frames,以供下一阶段使用。 请参阅 Batcher transaction format 和 Frame format 规范。
Channel Bank 阶段负责管理从 L1 检索阶段写入的通道库的缓冲。通道库阶段中的一个步骤尝试从“就绪”的通道读取数据。
当前已完全缓冲通道,直到读取或丢弃,流式通道可能在 ChannelBank 的未来版本中支持。
为了限制资源使用,Channel Bank 根据通道大小进行剪枝,并使旧通道超时。
通道以 FIFO 顺序记录在称为 channel queue 的结构中。将帧添加到通道队列是第一次看到属于该通道的帧。
成功插入新的帧后,将对 ChannelBank 进行剪枝:丢弃 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 区块的数量表示。已超时通道的新帧将被丢弃,而不是缓冲。
channel-bank 只能输出来自第一个打开的通道的数据。
读取时,如果第一个打开的通道已超时,则将其从 channel-bank 中删除。
一旦第一个打开的通道(如果有)未超时并且已准备就绪,则将其从 channel-bank 中读取并删除。
通道在以下情况下准备就绪:
如果没有通道准备就绪,则读取下一个帧并将其提取到通道库中。
当帧引用的通道 ID 尚未出现在 Channel Bank 中时,将打开一个新通道,标记为当前 L1 区块,并附加到 channel-queue。
帧插入条件:
is_last == 1
,但通道已经看到关闭帧且尚未从 channel-bank 中剪枝)。如果某个帧正在关闭 (is_last == 1
),则会从通道中删除任何现有编号较高的帧。
请注意,虽然这允许在从 channel-bank 中剪枝后重用通道 ID,但建议批处理程序实现使用唯一的通道 ID。
在此阶段,我们解压缩从上一阶段提取的通道,然后从解压缩的字节流中解析 batches。
有关解压缩和解码规范,请参阅 Batch Format。
在 Batch Buffering(批量缓冲) 阶段,我们按时间戳重新排序批处理。如果某些 time slots 缺少批处理,并且存在具有较高时间戳的有效批处理,则此阶段还会生成空批处理以填充间隙。
每当当前 safe L2 head (可以从规范 L1 链派生的最后一个区块) 的时间戳之后直接存在一个顺序批次时,批次就会被推送到下一阶段。 批次的父哈希也必须与当前安全 L2 head 的哈希匹配。
注意,来自 L1 的批次中存在任何间隙意味着此阶段将需要缓冲整个 sequencing window,然后才能生成空批次(因为在最坏的情况下,缺失的批次可能在窗口的最后一个 L1 区块中包含数据)。
批次可以有 4 种不同的有效性形式:
drop
:批次无效,并且除非我们重新组织,否则将来始终无效。可以从缓冲区中删除。accept
:批次有效,应该处理。undecided
:我们缺乏 L1 信息,直到我们可以继续批处理过滤。future
:批次可能有效,但现在还无法处理,应稍后再次检查。批次按包含在 L1 上的顺序进行处理:如果可以 accept
多个批次,则应用第一个。
实现可以将 future
批次推迟到以后的派生步骤,以减少验证工作。
批次的有效性派生如下:
定义:
batch
如 Batch format section 中定义的。epoch = safe_l2_head.l1_origin
是一个 L1 origin 与批次耦合,具有以下属性:
number
(L1 区块号)、hash
(L1 区块哈希)和 timestamp
(L1 区块时间戳)。inclusion_block_number
是首次完全派生 batch
时的 L1 区块号,
即被前面的阶段解码和输出。`next_timestamp = safel2- ``` batch.timestamp > next_timestamp
-> `future`:即,batch 必须准备好进行处理。
batch.timestamp < next_timestamp
-> drop
:即,batch 不能太旧。
batch.parent_hash != safe_l2_head.hash
-> drop
:即,父哈希必须等于 L2 安全头区块哈希。
batch.epoch_num + sequence_window_size < inclusion_block_number
-> drop
:即,batch 必须及时包含。
batch.epoch_num < epoch.number
-> drop
:即,batch 的来源不能早于 L2 安全头。
batch.epoch_num == epoch.number
:将 batch_origin
定义为 epoch
。
batch.epoch_num == epoch.number+1
:
next_epoch
未知 -> undecided
:
即,在拥有 L1 来源数据之前,无法处理更改 L1 来源的 batch。batch_origin
定义为 next_epoch
batch.epoch_num > epoch.number+1
-> drop
:即,每个 L2 区块的 L1 来源更改不能超过一个 L1 区块。
batch.epoch_hash != batch_origin.hash
-> drop
:即,batch 必须引用规范的 L1 来源,
以防止 batch 被重播到意外的 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
:
这暗示该 batch 尚未提前 L1 来源,因此必须对照 next_epoch
进行检查。
next_epoch
未知 -> undecided
:
如果没有下一个 L1 来源,我们还无法确定是否可以保持时间不变性。batch.timestamp >= next_epoch.time
-> drop
:
batch 本可以采用下一个 L1 来源,而不会破坏 L2 time >= L1 time
不变性。len(batch.transactions) > 0
-> drop
:
当超过排序器时间漂移时,永远不允许排序器包含交易。
batch.transactions
:如果 batch.transactions
列表包含以下交易,则 drop
这些交易无效或仅通过其他方式派生:
如果没有 batch 可以被 accept
,并且该阶段已完成从高度为 epoch.number + sequence_window_size
的 L1 区块中完全读取的所有 batch 的缓冲,并且 next_epoch
可用,则可以使用以下属性派生一个空 batch:
parent_hash = safe_l2_head.hash
timestamp = next_timestamp
transactions
为空,即没有排序器交易。 存款交易可能会在下一阶段添加。next_timestamp < next_epoch.time
:重复当前的 L1 来源,以保留 L2 时间不变性。
epoch_num = epoch.number
epoch_hash = epoch.hash
epoch_num = epoch.number
epoch_hash = epoch.hash
epoch_num = next_epoch.number
epoch_hash = next_epoch.hash
在 Payload Attributes Derivation(有效负载属性推导) 阶段,我们将从上一阶段获得的 batch 转换为 PayloadAttributes
结构的实例。 这种结构编码了需要放入区块中的交易,以及其他区块输入(时间戳、费用接收者等)。 有效负载属性推导在以下 Deriving Payload Attributes section(推导有效负载属性部分) 中详细介绍。
此阶段维护其自己的系统配置副本,独立于 L1 检索阶段。 每当 batch 输入引用的 L1 epoch 发生更改时,系统配置都会使用 L1 日志事件进行更新。
在 Engine Queue(引擎队列) 阶段中,先前派生的 PayloadAttributes
结构被缓冲并发送到 execution engine(执行引擎),以执行并转换为适当的 L2 区块。
该阶段维护对三个 L2 区块的引用:
此外,它还会缓冲最近处理的安全 L2 区块的引用历史,以及每个区块派生的 L1 区块的引用。 此历史记录不必完整,但可以使以后的 L1 终结信号转换为 L2 终结性。
要与引擎交互,请使用 execution engine API(执行引擎 API),包括以下 JSON-RPC 方法:
engine_forkchoiceUpdatedV1
— 如果不同,则将 forkchoice(即链头)更新为 headBlockHash
,如果有效负载属性参数不为 null
,则指示引擎开始构建执行有效负载。engine_getPayloadV1
— 检索先前请求的执行有效负载构建。engine_newPayloadV1
— 执行执行有效负载以创建区块。执行有效负载是 ExecutionPayloadV1
类型的对象。
如果有任何 forkchoice 更新要应用,则在派生或处理其他输入之前,首先将这些更新应用于引擎。
以下情况下可能会发生此同步:
新的 forkchoice 状态通过 engine_forkchoiceUpdatedV1
应用。
如果 forkchoice 状态无效,则必须重置派生管道以恢复到一致状态。
如果 unsafe head(不安全头)领先于 safe head(安全头),那么会尝试进行 consolidation(合并),验证现有的 unsafe L2 链是否与从规范的 L1 数据派生的 L2 输入匹配。
在合并期间,我们会考虑最旧的 unsafe L2 区块,即 safe head(安全头)之后的 unsafe L2 区块。 如果有效负载属性与此最旧的 unsafe L2 区块匹配,则该区块可以被认为是“安全的”,并成为新的 safe head(安全头)。
将检查派生的 L2 有效负载属性的以下字段是否与 L2 区块相等:
parent_hash
timestamp
randao
fee_recipient
transactions_list
(先是长度,然后是每个编码交易的相等性,包括存款)如果合并成功,则 forkchoice 更改将如上节所述进行同步。
如果合并失败,L2 有效负载属性将立即处理,如下一节所述。 选择有效负载属性而不是以前的 unsafe L2 区块,从而在当前的 safe block(安全区块)之上创建 L2 链重组。 立即处理新的替代属性使像 go-ethereum 这样的执行引擎能够实施更改,因为可能不支持链尖端的线性倒带。
如果 safe(安全)和 unsafe(不安全)的 L2 heads(头)相同(无论是由于合并失败还是其他原因),我们会将 L2 有效负载属性发送到执行引擎,以构建成适当的 L2 区块。 然后,此 L2 区块将成为新的 L2 safe(安全)和 unsafe head(不安全头)。
如果由于验证错误(即,区块*中存在无效交易或状态转换)而无法将从 batch 创建的有效负载属性插入到链中,则应删除该 batch,并且不应提前安全头。 引擎队列将尝试使用来自 batch 队列的该时间戳的下一个 batch。 如果未找到有效的 batch,则汇总节点将创建一个仅存款 batch,该 batch 应始终通过验证,因为存款始终有效。
与执行引擎通过执行引擎 API 的交互在 Communication with the Execution Engine(与执行引擎通信) 部分中进行了详细说明。
然后,通过以下顺序处理有效负载属性:
engine_forkchoiceUpdatedV1
具有该阶段的当前 forkchoice 状态,以及用于启动区块构建的属性。
engine_getPayload
按上一步结果中的有效负载 ID 检索有效负载。engine_newPayload
将新有效负载导入到执行引擎中。engine_forkchoiceUpdatedV1
使新有效负载规范化,
现在 safe
和 unsafe
字段都已更改,以引用有效负载,而没有有效负载属性。引擎 API 错误处理:
如果没有 forkchoice 更新或 L1 数据需要处理,并且如果下一个可能的 L2 区块已通过不安全的来源(例如排序器通过 p2p 网络发布)获得,则会乐观地将其处理为“unsafe(不安全)”的区块。 在理想情况下,这会将以后的派生工作减少到仅与 L1 合并,并使用户能够比 L1 确认 L2 batch 更快地看到 L2 链的头。
要处理不安全的有效负载,该有效负载必须:
然后,通过以下顺序处理有效负载:
engine_newPayloadV1
:处理有效负载。 它尚未成为规范的。engine_forkchoiceUpdatedV1
:使有效负载成为规范的 unsafe(不安全)L2 头,并保留 safe(安全)/finalized(最终确定)的 L2 头。引擎 API 错误处理:
可以重置管道,例如,如果我们检测到 L1 reorg (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
,否则如果不更改则递增 1
)n
的 L1 来源比 highest
的 L1 来源早于一个序列窗口,
并且 n.sequence_number == 0
,则 n
的父 L2 区块将是 safe
起始点。finalized
L2 区块作为 finalized
起始点保留。l2base
的此区块引用的 L1 来源将是 L2 管道派生的 base
:
通过从此处开始,各个阶段可以缓冲任何必要的数据,同时删除不完整的派生输出,直到
L1 遍历已赶上实际的 L2 安全头。在向后遍历 L2 链时,实现可能会健全性检查起始点永远不会设置得太远 与现有的 forkchoice 状态相比,为了避免由于配置错误导致的密集重组。
实施者请注意:步骤 1-4 称为 FindL2Heads
。 步骤 5 当前是引擎队列重置的一部分。
这可能会更改为将起始点搜索与裸重置逻辑隔离。
base
作为下一个阶段要提取的第一个块开始。base
L1 数据,或将获取工作推迟到以后的管道步骤。base
作为初始 L1 参考点。finalized
/safe
/unsafe
)base
。在必要时,从 base
开始的阶段可以从 l2base
区块中编码的数据初始化其系统配置。
请注意,在 [merge(合并)] 之后,重组的深度将受到 L1 finality delay(L1 终结延迟) 的限制 (2 个 L1 信标 epoch,或大约 13 分钟,除非超过 1/3 的网络持续存在分歧)。 新的 L1 区块可以每隔一个 L1 信标 epoch(大约 6.4 分钟)完成,并且取决于这些 终结信号和批量包括,派生的 L2 链也将变得不可逆转。
请注意,这种终结形式只会影响输入,然后节点可以主观地说链是不可逆转的, 通过从这些不可逆转的输入和设置的协议规则和参数重现链。
然而,这与发布在 L1 上的输出完全无关,这需要一种证明形式,如故障证明或 zk 证明才能完成。 乐观 Rollup输出(如 L1 上的提款)仅在经过一周后才被标记为“已完成”,而没有争议(故障证明挑战窗口),这是与权益证明完成的名称冲突。
对于从 L1 数据派生的每个 L2 区块,我们需要构建 payload attributes(有效负载属性),
由 PayloadAttributesV1
对象的 expanded version(扩展版本) 表示,
其中包括附加的 transactions
和 noTxPool
字段。
此过程发生在一个验证器节点运行的有效负载属性队列期间,以及由一个排序器节点运行的区块生产期间(如果交易是批量提交的,则排序器可以启用 tx-pool 的使用)。
对于排序器要创建的每个 L2 区块,我们从与 目标 L2 区块编号匹配的 sequencer batch(排序器批次) 开始。 如果 L1 链没有包含目标 L2 区块编号的批次,这可能是一个自动生成的空批次。 记住,批次包括sequencing epoch(排序纪元) 编号、L2 时间戳和交易列表。
该区块是sequencing epoch(排序纪元)的一部分, 其编号与 L1 区块(其L1 origin(L1 源))的编号匹配。 此 L1 区块用于派生 L1 属性和(对于纪元中的第一个 L2 区块)用户存款。
因此,PayloadAttributesV1
对象必须包含以下交易:
交易必须按此顺序出现在有效负载属性中。
L1 属性从 L1 区块头读取,而存款从 L1 区块的 receipts(收据) 读取。 有关如何将存款编码为日志条目的详细信息,请参阅 deposit contract specification(存款合约规范)。
在派生交易列表后,rollup 节点按如下所示构造一个 PayloadAttributesV1
:
timestamp
设置为批次的时间戳。random
设置为 prev_randao
L1 区块属性。suggestedFeeRecipient
设置为排序器费用库地址。 请参阅 [Fee Vaults(费用库)] 规范。transactions
是派生交易的数组:已存入的交易和排序的交易,全部使用 EIP-2718 进行编码。noTxPool
设置为 true
,以便在构造区块时使用上面的 exact(完全)transactions
列表。gasLimit
设置为此有效负载的 system configuration(系统配置) 中的当前 gasLimit
值。
- 原文链接: github.com/ethereum-opti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!