Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查阅读约 13 分钟

大规模使用 BSC Parity Trace:trace_filter、trace_block 与区块范围限制

面向 BNB Smart Chain 上 Parity 风格 trace 命名空间的生产实践指南,涵盖 trace_filter、trace_block、能力探测、游标持久化与重组修复。

TL;DR

BNB Smart Chain 上的 Parity 风格 trace 命名空间提供 trace_filter、trace_block、trace_transaction 和 trace_get,各自回答关于内部调用、价值转移、合约创建和 selfdestruct 的不同问题。由于单个 BSC 区块可能包含数千个 trace 对象,一个较宽的 trace_filter 范围在负载上远重于等价的 eth_getLogs 范围,通常会被服务商限制上限或超时。该命名空间是可选的,因此许多公共端点在你探测能力之前会返回 JSON-RPC method-not-found(-32601)。本文介绍如何检测 trace 支持、以持久化游标分页固定区块窗口、按 (blockNumber, transactionHash, traceAddress) 去重、在范围过大错误时退避,以及在链重组后修复最后几个区块。文章还将有文档记载的 OpenEthereum trace 行为与服务商特定限制区分开来,并提供一张结果表供你针对自己的端点填写。

BNB Smart Chain 上的 trace 命名空间方法集

BNB Smart Chain 继承了 Parity/OpenEthereum 的 trace 命名空间,这是一组报告执行层事件而非收据层日志的方法。你最常使用的四个方法是 trace_filter、trace_block、trace_transaction 和 trace_get。每个方法回答不同的问题,选错方法会得到静默不完整的答案,而不是报错。

trace_filter 接受一个区块范围以及可选的 fromAddress 和 toAddress 过滤器,返回匹配的 trace。trace_block 返回单个区块中的全部 trace。trace_transaction 返回一笔交易的全部 trace,trace_get 则通过交易哈希和 traceAddress 路径返回单条 trace。这些方法及其过滤器和输出字段的历史来源是 OpenEthereum trace 模块文档(openethereum.github.io)。

以太坊 JSON-RPC 规范(ethereum.org/en/developers/docs/apis/json-rpc)定义了与该命名空间并列的标准 eth_* 方法,而 geth debug 命名空间(geth.ethereum.org)则涵盖逐笔交易的调用树。如果你要在 trace_transaction 和 debug_traceTransaction 之间做选择,请参阅 trace_transaction 与 debug_traceTransaction 及 trace_call 对比

trace 命名空间本身并不属于基础以太坊 JSON-RPC 方法集;它源自 OpenEthereum 客户端,其 trace 模块文档 至今仍是 trace_filter、trace_block 及其输出字段的参考。与它并列的普通方法则由 以太坊 JSON-RPC 规范 定义。应结合两者阅读,因为规范解释了标准接口,而 trace 模块记录了可选接口。

  • trace_filter —— 区块范围加可选的 fromAddress/toAddress;返回匹配的外部可见 trace。
  • trace_block —— 单个区块中的全部 trace;适合面向区块的索引。
  • trace_transaction —— 一笔交易哈希对应的全部 trace。
  • trace_get —— 通过交易哈希和 traceAddress 路径获取单条 trace。

trace 与日志的区别,以及为什么 BSC 区块负载很重

日志由合约发出并存储在收据中;trace 则记录每一次内部调用、价值转移、合约创建和 selfdestruct,每一项都带有调用类型和描述其在调用树中位置的 traceAddress 路径。这意味着即使一个 BSC 区块只包含几百笔交易,也可能产生数千个 trace 对象,因为每笔交易都可能展开为许多内部调用。

实际后果是,一个较宽的 trace_filter 范围在负载上远重于等价的 eth_getLogs 范围。同一个区块窗口返回的日志载荷可能尚可管理,但返回的 trace 对象可能多出一个数量级,因此对日志成功的范围对 trace 可能被限制上限或超时。日志的分页策略仍然适用,但窗口要更小;日志专属版本见 大规模扫描 BSC 日志:eth_getLogs 范围限制

由于 trace_filter 只覆盖外部可见 trace,内部调用细节需要 debug_traceTransaction。如果你的问题是“这个地址在顶层做了什么”,trace_filter 是合适的工具;如果是“这笔交易内部发生了什么”,则需要 debug 系列。

为什么 trace 支持因端点而异,以及如何探测

trace 命名空间是可选的。许多公共端点并未启用它,在此类端点上调用 trace_filter 会返回 JSON-RPC method-not-found 错误,代码为 -32601,如 JSON-RPC 2.0 规范(jsonrpc.org/specification)所定义。这不是暂时性故障;重试无济于事。客户端在依赖该命名空间之前必须先探测能力。

探测方式是对最近区块做一次开销很小的 trace_block 调用,或使用单区块范围的 trace_filter。如果响应是结果数组,说明命名空间已启用。如果是代码为 -32601 的错误对象,说明该端点不暴露 trace,你应故障转移到另一个端点。命名空间错误的一般模式见 RPC method not found (-32601) 与端点命名空间

有文档记载的 OpenEthereum 行为定义了方法语义;但某个服务商是否启用该命名空间、限制多大的范围,则因服务商而异。应将能力和上限视为服务商特定,并通过实测而非假设来确定。

async function probeTraceSupport(url) {
  const body = {
    jsonrpc: '2.0',
    id: 1,
    method: 'trace_block',
    params: ['latest']
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await res.json();
  if (json.error && json.error.code === -32601) {
    return { supported: false, reason: 'trace namespace not enabled' };
  }
  if (json.error) {
    return { supported: false, reason: json.error.message };
  }
  return { supported: true, sample: json.result.length };
}

大区块范围的分页策略

安全的模式是固定区块窗口加持久化游标。选择一个窗口大小,对 [cursor, cursor + window - 1] 请求 trace_filter,处理结果,然后将游标推进到 cursor + window。每个窗口成功后持久化游标,这样中断后可从最后完成的区块恢复,而不是从头开始。

按元组 (blockNumber, transactionHash, traceAddress) 去重。由于 traceAddress 是路径数组,同一笔交易中的两条 trace 可能共享交易哈希但路径不同;该元组才是稳定标识。如果在超时后重试某个窗口,去重可防止重复计数。

遇到范围过大或超时错误时,将窗口减半并重试同一游标。若反复失败,则指数退避,并考虑切换到上限更大的服务商。这与 eth_getLogs 的做法类似,但窗口更小,以应对每区块更大的 trace 载荷。与区块头对账的方法见 逐块 EVM 索引器对账

  • 固定窗口、持久化游标,仅在窗口成功后推进。
  • 去重键:(blockNumber, transactionHash, traceAddress)。
  • 范围过大:窗口减半,重试同一游标。
  • 反复失败:指数退避,然后故障转移。

使用 trace_filter 进行面向地址的扫描

当问题是“这个地址做了什么”时,带 fromAddress 或 toAddress 的 trace_filter 无需下载链上全部 trace 即可回答。过滤器在服务端应用,因此响应只包含该地址作为外部可见调用或转移的发送方或接收方出现的 trace。

地址是不区分大小写的十六进制。校验和格式或被截断的字符串会静默匹配不到任何内容,返回空结果而非错误。发送前应规范化为小写,并在客户端校验长度和十六进制字符,使格式错误的过滤器在你的代码中显式失败,而不是在端点处悄无声息地失败。

由于 trace_filter 只覆盖外部 trace,仅作为内部调用目标出现的地址不会显示。如果需要内部出现记录,必须退回到逐笔交易的 debug_traceTransaction,其开销大得多,应仅用于定向查询。

function normalizeAddress(addr) {
  if (typeof addr !== 'string') throw new Error('address must be a string');
  const hex = addr.toLowerCase();
  if (!/^0x[0-9a-f]{40}$/.test(hex)) {
    throw new Error('malformed address: ' + addr);
  }
  return hex;
}

async function scanAddress(url, address, fromBlock, toBlock) {
  const body = {
    jsonrpc: '2.0',
    id: 1,
    method: 'trace_filter',
    params: [{
      fromBlock: '0x' + fromBlock.toString(16),
      toBlock: '0x' + toBlock.toString(16),
      fromAddress: [normalizeAddress(address)]
    }]
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

可运行的 Node.js 分页器(带游标持久化)

下面的分页器会检测 trace 命名空间支持、分页请求 trace_filter 窗口、将游标持久化到 JSON 文件,并在中断后恢复。遇到范围过大错误时将窗口减半,反复失败时退避。请针对你自己的端点运行,并将 WINDOW 调整为你的服务商接受的最大值。

游标文件存储下一个要请求的区块。重启时,分页器读取它并继续。去重由以标识元组为键的 Set 处理,因此重试的窗口不会重复计数。这是一个最小参考实现;生产代码应添加结构化日志,并为始终无法成功的窗口设置死信队列。

const fs = require('fs');
const ENDPOINT = process.env.BSC_RPC_URL;
const CURSOR_FILE = './trace-cursor.json';
const START = Number(process.env.START_BLOCK || 0);
const END = Number(process.env.END_BLOCK || 0);
let WINDOW = Number(process.env.WINDOW || 50);

function loadCursor() {
  if (fs.existsSync(CURSOR_FILE)) {
    return JSON.parse(fs.readFileSync(CURSOR_FILE, 'utf8')).next;
  }
  return START;
}

function saveCursor(next) {
  fs.writeFileSync(CURSOR_FILE, JSON.stringify({ next }));
}

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 })
  });
  return res.json();
}

async function traceWindow(from, to) {
  return rpc('trace_filter', [{
    fromBlock: '0x' + from.toString(16),
    toBlock: '0x' + to.toString(16)
  }]);
}

async function main() {
  const probe = await rpc('trace_block', ['latest']);
  if (probe.error && probe.error.code === -32601) {
    throw new Error('trace namespace not enabled on this endpoint');
  }
  const seen = new Set();
  let cursor = loadCursor();
  while (cursor <= END) {
    const to = Math.min(cursor + WINDOW - 1, END);
    const json = await traceWindow(cursor, to);
    if (json.error) {
      if (/range|too large|timeout/i.test(json.error.message)) {
        WINDOW = Math.max(1, Math.floor(WINDOW / 2));
        console.warn('shrinking window to', WINDOW);
        continue;
      }
      throw new Error(json.error.message);
    }
    for (const t of json.result) {
      const key = t.blockNumber + ':' + t.transactionHash + ':' + JSON.stringify(t.traceAddress);
      if (seen.has(key)) continue;
      seen.add(key);
      // process(t)
    }
    cursor = to + 1;
    saveCursor(cursor);
    console.log('processed through block', to, 'traces', json.result.length);
  }
}

main().catch((e) => { console.error(e); process.exit(1); });

针对你自己的端点填写的结果表

服务商的上限和 trace 可用性各不相同,因此应针对你实际使用的端点进行实测。运行上面的探测和分页器,然后记录以下数值。不要假设其他服务商的数字适用于你。

从小窗口开始,逐步增大,直到遇到范围过大或超时错误,然后记录最后一次成功的值。在繁忙区块高度和空闲区块高度各重复一次,因为每区块的 trace 数量随网络活动而变化。

  • 是否支持 trace:是 / 否(来自 -32601 探测)。
  • trace_filter 最大成功范围:___ 个区块。
  • 每区块 trace 数(繁忙高度):___。
  • 每区块 trace 数(空闲高度):___。
  • 成功前的重试次数:___。
  • 退避后的窗口大小:___ 个区块。

trace_filter 和 trace_block 故障排查

-32601 错误意味着该端点未启用命名空间。它不是暂时性的;应故障转移到暴露 trace 的端点。请确认错误代码而非消息,因为消息因服务商而异。

范围过大或超时错误意味着窗口超过服务商上限,或响应构建耗时过长。将窗口减半并重试同一游标。如果窗口缩小到单个区块仍持续报错,该区块本身可能过重;可考虑对该高度使用 trace_block,或改用上限更大的服务商。

trace_filter 返回空结果通常意味着地址过滤器格式错误。地址是不区分大小写的十六进制,因此校验和格式或被截断的字符串会静默匹配不到任何内容。发送前规范化为小写,并校验 0x 前缀加 40 个十六进制字符的形式。

对于最后几个区块,链重组可能使你已处理的 trace 失效。应同时记录区块哈希和区块号,一旦哈希不匹配,就重新请求该区块及之后的所有区块。对账模式见 逐块 EVM 索引器对账

  • -32601:命名空间未启用;故障转移,不要重试。
  • 范围过大:窗口减半,重试同一游标。
  • 空结果:规范化并校验地址过滤器。
  • 重组:比较区块哈希,从分叉点重新请求。

trace 命名空间的局限与权衡

trace 存储比收据存储更重,因此服务商可能在归档节点上裁剪 trace,或在公共端点上完全禁用该命名空间。有文档记载的 OpenEthereum 行为定义了方法语义,但某个服务商是否保留 trace、保留多久,则因服务商而异。

trace_filter 只覆盖外部可见 trace。内部调用细节需要 debug_traceTransaction,其开销更大,且通常受到更严格的速率限制。如果你的问题需要完整调用树,应为 debug 系列预留预算,而不是试图从 trace_filter 输出重建。

宽范围是主要的运维风险。即使在接受宽范围的端点上,大型 trace_filter 响应也可能构建缓慢、传输量大,因此使用较小窗口加持久化游标比一次大请求更可靠。关于端点可靠性和超时行为,见 BNB Smart Chain RPC 可靠性与超时

生产环境 trace 索引的后续步骤

首先探测你打算使用的端点上的 trace 支持,然后用你自己的测量结果填写结果表。选择一个在繁忙高度(而不仅是空闲高度)也能可靠成功的窗口大小,并持久化游标,使重启成本低廉。

如果你需要可用的托管 BSC 端点并提供 trace 命名空间,请查看 BNB Smart Chain RPC 端点(RPC Assistant)BNB Smart Chain 网络页面。关于容量规划和成本,见 RPC 定价API 服务。更多 RPC 故障排查指南汇集在 OnFinality Learn 中心

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始