在 Base(OP-Stack L2)上,JSON-RPC 区块标签 'latest'、'safe' 和 'finalized' 代表不同的最终性阶段。'latest' 是排序器的头部,可能发生重组;'safe' 已提交到不太可能重组的 L1 区块;'finalized' 是不可逆的。对于关键读取(如提款或索引器水位线),请使用 'finalized';对于大多数需要稳定性的应用逻辑,请使用 'safe'。
直接回答:在 Base 上,Safe、Finalized 和 Latest 分别代表什么?
当您查询 Base JSON-RPC 端点时,区块标签 latest、safe 和 finalized 并不总是指向同一个区块。latest 是排序器所见的链头,可能被重组。safe 是其批次已提交到足够老的 L1 区块(被认为不太可能重组)的区块。finalized 是已通过 L1 最终性和 OP-Stack 派生过程完全确认的区块,实际上是不可逆的。本文解释了这些阶段背后的机制,以及如何在您的应用程序中正确使用它们。
有关权威细节,请参阅 Base 文档中的交易生命周期 和 Optimism 文档中的交易最终性。这些是描述确切语义和当前行为的主要来源。
OP-Stack 架构如何产生最终性阶段
Base 是一个 OP-Stack(Optimism)L2。排序器提出区块,并定期将交易批次提交到 L1 合约。L1 合约以及争议游戏和输出根系统决定了 L2 区块何时被视为安全或最终。这个过程是异步的:排序器快速产生区块,但 L1 上的最终性需要时间。
Base 节点暴露的三个头部值为:
latest:排序器产生的最新区块。此区块尚未锚定到 L1,如果排序器重组或发生争议,则可能被重组。
safe:其批次已提交到足够老的 L1 区块(不太可能重组)的区块。这是大多数应用程序“可安全构建”的实际标记。
finalized:已通过 L1 最终性和规范 OP-Stack 派生完全确认的区块。这实际上是不可逆的。
这些阶段的确切深度和时间是文档化的行为,不是固定常量。它们取决于 L1 最终性和 OP-Stack 配置。作为开发人员,您不应假设固定的区块数或秒数;相反,应查询节点以获取当前的 safe 和 finalized 头部。
使用 JSON-RPC 查询 Safe、Finalized 和 Latest
您可以使用标准的 eth_getBlockByNumber 方法并传入区块标签参数来查询每个头部。区块标签可以是字符串,如 'latest'、'safe' 或 'finalized',也可以是 EIP-1898 区块号对象。例如,要获取最新区块:eth_getBlockByNumber('latest', false)。要获取安全区块:eth_getBlockByNumber('safe', false)。要获取最终区块:eth_getBlockByNumber('finalized', false)。
返回的 safe 和 finalized 区块号通常会落后于 latest。差异不是固定常量;它根据 L1 条件和排序器的批量提交计划而变化。您可以使用下面的脚本在您的环境中测量差异。
将 latest 用于不可逆读取是危险的。例如,如果您正在构建一个将最新区块记录为水位线的索引器,重组可能导致您处理一个后来被丢弃的区块。同样,如果您正在构造提款证明,您必须使用在 L1 上已最终确定的区块,而不仅仅是最新的 L2 区块。
可复现的 Node.js 脚本,用于测量最终性前沿
以下脚本连接到 Base 端点,获取最新、安全和最终的区块号,并打印差异。它还会随时间重新采样安全头部,以观察最终性前沿如何推进。将 YOUR_BASE_ENDPOINT 替换为您的实际端点 URL。
使用 Node.js(v18 或更高版本)运行脚本。它使用内置的 fetch API,因此不需要外部依赖。
const endpoint = 'YOUR_BASE_ENDPOINT';
async function rpc(method, params) {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const data = await res.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
async function getHeads() {
const latest = parseInt(await rpc('eth_blockNumber', []), 16);
const safe = parseInt((await rpc('eth_getBlockByNumber', ['safe', false])).number, 16);
const finalized = parseInt((await rpc('eth_getBlockByNumber', ['finalized', false])).number, 16);
return { latest, safe, finalized };
}
async function main() {
console.log('Sampling Base finality heads...');
const samples = [];
for (let i = 0; i < 5; i++) {
const heads = await getHeads();
samples.push(heads);
console.log(`Sample ${i+1}: latest=${heads.latest}, safe=${heads.safe}, finalized=${heads.finalized}, behind_safe=${heads.latest - heads.safe}, behind_finalized=${heads.latest - heads.finalized}`);
await new Promise(resolve => setTimeout(resolve, 5000)); // wait 5 seconds
}
console.log('\nFill in the table below with your observed values:');
console.log('| Sample | latest | safe | finalized | behind_safe | behind_finalized |');
console.log('|--------|--------|------|-----------|-------------|------------------|');
samples.forEach((s, i) => {
console.log(`| ${i+1} | ${s.latest} | ${s.safe} | ${s.finalized} | ${s.latest - s.safe} | ${s.latest - s.finalized} |`);
});
}
main().catch(err => { console.error(err); process.exit(1); });预期输出和填写结果表格
当您运行脚本时,您将看到类似于以下的输出(实际数字因网络条件和端点而异):
在下面的表格中记录您自己的观察结果。这些值特定于环境,并会随时间变化。此方法允许您验证所选端点上的最终性行为。
- | Sample | latest | safe | finalized | behind_safe | behind_finalized |
- |--------|--------|------|-----------|-------------|------------------|
- | 1 | | | | | |
- | 2 | | | | | |
- | 3 | | | | | |
- | 4 | | | | | |
- | 5 | | | | | |
Sample 1: latest=12345678, safe=12345670, finalized=12345660, behind_safe=8, behind_finalized=18何时使用哪个头部?
选择正确的区块标签取决于您的用例。以下是一个决策指南:
- 使用
finalized用于:不可逆操作,例如构造提款证明、完成跨域转账或记录永久索引器水位线。这确保您永远不会构建在可能被重组的区块上。
- 使用
safe用于:大多数需要稳定链视图的应用逻辑,例如读取账户余额、执行依赖状态的智能合约调用,或构建不应被重组的交易。safe是需要一致性的读取的推荐默认值。
- 使用
latest用于:实时监控、可容忍重组的面向用户的交易状态,或当您需要最新区块并了解重组风险时。
对于关键金融操作,始终优先使用 finalized 或 safe 而不是 latest。例如,如果您正在构建一个在 Base 上锁定资产的桥,您应该等待 finalized 再考虑存款不可逆。
常见陷阱及如何避免
开发人员在处理 L2 最终性时经常犯错。以下是最常见的陷阱和修复方法:
- 将
latest视为最终:这是最常见的错误。latest可能重组。对于任何必须永久的内容,始终使用safe或finalized。 - 假设固定的重组深度:
latest和safe之间的差异不是常量。它取决于 L1 条件和排序器批量提交。在您的环境中测量它,而不是硬编码一个值。 - 使用 L2 收据进行跨域最终性:Base 上的交易收据并不意味着交易在以太坊上是最终的。跨域提款需要 Optimism 证明窗口,该窗口比 L2 最终性更长。有关详细信息,请参阅 Optimism 文档中的跨域通信。
- 忽略交错推进:
safe和finalized不会同步推进。safe通常比finalized推进得更快。您的应用程序应独立处理两者。 - 不使用 EIP-1898 查询特定区块:如果您需要查询特定区块号,请使用 EIP-1898 对象格式,例如
{ blockNumber: '0x...' },以避免歧义。
局限性和权衡
虽然 safe 和 finalized 提供了更强的保证,但它们也有权衡。safe 和 finalized 区块落后于 latest,因此使用它们意味着您的应用程序会看到稍微延迟的链视图。对于大多数用例,这种延迟是可以接受的,但对于价格馈送或实时仪表板等实时应用程序,您可能需要使用 latest 并优雅地处理重组。
此外,safe 和 finalized 的确切语义可能会随着 OP-Stack 的变化而演变。始终参考官方文档以获取最新行为。Base 文档 和 Optimism 文档 是权威来源。
最后,请注意,此处描述的行为特定于像 Base 这样的 OP-Stack L2。其他 L2(例如 Arbitrum、zkSync)具有不同的最终性模型。不要假设相同的区块标签在不同网络上意味着相同的事情。
后续步骤和进一步阅读
既然您了解了 Base 最终性,您可以将此知识应用于构建更健壮的应用程序。有关 Base RPC 使用的更多详细信息,请探索以下资源:
- Base 网络概述 - 了解 Base 的架构和功能。
- Base RPC 延迟 - 了解选择端点时的延迟注意事项。
- Base WebSocket RPC - 使用 WebSocket 进行实时更新。
- Base 速率限制和可靠性 - 确保您的应用程序处理速率限制。
- 通过 RPC 查询 Base 历史状态 - 使用归档节点访问历史数据。
- Base RPC 节点(RPC 助手) - 查找并配置 Base RPC 端点。
- OnFinality 学习中心 - 探索更多教程和指南。
- RPC 定价 - 了解 RPC 服务的定价。
- API 服务 - 了解 OnFinality 的 API 产品。