当节点跟随的链切换到另一条分支时,以太坊会发生区块重组,导致先前被视为规范的区块失效。从单个 RPC 端点检测重组需要以区块哈希为键,并验证父哈希链接,而不能将区块号视为身份标识。以太坊 JSON-RPC 规范定义了 eth_getBlockByNumber 和 eth_getBlockByHash,两者都返回包含 parentHash 字段的区块对象,该字段将区块链接到其前驱。有界检测器维护一个近期(高度、哈希、父哈希)行的滚动窗口,每次轮询重新读取头部,并向后遍历直到存储的哈希与观察到的 parentHash 匹配,将替换的区块数量报告为重组深度。safe 和 finalized 标签提供更强的保证,但它们是共识层判断,并非绝对承诺。任何观察到的重组计数都是该端点视图的样本,而非网络的真实重组率。
重组机制及为何区块号不是身份标识
区块链重组(reorg)发生在节点从一条链分支切换到另一条累积工作量更大(或在权益证明以太坊中,证明权重更大)的分支时。旧分支下的规范区块变为非规范区块,任何从这些区块派生的状态都可能无效。以太坊 JSON-RPC 规范将区块描述为包含 parentHash 字段的对象,该字段通过密码学方式将每个区块链接到其前驱,形成一条链。区块号仅仅是链中某个位置的标签,而非唯一身份标识。两个不同的区块可以占据竞争分支上的同一高度。
由于区块号是位置标签,仅存储最新区块号并假设该高度上的区块稳定的检测器将完全错过重组。正确的方法是将区块哈希视为身份标识,并通过遍历父哈希链接验证该哈希是否仍可从当前头部到达。这是可靠重组检测背后的核心原则,且独立于任何特定的 RPC 提供商。有关以太坊 RPC 方法的更广泛介绍,请参阅 RPC 端点指南(RPC Assistant)。
- 只有当节点跟随的链包含某个区块时,该区块才是规范的。
- 区块号是位置,不是身份标识;哈希才是身份标识。
- 父哈希链接是检测链切换的主要信号。
- 重组后,分叉点以下的派生状态将失效。
父哈希链接作为主要检测信号
以太坊 JSON-RPC 规范指出,eth_getBlockByNumber 返回给定高度的区块对象,eth_getBlockByHash 返回给定哈希的区块对象,如果节点不知道该区块则返回 null。两个对象都包含 parentHash 字段。要检测重组,请重新请求高度 N 处存储的哈希,并将其与当前位于高度 N+1 的区块的 parentHash 进行比较。如果为 N 存储的哈希不再作为 N+1 的父哈希出现,则链已切换。这种比较是基本的重组信号。
一个微妙但关键的点:eth_getBlockByHash 在节点仍持有该区块时返回它,无论它是否规范。一个可解析的哈希不一定是规范的哈希。规范性的测试始终需要从头部向后遍历。这一区别在 eth_getBlockByHash 的以太坊 JSON-RPC 规范 中有文档说明,对于正确的检测器逻辑至关重要。
- 将高度 N 处存储的哈希与高度 N+1 处区块的 parentHash 进行比较。
- 可解析的哈希并不能证明其规范性。
- 只有从头部向后遍历才能确定规范性。
- 对于未知区块,eth_getBlockByNumber 和 eth_getBlockByHash 都返回 null。
Safe 和 Finalized 标签:文档化保证及其限制
以太坊 JSON-RPC 规范定义了区块标签,包括 latest、safe、finalized 和 pending。finalized 标签是节点能提供的最强保证,是进行不可逆结算的合适锚点。safe 标签是较弱的中间状态,在某些网络条件下仍可能发生重组。两者都是节点根据其对链的视图暴露的共识层判断,而非在该深度永远不会发生重组的绝对承诺。
对于高频应用,仅依赖 finalized 进行所有操作可能不切实际,因为最终性需要时间。许多系统使用基于观察到的重组统计数据的确认深度策略,而不是等待最终性。这是在结算速度与安全性之间的权衡。有关最终性如何与重组相互作用的详细讨论,请参阅 eth_getBlockByNumber 的以太坊 JSON-RPC 规范。
- finalized 是最强标签,但仍是共识层判断。
- safe 较弱,在某些条件下可能被重组。
- 确认深度策略在速度与安全性之间取得平衡。
- 这些标签的提供商行为有文档记录,但因提供商而异。
使用滚动窗口构建有界重组检测器
实用的重组检测器维护一个近期(高度、哈希、父哈希)行的滚动窗口。每次轮询时,它重新读取头部区块,并向后遍历存储的窗口,将每个高度处存储的哈希与下一个高度处区块的 parentHash 进行比较。当存储的哈希与观察到的 parentHash 匹配时,遍历停止。被替换的区块数量即为重组深度,遍历停止的高度即为分叉点。
这种有界方法避免了内存无限增长,并保持检测器的响应性。窗口大小应至少与您打算执行的确认策略一样深。有关协调区块和交易的补充视角,请参阅 逐块 EVM 索引器对账。
- 维护(高度、哈希、父哈希)行的滚动窗口。
- 每次轮询重新读取头部并向后遍历,直到哈希匹配。
- 重组深度 = 被替换的区块数量;分叉点 = 遍历停止的高度。
- 窗口大小应超过您预期的确认深度。
可运行的 Node.js 检测器及每次轮询输出
以下 Node.js 脚本连接到单个以太坊 JSON-RPC 端点,轮询头部区块,并通过父哈希链接检测重组。它输出每次轮询的结果,包括头部编号、头部哈希、观察到的深度、分叉点高度和被替换的区块。它还维护一个累积日志,以便您积累自己测量的重组统计数据。将 RPC_URL 替换为您从 OnFinality 以太坊网络页面 获取的端点。
该脚本使用 eth_getBlockByNumber 和 latest 标签获取头部,并在向后遍历时使用 eth_getBlockByHash 按哈希检索区块。它存储最近 WINDOW_SIZE 个区块的滚动窗口。每次轮询时,它将每个高度处存储的哈希与下一个高度处区块的 parentHash 进行比较。如果发现不匹配,则报告重组深度和分叉点。
const { ethers } = require('ethers');
const RPC_URL = 'https://your-endpoint.onfinality.io'; // replace with your endpoint
const WINDOW_SIZE = 64; // number of recent blocks to track
const POLL_INTERVAL_MS = 12000; // ~1 block time
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Rolling window: array of { height, hash, parentHash }
let window = [];
let cumulativeReorgs = [];
async function fetchBlockByNumber(height) {
const block = await provider.send('eth_getBlockByNumber', [
'0x' + height.toString(16),
false
]);
if (!block) return null;
return {
height: parseInt(block.number, 16),
hash: block.hash,
parentHash: block.parentHash
};
}
async function fetchBlockByHash(hash) {
const block = await provider.send('eth_getBlockByHash', [hash, false]);
if (!block) return null;
return {
height: parseInt(block.number, 16),
hash: block.hash,
parentHash: block.parentHash
};
}
async function poll() {
const head = await fetchBlockByNumber('latest');
if (!head) {
console.log('Failed to fetch head block');
return;
}
// Add head to window if new
if (window.length === 0 || window[window.length - 1].height < head.height) {
window.push(head);
if (window.length > WINDOW_SIZE) window.shift();
}
// Walk backward to find fork point
let forkPoint = null;
let blocksReplaced = 0;
for (let i = window.length - 1; i > 0; i--) {
const stored = window[i - 1];
const current = window[i];
if (stored.hash !== current.parentHash) {
// Mismatch: reorg detected
forkPoint = stored.height;
blocksReplaced = window.length - i;
break;
}
}
if (forkPoint !== null) {
console.log(`REORG DETECTED: head=${head.height} hash=${head.hash}`);
console.log(` fork point height: ${forkPoint}`);
console.log(` blocks replaced: ${blocksReplaced}`);
cumulativeReorgs.push({ timestamp: Date.now(), forkPoint, blocksReplaced });
// Rewind window to fork point
window = window.filter(b => b.height <= forkPoint);
} else {
console.log(`No reorg. head=${head.height} hash=${head.hash}`);
}
console.log(`Cumulative reorgs observed: ${cumulativeReorgs.length}`);
}
setInterval(poll, POLL_INTERVAL_MS);
poll();测量重组深度和分叉点分布
在长时间观察期内运行检测器会产生分叉点和重组深度的分布。该分布是选择确认深度的经验基础。较短的观察窗口只能得到该端点视图的样本,而非对网络真实重组率的估计。要对网络范围的重组频率做出断言,您需要较长的观察期,最好使用多个独立端点。
使用以下结果表模板记录您自己的测量数据。用检测器的数据填写。不要依赖博客文章中的引用数字;针对您自己的端点进行测量。有关比较端点的方法,请参阅 多端点 RPC 一致性与头部延迟。
- 结果表列:观察期、端点、总轮询次数、观察到的重组次数、最大深度、中位深度、分叉点高度。
- 记录每次重组事件的时间戳和区块高度。
- 如果可能,比较多个端点的分布。
- 完整分布需要较长的观察期,而非短样本。
根据观察数据选择确认深度
确认深度策略应源自观察到的分叉点分布,而不是从博客文章复制。如果您的检测器在长时间内观察到 99% 的重组深度为 2 个或更少的区块,那么 12 个区块的深度提供了很大的余量。然而,选择还取决于风险价值和延迟结算的成本。更深的确认策略会减慢结算速度,但降低在重组区块上操作的概率。
对于不可逆结算,finalized 标签是最强的锚点。对于高频操作,基于深度的策略可能更实用。关键是根据您自己的测量做出决定,并记录假设。有关检测延迟的相关讨论,请参阅 检测落后于链头的 RPC 节点。
- 根据观察到的分叉点分布推导确认深度。
- 考虑风险价值和结算速度的权衡。
- 尽可能使用 finalized 进行不可逆结算。
- 记录假设并定期重新评估。
索引器补救:重组后回退并重新索引
对于索引器,重组会使分叉点以下的派生状态失效。仅记录重组的检测器是不够的;补救路径必须在触发之前设计好。标准方法是将索引器回退到分叉点,并从那里向前重新索引。这要求索引器能够识别分叉点,并且有办法回滚派生状态,例如数据库事务或版本化记录。
补救路径应定期测试。如果索引器无法回退,它可能在重组后提供过时或不正确的数据。有关对账的详细处理,请参阅 逐块 EVM 索引器对账。像 eth_getBlockReceipts:一次调用批量获取收据 这样的批量检索方法可以加速重新索引。
- 重组会使分叉点以下的派生状态失效。
- 补救:回退到分叉点,向前重新索引。
- 在需要之前设计和测试补救路径。
- 使用批量检索方法加速重新索引。
限制、权衡和提供商差异
没有单个 RPC 端点能提供网络的完整视图。任何观察到的重组计数都是该端点视图的样本,可能不反映网络的真实重组率。提供商在区块标签、缓存和头部传播方面的具体行为各不相同。以太坊 JSON-RPC 规范定义了协议语义,但实现细节因提供商而异,并可能有所不同。
更深的确认策略提高安全性但减慢结算速度。较浅的策略加快结算速度但增加重组风险。最佳深度取决于应用的风险承受能力和观察到的重组分布。没有通用的安全数字。此外,检测器本身会增加端点负载;应调整轮询频率和窗口大小,以平衡检测延迟与资源使用。有关定价考虑,请参阅 RPC 定价。
- 单端点观察是样本,不是网络范围的真相。
- 提供商对标签和缓存的行为各不相同;请查阅文档。
- 更深的确认减慢结算;更浅的确认增加风险。
- 检测器轮询增加负载;调整频率和窗口大小。
常见检测器问题排查
如果检测器报告没有重组,但您怀疑发生了重组,请验证窗口大小是否足够覆盖重组深度。比窗口更深的重组将无法被检测到,因为存储的哈希已经被移出。还要检查端点是否缓存响应;一些提供商会短暂缓存 eth_getBlockByNumber,这可能会掩盖重组。使用 eth_getBlockByHash 进行按哈希寻址的检索,以避免缓存歧义。
如果检测器报告频繁的误报,请确保您比较的是正确的字段。高度 N+1 处区块的 parentHash 应与高度 N 处区块的哈希匹配。高度编号不匹配或轮询之间的竞争条件可能导致虚假结果。还要验证端点是否完全同步;滞后的节点可能提供过时的区块。有关端点选择的帮助,请参阅 API 服务 和 OnFinality Learn 中心。
- 窗口太小:深层重组会被遗漏。
- 提供商缓存可能掩盖重组;使用 eth_getBlockByHash。
- 误报:检查字段比较和高度编号。
- 滞后节点:在信任结果之前验证同步状态。
后续步骤:将重组检测投入运营
要将重组检测投入运营,请将检测器集成到您的监控栈中,并对重组事件发出警报。将每次重组的时间戳、分叉点和深度记录到持久存储中。随着时间的推移,这些数据将为您的确认深度策略提供依据。考虑针对多个端点运行检测器,以比较视图并检测提供商特定的异常。
对于生产系统,将重组检测与稳健的补救路径相结合。定期测试回退和重新索引程序。尽可能使用 finalized 标签进行不可逆结算。有关端点选项,请探索 OnFinality 以太坊网络页面 并查看 RPC 定价,以选择适合您轮询频率的计划。RPC 端点指南(RPC Assistant) 提供了有关端点选择的额外背景信息。
- 将检测器集成到监控中,并对重组发出警报。
- 持久化重组事件以进行长期分布分析。
- 针对多个端点运行检测器以进行比较。
- 定期测试补救程序。