Polkadot 将区块生产(BABE)与最终性(GRANDPA)分离。最终化头块是由验证人权威集 2/3+1 签名认证的不可逆区块,而最佳头块是乐观的,可能被回滚。通过 JSON-RPC,可以使用 chain_getFinalizedHead 和 chain_subscribeFinalizedHeads 跟踪安全头块,并使用 polkadot.js 的 waitForFinalized 轮询直到特定区块被最终化。本文解释了该机制,并提供了一个可运行脚本,用于测量最佳区块与最终化区块之间的差距。
直接回答:为什么必须跟踪最终化头块,而不是最佳头块
当你在 Polkadot 上构建时,绝不能将 chain_getBlock 或 chain_getHead 返回的最新区块视为永久。该区块是 BABE(区块生产引擎)产生的最佳头块,如果分叉成为规范链,它可能会被回滚。唯一不可逆的区块是最终化头块,由 GRANDPA(基于 GHOST 的递归祖先派生前缀协议)认证。通过 JSON-RPC,你可以使用 chain_getFinalizedHead 查询它,使用 chain_subscribeFinalizedHeads 订阅它,并在 polkadot.js 中使用 api.rpc.chain.getFinalizedHead() 或 api.derive.chain.waitForFinalized() 等待特定区块被最终化。本文解释了 GRANDPA 机制,将其映射到 JSON-RPC 方法,并提供了一个可复现的脚本,以在实践中观察差异。
有关 Polkadot 网络和端点的更广泛背景,请参阅 Polkadot 网络页面 和 Polkadot RPC 指南。
GRANDPA 最终性机制:Polkadot 如何实现不可逆性
Polkadot 的共识将区块生产与最终性分离。BABE(区块链扩展的盲分配)在时隙中产生区块,每个时隙随机选举一个区块。这是乐观且快速的,但如果不同的分叉获得更多工作量,区块可能会被取代。GRANDPA 是一个并行运行的最终性小工具:一组有权限的验证人权威对链前缀(GHOST 祖先)进行投票,而不是对单个区块投票。一旦超过三分之二的加权权威集投票支持某个前缀,他们就会生成一个签名的证明,该证明将该前缀不可逆地最终化。这个证明是一个密码学证明,任何轻客户端都可以验证。
关键含义是链有两个“头”概念:最佳头块(来自 BABE 的最多工作量区块)和最终化头块(由 GRANDPA 认证)。对于任何不可逆的效果——付款、索引器水位线或你将采取行动的账户状态——你应该依赖最终化头块。对于推测性 UI(例如,显示可能被回滚的近期交易),最佳头块是可以接受的。这种区别在 Polkadot wiki 关于 GRANDPA 和 GRANDPA 论文 中有记录。
权威集变更跨时代发生。当验证人集合改变时,GRANDPA 使用新的权威集,而先前时代的证明对于历史区块仍然有效。这就是为什么即使在集合改变后,你仍然可以验证旧区块的最终性。
将最终性映射到 JSON-RPC:方法和命名空间
Polkadot 的 JSON-RPC 公开了几种查询最终性的方法。最重要的是:
chain_getFinalizedHead返回最新最终化区块的哈希。这是规范的“可安全信任”标记。
chain_getHead(或带有“latest”参数的chain_getBlock)返回最佳区块,该区块不是最终的。
chain_subscribeFinalizedHeads在区块被最终化时推送最终化区块头的流。
chain_subscribeAllHeads推送最佳和最终化头(可能还有其他),允许你比较它们。
grandpa_*命名空间(例如,grandpa_proveFinality、grandpa_roundState、grandpa_submitGrandpaExtrinsic)提供证明和轮次证据。这些方法可能不会在所有公共端点上公开;请检查你的提供商的文档。
在 polkadot.js 中,api.rpc.chain.getFinalizedHead() 和 api.rpc.chain.subscribeFinalizedHeads() 包装了这些方法。此外,api.derive.chain.waitForFinalized(blockHash) 轮询最终化头,直到给定区块被最终化,如果最终性停滞,则抛出 RpcError 或 TimeoutError。这是在应用程序中等待最终性的推荐方式。
有关 RPC 方法和端点选择的更深入探讨,请参阅 Polkadot RPC 指南 和 Polkadot WebSocket RPC 指南。
实际示例:测量最佳区块与最终化区块的差距
要实际查看最佳区块和最终化区块之间的差异,请运行以下 Node.js 脚本。它订阅 allHeads 和 finalizedHeads,在 60 秒间隔内打印区块号差距,然后使用 waitForFinalized 确认特定区块已最终化。你需要一个 Polkadot 端点(例如 wss://rpc.polkadot.io)和 @polkadot/api 包。
将 YOUR_ENDPOINT 替换为你自己的端点。如果你使用 OnFinality 的 API 服务,你将获得一个专用端点,可以访问所有 RPC 方法。请注意,公共端点可能有速率限制;有关详细信息,请参阅 RPC 定价。
// 保存为 finality-check.js
// 运行:node finality-check.js
const { ApiPromise, WsProvider } = require('@polkadot/api');
const WS_URL = process.env.WS_URL || 'wss://rpc.polkadot.io';
const INTERVAL_MS = 60000; // 60 秒
async function main() {
const provider = new WsProvider(WS_URL);
const api = await ApiPromise.create({ provider });
console.log('已连接到', WS_URL);
// 订阅所有头(最佳和最终化)
const unsubAll = await api.rpc.chain.subscribeAllHeads((header) => {
console.log(`所有头: #${header.number} hash=${header.hash}`);
});
// 订阅最终化头
const unsubFinalized = await api.rpc.chain.subscribeFinalizedHeads((header) => {
console.log(`最终化头: #${header.number} hash=${header.hash}`);
});
// 等待间隔
await new Promise(resolve => setTimeout(resolve, INTERVAL_MS));
// 获取当前最佳和最终化
const best = await api.rpc.chain.getHead();
const finalized = await api.rpc.chain.getFinalizedHead();
const bestHeader = await api.rpc.chain.getHeader(best);
const finalizedHeader = await api.rpc.chain.getHeader(finalized);
console.log(`\n${INTERVAL_MS/1000} 秒后:`);
console.log(`最佳区块: #${bestHeader.number}`);
console.log(`最终化区块: #${finalizedHeader.number}`);
console.log(`差距: ${bestHeader.number - finalizedHeader.number} 个区块`);
// 等待特定区块被最终化(例如,我们刚刚看到的最佳区块)
try {
const finalizedHash = await api.derive.chain.waitForFinalized(best);
console.log(`区块 #${bestHeader.number} 已最终化,哈希为 ${finalizedHash}`);
} catch (e) {
console.error('waitForFinalized 失败:', e.message);
}
// 清理
await unsubAll();
await unsubFinalized();
await api.disconnect();
}
main().catch(console.error);预期输出和结果表
当你运行脚本时,你会看到一系列头。最终化头通常会落后于最佳头几个区块,但如果验证人离线或网络拥塞,差距可能会增大。具体数字因网络条件而异,并非固定。用你的观察填写下表,以描述你的端点的行为。
注意:脚本在间隔结束时对最佳区块使用 waitForFinalized。如果最终性缓慢,这可能会超时。超时在 polkadot.js 中是可配置的;默认情况下,它可能会在一段时间后抛出。
- 在间隔期间的几个时间点记录最佳区块号和最终化区块号。
- 计算差距(最佳 - 最终化)并注意任何变化。
- 如果你使用自定义端点,请与公共端点比较差距,以查看最终性传播是否存在差异。
- 如果你看到较大的差距(例如,> 10 个区块),请调查网络是否处于压力之下,或者你的端点是否跟不上。
| 时间(秒) | 最佳区块 | 最终化区块 | 差距 |
|----------|------------|-----------------|--------|
| 0 | | | |
| 15 | | | |
| 30 | | | |
| 45 | | | |
| 60 | | | |失败与修复清单:当最终性停滞或滞后时
最终性滞后是正常的,但最终化头长时间不前进表明存在问题。以下是诊断和处理问题的清单:
- 检查最佳头是否在前进:如果最佳头也停滞,则节点可能已断开连接或网络已关闭。验证你的 WebSocket 连接,并尝试另一个端点。
- 检查权威集健康状况:如果验证人离线,GRANDPA 无法最终化。你可以查询
grandpa_roundState(如果可用)以查看当前轮次和投票。公共端点可能不会公开此方法;如果需要,请使用 OnFinality 的 API 服务 提供的专用端点。
- 决定是否基于“最佳”行动:如果最终性停滞,你必须决定是否继续基于最佳头行动。对于不可逆操作,更安全的做法是暂停,直到最终性恢复。对于推测性 UI,你可以继续,但明确指示区块不是最终的。
- 使用带超时的
waitForFinalized:在你的应用程序中,等待最终性时始终设置合理的超时。如果超时,请记录错误并提醒你的团队。
- 监控证明可用性:如果需要证明最终性,请使用
grandpa_proveFinality获取证明。请注意,完整节点可能会修剪历史证明;归档节点可能会保留它们。有关更多信息,请参阅 通过 RPC 查询 Polkadot 历史状态。
- 了解权威集变更:当验证人集合改变时,最终性可能会短暂暂停。这是正常的,应在几轮内解决。
最终性查询的限制和权衡
虽然 JSON-RPC 方法是标准的,但需要注意一些限制:
- 端点可用性:并非所有公共端点都公开
grandpa_*方法。如果需要,请使用提供商(如 OnFinality 的 API 服务)提供的专用端点。
- 历史证明修剪:完整节点可能会修剪旧的证明以节省空间。归档节点更有可能保留它们,但成本更高。请检查你的节点配置。
- 最终性延迟是可变的:最终性时间取决于网络条件、验证人表现和权威数量。不要假设固定的延迟;始终为最终最终性设计。
waitForFinalized可能抛出异常:如果区块从未被最终化(例如,由于分叉),promise 可能会拒绝。请优雅地处理错误。
- 隐私和速率限制:公共端点可能会限制订阅速率。对于生产环境,请使用具有更高限制的专用端点。有关选项,请参阅 RPC 定价。
有关与其他最终性模型的比较,请参阅 OP-Stack 最终性阶段文章。
后续步骤:构建可靠的 Polkadot 集成
既然你了解了 GRANDPA 最终性以及如何查询它,你就可以构建更可靠的集成。首先,对于你打算采取行动的任何状态,使用 chain_getFinalizedHead,并订阅 chain_subscribeFinalizedHeads 以获取实时更新。当你需要确认特定交易不可逆时,使用 waitForFinalized。
如需进一步阅读,请浏览 OnFinality Learn 中心 获取有关 Polkadot 和其他网络的更多指南。如果你担心延迟,请参阅 Polkadot RPC 延迟。有关 WebSocket 最佳实践,请参阅 Polkadot WebSocket RPC。有关历史状态查询,请参阅 通过 RPC 查询 Polkadot 历史状态。
如果你需要能够访问所有 RPC 方法的生产级端点,请考虑 OnFinality 的 API 服务。我们提供高可用性和低延迟的专用端点,以便你可以专注于构建而不是管理基础设施。