本文档是 L2 链衍生规范,描述了从 L1 数据推导 L2 区块的过程,这是 Rollup 节点的主要职责之一。内容涵盖了从 L1 区块中提取数据、构建批次、处理交易、以及最终生成 L2 区块的整个流程,还包括批量提交的格式、架构设计、以及如何在 L1 重组时重置管道等重要方面。本文档针对rollup节点。
<!-- 本文件中所有词汇表的引用。-->
<!-- 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 区块。epoch 编号定义为等于相应的 L1 区块编号。
为了推导出时期 E
中的 L2 区块,我们需要以下输入:
E
的 L1 排序窗口:范围 [E, E + SWS)
内的 L1 区块,其中 SWS
是排序窗口大小(请注意,这意味着 epoch 是重叠的)。特别是,我们需要:
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_timestamp
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 区块的 epoch 关联的 L1 区块的时间戳max_sequencer_drift
是允许排序器超前于 L1 的最大值总而言之,这些约束条件意味着必须每 l2_block_time
秒有一个 L2 区块,并且一个 epoch 的第一个 L2 区块的时间戳绝不能落后于与该 epoch 匹配的 L1 区块的时间戳。
合并后,以太坊具有 12 秒的固定区块时间(尽管可以跳过某些时段)。因此,预计使用 2 秒的 L2 区块时间,大多数情况下,每个 epoch 将包含 12/2 = 6
个 L2 区块。但是,排序器可以延长或缩短 epoch(受上述约束)。理由是维护在 L1 上跳过时段或与 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
。有关更多信息,请参阅 [批量队列]。
在实践中,通常没有必要等待 L1 区块的完整排序窗口才能开始推导 epoch 中的 L2 区块。实际上,只要我们能够重建顺序批次,我们就可以开始推导相应的 L2 区块。我们称之为 eager 区块推导。
但是,在最坏的情况下,我们只能通过读取排序窗口的最后一个 L1 区块来重建 epoch 中第一个 L2 区块的批次。当该批次的某些数据包含在窗口的最后一个 L1 区块中时,就会发生这种情况。在这种情况下,我们不仅无法推导 epoch 中的第一个 L2 区块,而且在此之前,我们也无法推导 epoch 中的任何进一步的 L2 区块,因为它们需要应用 epoch 的第一个 L2 区块所产生的状态。(请注意,这仅适用于 区块 推导。批次仍然可以被推导并临时排队,我们只是无法从中创建区块。)
排序器 接受来自用户的 L2 交易。它负责从这些交易中构建区块。对于每个这样的区块,它还会创建一个相应的排序器批量。它还负责将每个批量提交给数据可用性提供商(例如以太坊 calldata),它通过其批量器 组件来做到这一点。
L2 区块和批量之间的区别很微妙但很重要:区块包括 L2 状态根,而批量仅在给定的 L2 时间戳(等效地:L2 区块编号)提交交易。区块还包括对前一个区块的引用(*)。
(*)这在发生 L1 重组并且批量将被重新发布到 L1 链但不是先前的批量的情况下很重要,而 L2 区块的前身不可能更改。
这意味着即使排序器错误地应用了状态转换,批量中的交易仍将被视为规范 L2 链的一部分。批量仍然受到有效性检查(即,它们必须被正确编码),并且批量中的单个交易也受到有效性检查(例如,签名必须有效)。无效批量和有效批量中的无效单个交易将被正确的节点丢弃。
如果排序器错误地应用了状态转换并发布了输出根,则此输出根将不正确。不正确的输出根将受到故障证明的挑战,然后被 现有排序器批量 的正确输出根替换。
有关更多信息,请参阅批量提交规范。
批量提交与 L2 链推导密切相关,因为推导过程必须解码已编码用于批量提交目的的批量。
批量器 将 批量器交易 提交给 数据可用性提供商。这些交易包含一个或多个 通道帧,这些帧是属于 通道 的数据块。
通道 是一系列压缩在一起的 排序器批量(对于任何 L2 区块)。将多个批量分组在一起的原因仅仅是为了获得更好的压缩率,从而降低数据可用性成本。
通道可能太大而无法容纳在单个 批量器交易 中,因此我们需要将其拆分为称为 通道帧 的块。单个批量器交易也可以携带多个帧(属于相同或不同的通道)。
这种设计使我们能够以最大的灵活性将批量聚合到通道中,并将通道拆分到批量器交易中。值得注意的是,它使我们能够最大化批量器交易中的数据利用率:例如,它使我们能够将窗口的最后一个(小)帧与来自下一个窗口的大帧打包在一起。
将来,这种通道识别功能还允许 批量器 雇用多个签名者(私钥)并行提交一个或多个通道(1)。
(1) 这有助于缓解以下问题:由于交易 nonce 值影响 L2 交易池,从而影响包含:同一签名者制作的多个交易都卡住等待先前交易的包含。
另请注意,我们使用流式压缩方案,并且当我们启动通道时,或者甚至在我们发送通道中的第一个帧时,我们不需要知道一个通道最终将包含多少个区块。
通过跨多个数据交易拆分通道,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 区块的 epoch 匹配的 L1 区块的信息。第一个数字表示 epoch/L1x 编号,而第二个数字(“序列号”)表示 epoch 中的位置。
最后,第六行显示从前面提到的存款合约事件中推导出的用户存款交易。
请注意该图右下角的 101-0
L1 属性交易。只有在以下情况下才可能存在:帧 B2
表明它是通道中的最后一个帧,并且 (2) 不能插入空区块。
该图未指定使用的排序窗口大小,但由此我们可以推断出它必须至少为 4 个区块,因为通道 A
的最后一帧出现在区块 102 中,但属于 epoch 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 区块的 排序 epoch 相对应的 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 上运行,对于每个交易:
每个数据 transaction 都是版本化的,并包含一系列 通道帧,供帧队列读取,请参阅 批量提交线格式。
帧队列一次缓冲一个数据 transaction,解码为 通道帧,供下一阶段使用。请参阅 批量器 transaction 格式 和 帧格式 规范。
通道银行 阶段负责管理从 L1 检索阶段写入的数据通道银行的缓冲。通道银行阶段中的一个步骤尝试从“准备就绪”的通道读取数据。
通道目前是完全缓冲的,直到读取或丢弃,流通道可能在未来版本的通道银行中得到支持。
为了绑定资源使用,通道银行基于通道大小进行修剪,并使旧通道超时。
通道以 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 后再次使用它们,但建议批量器实现使用唯一的通道 ID。
在这个阶段,我们解压缩从上一阶段提取的通道,然后从解压缩的字节流中解析 batch。
有关解压缩和解码规范,请参阅 批量格式。
在 批量缓冲 阶段,我们按时间戳重新排序批量。如果某些 时间槽 缺少批量,并且存在具有较高时间戳的有效批量,则此阶段还会生成空批量以填补空白。
每当有一个顺序批量直接跟随当前 safe L2 head(可以从规范 L1 链推导出的最后一个区块)的时间戳时,批量就会被推送到下一阶段。批量的父哈希也必须与当前 safe L2 head 的哈希匹配。
请注意,从 L1 推导出的批量中存在任何间隙意味着此阶段将需要缓冲整个 排序窗口,然后才能生成空批量(因为在最坏的情况下,缺失的批量可能在窗口的最后一个 L1 区块中包含数据)。
批量可以有 4 种不同的有效性形式:
drop
:批量无效,并且将来总是无效,除非我们进行重组。可以从缓冲区中删除。accept
:批量有效,应进行处理。undecided
:我们缺少 L1 信息,直到我们可以继续进行批量过滤。future
:批量可能有效,但尚未处理,应稍后再次检查。批量按包含在 L1 上的顺序处理:如果可以“accept”多个批量,则应用第一个批量。实现可以将“future”批量延迟到以后的推导步骤,以减少验证工作。
批量的有效性推导如下:
定义:
batch
。epoch = safe_l2_head.l1_origin
与批量相关联的 L1 origin,具有属性: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_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
:
当超过排序器时间漂移时,永远不允许排序器包含交易。batch.transactions
:如果 batch.transactions
列表包含交易,则 drop
该交易无效或仅通过其他方式派生:
如果没有批次可以被 accept
,并且该阶段已完成从 L1 完全读取的所有批次的缓冲
在高度为 epoch.number + sequence_window_size
的区块处,并且 next_epoch
可用,
然后可以使用以下属性派生一个空批次:
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(负载属性推导) 阶段,我们将从前一阶段获得的批次转换为
PayloadAttributes
结构的实例。 这样的结构编码了需要体现在
区块中的交易,以及其他区块输入(时间戳、费用接收者等)。负载属性推导在
推导负载属性部分 中详细介绍。
此阶段维护其自己的 系统配置 副本,独立于 L1 检索阶段。 每当批次输入引用的 L1 epoch 发生更改时,系统配置都会使用 L1 日志事件进行更新。
在 Engine Queue(引擎队列) 阶段,先前派生的 PayloadAttributes
结构会被缓冲并发送到
执行引擎 以执行并转换为适当的 L2 区块。
该阶段维护对三个 L2 区块的引用:
此外,它还会缓冲最近处理的安全 L2 区块的引用的一小段历史记录,以及引用 从哪些 L1 区块派生出每个区块。 该历史记录不必完整,但可以将以后的 L1 最终确定信号转换为 L2 最终确定性。
为了与引擎交互,使用了 execution engine API(执行引擎 API),其中包含以下 JSON-RPC 方法:
engine_forkchoiceUpdatedV1
- 将 forkchoice(即链头)更新为 headBlockHash
(如果不同),并且
指示引擎在 payload 属性参数不是 null
时开始构建执行 payload。engine_getPayloadV1
- 检索先前请求的执行 payload 构建。engine_newPayloadV1
- 执行执行 payload 以创建区块。执行 payload 是类型为 ExecutionPayloadV1
的对象。
如果有任何要应用的 forkchoice 更新,则在派生或处理其他输入之前,这些更新将首先应用于引擎。
发生这种情况的情况包括:
新的 forkchoice 状态通过 engine_forkchoiceUpdatedV1
应用。
如果发生 forkchoice 状态有效性错误,必须重置派生管道以恢复到一致的状态。
如果不安全头领先于安全头,则尝试进行 合并,验证 现有不安全的 L2 链是否与从规范 L1 数据派生的 L2 输入相符。
在合并期间,我们考虑最旧的不安全 L2 区块,即紧接安全头之后的不安全 L2 区块。 如果 payload 属性与此最旧的不安全 L2 区块匹配,则该区块可以被认为是“safe(安全)”并且成为新的 安全头。
派生的 L2 payload 属性的以下字段经过检查,以确保与 L2 区块相等:
parent_hash
timestamp
randao
fee_recipient
transactions_list
(首先是长度,然后是每个编码交易的相等性,包括存款)如果合并成功,则 forkchoice 更改将如上面的部分中所述进行同步。
如果合并失败,则 L2 payload 属性将立即处理,如下所述。
如果安全和不安全 L2 头相同(无论是由于合并失败还是其他原因),我们会将 L2 payload 属性发送到执行引擎,以构建成适当的 L2 区块。 然后,该 L2 区块将成为新的 L2 安全头和不安全头。
如果由于验证错误(即存在 区块中的无效交易或状态转换),则不应提前。 引擎队列将尝试使用来自批次队列的该时间戳的下一个批次。 如果找不到有效的批次 ,则汇总节点将创建一个仅存款批次,该批次应始终通过验证,因为存款始终有效。
通过执行引擎 API 与执行引擎的交互在 与执行引擎通信 部分中详细介绍。
然后使用以下顺序处理 payload 属性:
engine_forkchoiceUpdatedV1
具有该阶段的当前 forkchoice 状态以及用于启动区块构建的属性。
engine_getPayload
以检索 payload,通过上一步结果中的 payload-ID。engine_newPayload
以将新的 payload 导入到执行引擎中。engine_forkchoiceUpdatedV1
使新的 payload 规范化,
现在 safe
和 unsafe
字段都更改为引用该 payload,并且没有 payload 属性。Engine API Error handling(引擎 API 错误处理):
如果没有 forkchoice 更新或 L1 数据需要处理,并且如果可以通过排序器通过 p2p 网络发布的不安全来源获得下一个可能的 L2 区块,则会乐观地将其处理为“不安全”区块。 这样可以减少以后仅与 L1 合并的派生工作,并使用户能够比 L1 的确认 L2 批次更快地看到 L2 链的头。
要处理不安全的 payload,payload 必须:
然后使用以下顺序处理 payload:
engine_newPayloadV1
:处理 payload。 它尚未成为规范。engine_forkchoiceUpdatedV1
:使 payload 成为规范的不安全 L2 头,并保留安全/最终确定的 L2 头。Engine API Error handling(引擎 API 错误处理):
可以重置管道,例如,如果我们检测到 L1 reorg (reorganization)(重组)。 这使rollup节点能够处理L1链重组事件。
重置会将管道恢复到一种状态,该状态产生的输出与完整的 L2 派生过程相同, 但从现有的 L2 链开始,该链被向后遍历,足以与当前的 L1 链协调。
请注意,此算法涵盖了几个重要的用例:
处理这些情况也意味着可以将节点配置为急切地同步具有 0 个确认的 L1 数据, 因为它可以在 L1 后来识别该数据为规范时撤消更改,从而实现安全的低延迟使用。
首先重置引擎队列,以确定从哪个 L1 和 L2 起始点继续派生。 在此之后,其他阶段将彼此独立地重置。
要找到起始点,相对于向后遍历的链头,有几个步骤:
finalized
区块,则从 Bedrock 创世区块开始。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
)将是 L2 管道派生的 base
:
通过从这里开始,各阶段可以缓冲任何必要的数据,同时删除不完整的派生输出,直到
L1 遍历赶上实际的 L2 安全头。在向后遍历 L2 链时,实现可以完整性检查,以确保起始点永远不会设置得太远 与现有的 forkchoice 状态相比,可以避免由于配置错误而导致的密集重组。
实施者请注意:步骤 1-4 称为 FindL2Heads
。 步骤 5 当前是引擎队列重置的一部分。
这可能会更改为将起始点搜索与裸重置逻辑隔离。
base
开始,作为下一个阶段拉取的第一个区块。base
L1 数据,或将获取工作推迟到以后的管道步骤。base
作为初始的 L1 参考点。finalized
/safe
/unsafe
)base
。在必要时,从 base
开始的阶段可以从 l2base
区块中编码的数据初始化其系统配置。
请注意,在 [合并] 之后,重组的深度将受到 L1 最终确定延迟 的限制 (2 个 L1 信标 epoch,或大约 13 分钟,除非超过 1/3 的网络始终不同意)。 每个 L1 信标 epoch(大约 6.4 分钟)可能会最终确定新的 L1 区块,并且取决于这些 最终确定性信号和批次包含,派生的 L2 链也将变得不可逆。
请注意,这种形式的最终确定性仅影响输入,节点可以主观地说链是不可逆的, 通过从这些不可逆的输入以及一组协议规则和参数中重现链。
然而,这与发布在 L1 上的输出完全无关,后者需要一种证明形式,如容错证明或 zk 证明才能最终确定。 乐观rollup输出(如 L1 上的提款)只有在经过一周后才被标记为“finalized(已最终确认)”, 没有争议(容错证明挑战窗口),这与权益证明最终确认性发生了名称冲突。
对于从 L1 数据派生的每个 L2 区块,我们需要构建 payload attributes(负载属性),
由 PayloadAttributesV1
对象的 expanded version(扩展版本) 表示,
其中包括附加的 transactions
和 noTxPool
字段。
此过程发生在由验证器节点运行的负载属性队列期间,以及在区块生产期间 由排序器节点运行(如果交易是批量提交的,则排序器可以启用交易池使用)。
对于排序器要创建的每个 L2 区块,我们从与 目标L2区块号。如果 L1 链中没有包含目标 L2 区块的批次,则这可能是自动生成的空批次。记住批次包括一个排序 epoch(排序 epoch) 号、一个 L2 时间戳和一个交易列表。
此区块是排序 epoch(排序 epoch)的一部分, 其编号与 L1 区块的编号相匹配(其L1 origin(L1 来源))。 此 L1 区块用于派生 L1 属性和(对于 epoch 中的第一个 L2 区块)用户存款。
因此,PayloadAttributesV1
对象必须包括以下交易:
交易必须以此顺序出现在 payload 属性中。
L1 属性从 L1 区块头中读取,而存款从 L1 区块的 receipts(收据) 中读取。 有关存款如何编码为日志条目的详细信息,请参阅deposit contract specification(存款合约规范)。
派生交易列表后,rollup 节点按如下方式构建 PayloadAttributesV1
:
timestamp
设置为批次的时间戳。random
设置为 prev_randao
L1 区块属性。suggestedFeeRecipient
设置为排序器费用金库地址。 请参阅 [Fee Vaults(费用金库)] 规范。transactions
是派生交易的数组:存入的交易和排序的交易,全部
使用 EIP-2718 进行编码。noTxPool
设置为 true
,以便在构建区块时使用上面确切的 transactions
列表。gasLimit
设置为此 payload 的 system configuration(系统配置) 中的当前 gasLimit
值。
- 原文链接: github.com/ethereum-opti...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!