在 OP Stack 中,派生管道读取 L1 批次和存款交易来生成 L2 区块,每个 L2 区块头都携带一个 L1 来源(L1 区块号和 L1 时间戳)。由于 L1 时间戳是规范的时间来源,L2 区块时间戳受其 L1 来源时间戳约束,因此 L2 时间是 L1 时间的派生量。你可以通过 RPC 读取这一来源信息:将标准的 L2 eth_getBlockByNumber 时间戳与 op-node rollup 命名空间结合使用,尤其是 optimism_syncStatus,它会暴露 unsafe、safe 和 finalized 的 L2 头部及其 L1 来源。仅依赖时间戳进行索引在重组期间是不安全的,因为同一个挂钟秒在重组前后可能映射到不同的 L2 区块;锚定到 L1 来源可以使映射可复现。本指南介绍其机制、可运行的 Node.js 读取器、用于在你自己的端点上填写的结结果表,以及 unsafe、safe 和 finalized 来源的局限性。
Base L2 区块生产与派生管道
Base 是一个 OP Stack L2。排序器对用户交易进行排序并快速生成 L2 区块;这些区块最初是 unsafe 的,因为它们尚未从发布到 L1 的数据中派生出来。随后 OP Stack 派生管道读取 L1 数据(包括批次提交和存款交易),并据此重建规范的 L2 链。OP Stack 规范 - 派生 将该管道定义为 L2 链状态的权威来源。
实际后果是 L2 区块生产和 L2 区块最终性是两回事。排序器提供低延迟的包含,而针对 L1 的派生则赋予链规范且可重新派生的历史。对于 RPC 消费者而言,这意味着你作为 latest 读取的区块之后可能被重组,而已经派生并在 L1 上最终确定的区块则是稳定的。
Base 文档在 Base 文档 - L2 执行引擎 / 节点 RPC 中描述了 L2 执行引擎和节点 RPC 接口。执行引擎存储 L2 区块;op-node(rollup 节点)驱动派生并暴露 rollup 专用 RPC 方法,以揭示 L1 来源。
- 排序器:对交易排序,生成 unsafe L2 区块。
- 批次提交器:将 L2 交易数据以批次形式发布到 L1。
- 派生管道:读取 L1 批次和存款,重建 L2 区块。
- op-node:暴露 rollup RPC,例如 optimism_syncStatus 和 optimism_outputAtBlock。
L1 来源作为 L2 时间戳的规范时间来源
每个 L2 区块头都携带一个 L1 来源:该 L2 区块所派生自的 L1 区块号和 L1 时间戳。L1 时间戳是 L2 区块时间戳的规范时间来源,因此 L2 区块时间戳受其 L1 来源时间戳约束。换句话说,L2 时间是 L1 时间的派生量,而不是独立的时钟。
这种设计使 L2 时间戳与最终保障其安全的 L1 链保持一致。如果你仅按时间戳索引 L2 数据,你就是在按一个只有相对于 L1 来源才有意义的值进行索引。两个具有相同时间戳的 L2 区块可以存在于不同的派生上下文中,而重组可能改变占据某个给定时间戳的 L2 区块。
op-node rollup RPC 暴露了这种关系。optimism_syncStatus 返回 unsafe、safe 和 finalized 的 L2 头部及其 L1 来源,optimism_outputAtBlock 返回给定 L2 区块的输出根和区块引用。这些方法让你能够为你读取的任何 L2 区块附加一个 L1 区块号。
- L2 区块头包含 L1 来源(L1 区块号和 L1 时间戳)。
- L2 时间戳受 L1 来源时间戳约束。
- optimism_syncStatus 暴露 unsafe/safe/finalized L2 头部及其 L1 来源。
- optimism_outputAtBlock 返回 L2 区块的输出根和区块引用。
使用标准 RPC 和 Rollup RPC 读取 L2 区块来源
标准的 L2 eth_getBlockByNumber 返回 L2 区块时间戳、编号、哈希和交易。仅凭这一点无法告诉你 L1 来源。要附加来源信息,请调用 op-node rollup 命名空间。optimism_syncStatus 为你提供当前 unsafe、safe 和 finalized 的 L2 头部及其 L1 来源;optimism_outputAtBlock 为你提供特定 L2 区块的输出根。
一个实用的模式是:先用 eth_getBlockByNumber 读取 L2 区块,然后读取 optimism_syncStatus 以了解每个头部当前的 L1 来源,再对你关心的特定区块使用 optimism_outputAtBlock。这样你就可以说明任何 L2 区块派生自哪个 L1 区块,以及它当前是 unsafe、safe 还是 finalized。
如果你使用的是托管端点,rollup 命名空间可能与 L2 执行 RPC 暴露在同一个 URL 上,也可能在单独的 op-node URL 上。请查阅你的提供商文档。OnFinality 的 Base RPC 端点(RPC Assistant) 页面描述了可用的 Base 端点,Base 网络页面 则涵盖了网络背景。
- eth_getBlockByNumber:L2 区块时间戳、编号、哈希、交易。
- optimism_syncStatus:unsafe/safe/finalized L2 头部及其 L1 来源。
- optimism_outputAtBlock:L2 区块的输出根和区块引用。
- Rollup 命名空间可能在同一或单独的端点上;请与你的提供商核实。
可运行的 Node.js 读取器:将 L2 区块映射到其 L1 来源
以下 Node.js 脚本按编号读取 L2 区块,然后读取 optimism_syncStatus 以获取 unsafe、safe 和 finalized 头部的 L1 来源,最后读取特定区块的 optimism_outputAtBlock。它打印 L2 区块时间戳、L1 来源区块号和时间戳,以及以秒为单位的 L1 到 L2 包含延迟。
将 BASE_L2_RPC_URL 设置为你的 L2 执行端点,将 BASE_ROLLUP_RPC_URL 设置为你的 op-node rollup 端点。如果你的提供商在一个 URL 上暴露两个命名空间,请将两个变量设置为相同的值。该脚本仅使用现代 Node.js 中内置的 fetch。
// map-l2-to-l1-origin.js
// Usage: BASE_L2_RPC_URL=... BASE_ROLLUP_RPC_URL=... node map-l2-to-l1-origin.js 12345678
const L2_URL = process.env.BASE_L2_RPC_URL;
const ROLLUP_URL = process.env.BASE_ROLLUP_RPC_URL || L2_URL;
const blockNumber = process.argv[2] ? parseInt(process.argv[2], 10) : null;
async function rpc(url, method, params) {
const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(method + ': ' + JSON.stringify(json.error));
return json.result;
}
async function main() {
if (blockNumber === null) throw new Error('Pass an L2 block number');
const l2Block = await rpc(L2_URL, 'eth_getBlockByNumber', [
'0x' + blockNumber.toString(16), false
]);
if (!l2Block) throw new Error('L2 block not found');
const sync = await rpc(ROLLUP_URL, 'optimism_syncStatus', []);
const output = await rpc(ROLLUP_URL, 'optimism_outputAtBlock', [
'0x' + blockNumber.toString(16)
]);
const l2Ts = parseInt(l2Block.timestamp, 16);
const l1Origin = output.blockRef ? output.blockRef.l1origin : null;
const l1Number = l1Origin ? parseInt(l1Origin.number, 16) : null;
const l1Ts = l1Origin ? parseInt(l1Origin.timestamp, 16) : null;
console.log('L2 block number:', blockNumber);
console.log('L2 block hash:', l2Block.hash);
console.log('L2 timestamp:', l2Ts, new Date(l2Ts * 1000).toISOString());
console.log('L1 origin number:', l1Number);
console.log('L1 origin timestamp:', l1Ts, l1Ts ? new Date(l1Ts * 1000).toISOString() : null);
if (l1Ts !== null) {
console.log('L1-to-L2 inclusion latency (s):', l2Ts - l1Ts);
}
console.log('Unsafe L2 head:', sync.unsafe_l2 ? sync.unsafe_l2.number : null);
console.log('Safe L2 head:', sync.safe_l2 ? sync.safe_l2.number : null);
console.log('Finalized L2 head:', sync.finalized_l2 ? sync.finalized_l2.number : null);
}
main().catch((err) => { console.error(err); process.exit(1); });结果表:在你的端点上测量 L1 到 L2 包含延迟
使用上面的脚本采样几个 L2 区块并记录结果。下表故意留空;请用在你自己的端点上测得的值填写。不要将任何单个样本视为基准。延迟会随 L1 区块时间、批次提交节奏以及你选择的区块而变化。
对于每一行,记录 L2 区块号、L2 时间戳、L1 来源编号、L1 来源时间戳以及以秒为单位的差值。还要记录测量时该区块是 unsafe、safe 还是 finalized。在不同时间重复测量,以观察数值如何变化。
- L2 区块号:你查询的区块。
- L2 时间戳:来自 eth_getBlockByNumber。
- L1 来源编号:来自 optimism_outputAtBlock blockRef.l1origin.number。
- L1 来源时间戳:来自 optimism_outputAtBlock blockRef.l1origin.timestamp。
- L1 到 L2 包含延迟(秒):L2 时间戳减去 L1 来源时间戳。
- 来源状态:测量时的 unsafe、safe 或 finalized。
为什么仅依赖时间戳的索引在重组时会失效
L1 上的重组可能改变哪些 L2 区块派生自哪个 L1 区块。如果你仅按时间戳索引 L2 数据,重组可能会静默地重新映射你的索引:同一个挂钟秒在重组后可能对应不同的 L2 区块。没有 L1 来源锚点,你无法判断某个 L2 区块是否仍然是规范的。
锚定到 L1 来源可以使映射可复现。当你将 L1 来源区块号与 L2 区块一起存储时,你可以在重组后重新检查派生,并检测到该 L2 区块的来源发生了变化。这对于需要对存款、提款或状态证明进行对账的应用尤为重要。
Base OP-Stack 最终性:safe、finalized 和 latest 区块标签 一文解释了 safe 和 finalized 标签与派生的关系,检查 Base OP-Stack 节点同步状态 一文则更深入地介绍了同步状态读取。
- 仅时间戳索引:重组后同一秒可能映射到不同的 L2 区块。
- L1 来源锚点:让你重新检查派生并检测来源变化。
- 对于必须与 L1 对账的存款、提款和状态证明至关重要。
Unsafe、Safe 和 Finalized 来源:各自保证什么
Unsafe L2 区块由排序器生成,尚未从 L1 派生。它们速度快但可能被重组。Safe L2 区块已从 L1 批次派生,在派生点之前被认为对 L1 重组安全。Finalized L2 区块派生自本身已最终确定的 L1 区块,提供最强的保证。
optimism_syncStatus 暴露所有三个头部及其 L1 来源。当你读取一个 L2 区块时,你应该记录它处于或低于哪个头部。处于或低于 finalized 头部的区块具有最强的来源;高于 safe 头部但处于或低于 unsafe 头部的区块仍然可能变化。
关于这些标签的更深入讨论,请参阅 Base OP-Stack 最终性文章。关于同步状态机制,请参阅 节点同步状态文章。
- Unsafe:排序器生成,尚未派生,可能重组。
- Safe:从 L1 批次派生,在派生点之前对 L1 重组安全。
- Finalized:派生自已最终确定的 L1 区块,最强保证。
- 为你索引的每个 L2 区块记录其头部级别。
L1 来源的局限性与权衡
通过 RPC 读取 L1 来源会增加调用和复杂性。optimism_syncStatus 和 optimism_outputAtBlock 是 rollup 命名空间方法;它们可能并非在每个端点上可用,并且它们的速率限制可能与标准 L2 方法不同。在生产环境中依赖它们之前,请与你的提供商核实可用性。
来源也是一个移动的目标。现在 unsafe 的区块之后可能变为 safe,现在 safe 的区块之后可能变为 finalized。如果你缓存来源信息,必须重新检查。排序器重启也可能影响 unsafe 头部:重启后,排序器可能重新派生或重新生成区块,unsafe 头部可能移动。
最后,L1 到 L2 包含延迟不是固定值。它取决于 L1 区块时间、批次提交节奏以及具体区块。不要将任何单次测量视为基准。请在你自己的端点上测量并记录条件。
- Rollup 命名空间在某些端点上可能不可用或受速率限制。
- 来源状态随时间变化;缓存值必须重新检查。
- 排序器重启可能移动 unsafe 头部。
- 包含延迟会变化;要测量,不要假设。
排查常见的来源 RPC 故障
如果 optimism_syncStatus 返回方法未找到错误,你的端点很可能没有暴露 rollup 命名空间。尝试专用的 op-node URL 或查阅你的提供商文档。OnFinality 的 Base RPC 端点(RPC Assistant) 页面列出了可用的 Base 端点及其命名空间。
如果 optimism_outputAtBlock 对某个区块号返回错误,该区块可能高于当前 unsafe 头部,或者 op-node 尚未派生它。稍等片刻后重试,或查询更低的区块号。如果 L1 来源字段缺失,请确认你正确读取了 blockRef 对象;字段名区分大小写。
如果你的 L2 区块时间戳早于 L1 来源时间戳,你很可能混淆了两个时间戳或读取了过期的区块。重新读取两个值并比较。如果差异持续存在,请检查你的端点是否在提供不同的链或缓存响应。
- 方法未找到:rollup 命名空间未暴露;尝试专用的 op-node URL。
- 区块未找到:区块可能高于 unsafe 头部或尚未派生;重试或降低区块号。
- 缺少 L1 来源字段:检查 blockRef 字段名和大小写。
- 时间戳倒置:重新读取两个时间戳;检查过期或缓存响应。
来源感知索引的操作实践
将 L1 来源区块号和 L1 来源时间戳与你索引的每个 L2 区块一起存储。这让你可以在重组后重新检查派生并检测来源变化。它还让你能够在整个数据集中一致地计算包含延迟。
按计划轮询 optimism_syncStatus 并记录 unsafe、safe 和 finalized 头部。当你读取一个 L2 区块时,将其编号与这些头部比较并记录来源状态。这为你提供了一种可复现的方式来分类区块,而不必仅依赖时间戳。
对于历史数据,请使用归档端点。Base 归档节点与历史 RPC 一文解释了归档访问,OP-Stack L1 数据费用与交易成本 一文涵盖了同样依赖 L1 数据的费用核算。关于存款和提款对账,请参阅 追踪 Base 跨链存款日志和提款证明。
- 为每个 L2 区块存储 L1 来源编号和时间戳。
- 轮询 optimism_syncStatus 并记录头部级别。
- 按来源状态而非仅时间戳对区块进行分类。
- 使用归档端点进行历史来源查询。
下一步:最终性、同步状态与费用背景
要深入了解,请阅读 Base OP-Stack 最终性文章 了解 safe 和 finalized 标签,以及 节点同步状态文章 了解同步状态机制。它们共同解释了你在 optimism_syncStatus 中读取的头部与派生和最终性的关系。
关于成本和核对工作,请参阅 OP-Stack L1 数据费用文章 和 存款与提款证明文章。关于历史查询,请参阅 Base 归档节点文章。
如果你正在选择端点,请从 Base 网络页面 和 Base RPC 端点(RPC Assistant) 页面开始。关于定价和服务详情,请参阅 RPC 定价 和 API 服务 页面。OnFinality Learn 中心 汇集了完整的指南集。
- 最终性标签:safe、finalized、latest。
- 同步状态:unsafe、safe、finalized 头部及其 L1 来源。
- 费用:L1 数据费用取决于 L1 数据可用性。
- 对账:存款和提款需要 L1 来源锚点。