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

以太坊交易追踪:trace_transaction 与 debug_traceTransaction 及 trace_call

理解以太坊的 trace 和 debug RPC 系列,何时使用 trace_transaction 与 debug_traceTransaction,以及如何运行它们。

TL;DR

以太坊提供两个不同的 RPC 系列用于执行追踪:'trace' 命名空间(trace_transaction、trace_call、trace_block)和 'debug' 命名空间(debug_traceTransaction、debug_traceCall)。它们在输出格式、追踪器选择和客户端支持方面有所不同。对于结构化、OpenEthereum 风格的追踪,并内置回滚原因和状态差异,选择 trace_transaction;对于灵活、基于追踪器的输出,选择带 callTracer 的 debug_traceTransaction。两者都会针对历史状态重新执行 EVM,因此比 eth_call 重得多。可用性和成本因提供商而异,因此请始终在你的端点上验证。

直接回答:你应该使用哪个追踪 RPC?

当你需要了解已挖出的以太坊交易内部发生了什么——为什么回滚、进行了哪些内部调用、gas 如何消耗——你有两个主要的 RPC 系列:trace_* 方法(来自 OpenEthereum/Parity trace 模块,现已被多个客户端支持)和 debug_* 方法(来自 Geth 的 debug 命名空间)。简短回答:当你想要一个结构化、带有固定格式(action/result)和内置回滚原因的追踪时,使用 trace_transaction;当你需要输出格式的灵活性或访问操作码级细节时,使用带 callTracer 等追踪器的 debug_traceTransaction。对于模拟尚未发送的交易,使用 trace_call(可选状态覆盖)或 debug_traceCall。两个系列都会重新执行 EVM,因此计算成本高昂,且常受提供商限制。在构建管道之前,务必检查你的端点支持哪些方法。

本文解释了两个系列背后的机制,比较了它们的输出,并提供了一个可复现的 Node.js 脚本,用于测试你自己的端点。有关以太坊 RPC 的更广泛概述,请参阅 OnFinality Learn 中心以太坊网络页面

EVM 追踪的底层工作原理

trace_transactiondebug_traceTransaction 都是通过在特定状态下,在 EVM 实例中重新执行目标交易来工作的。对于已挖出的交易,该状态是交易所在区块的历史状态——这需要归档节点或具有足够历史状态的节点。对于 trace_calldebug_traceCall,状态是当前最新区块(或指定区块)加上可选的状态覆盖,允许你在执行前修改余额、代码或存储。

在重新执行期间,EVM 记录每个执行的操作码、每个内部消息调用、合约创建和状态更改。区别在于原始数据的打包方式。debug_* 命名空间返回追踪器的输出——追踪器是观察 EVM 执行的代码。默认追踪器是结构记录器,生成冗长的逐操作码日志。更常见的是,你指定 callTracer 来获取结构化的调用树,或 prestateTracer 来捕获执行前的状态。另一方面,trace_* 命名空间具有由 OpenEthereum trace 规范定义的固定输出模式:每个追踪都是一个对象,包含 actionresultsubtracestraceAddress,涵盖调用、创建和自毁操作。它还提供 trace_filter 等方法,按地址或区块范围检索追踪,并在某些实现中包含 revertReasonstateDiff

由于追踪会重新执行 EVM,因此比简单的 eth_call 重得多。具体成本取决于交易的复杂性和使用的追踪器。许多提供商默认禁用追踪,或应用严格的速率限制和更长的超时。请务必查阅提供商的文档——例如,RPC 定价API 服务 页面描述了典型层级。有关状态覆盖的深入探讨,请参阅我们的指南 eth_call 状态覆盖与模拟

比较 trace_transaction 和 debug_traceTransaction 的输出

为了说明差异,考虑一个简单的交易,它转移 ETH 并调用一个合约。trace_transaction 返回一个追踪对象数组,每个对象包含 type(call、create、suicide)、action(from、to、value、gas、input)和 result(output、gasUsed)。它还包含 traceAddress 来表示调用树。以下是一个简化示例:

相比之下,带 callTracerdebug_traceTransaction 返回一个表示调用树的嵌套 JSON 对象,包含 typefromtovaluegasgasUsedinputoutputcalls(子调用数组)等字段。它不包含 traceAddress,因为嵌套本身编码了结构。示例:

trace_* 命名空间还提供 trace_filter,按地址或区块范围查询追踪,这对索引器很有用。debug_* 命名空间没有直接的过滤器;你必须追踪单个区块或交易。有关客户端支持的全面比较,请参阅官方 Geth debug API 文档Erigon trace API 文档。可用性因客户端和端点而异,因此请务必验证。

[
  {
    "action": {
      "callType": "call",
      "from": "0x...",
      "gas": "0x7a120",
      "input": "0x...",
      "to": "0x...",
      "value": "0x0"
    },
    "result": {
      "gasUsed": "0x5208",
      "output": "0x"
    },
    "subtraces": 1,
    "traceAddress": [],
    "type": "call"
  },
  {
    "action": {
      "callType": "call",
      "from": "0x...",
      "gas": "0x...",
      "input": "0x...",
      "to": "0x...",
      "value": "0x0"
    },
    "result": {
      "gasUsed": "0x...",
      "output": "0x..."
    },
    "subtraces": 0,
    "traceAddress": [0],
    "type": "call"
  }
]

{
  "type": "CALL",
  "from": "0x...",
  "to": "0x...",
  "value": "0x0",
  "gas": "0x7a120",
  "gasUsed": "0x5208",
  "input": "0x...",
  "output": "0x",
  "calls": [
    {
      "type": "CALL",
      "from": "0x...",
      "to": "0x...",
      "value": "0x0",
      "gas": "0x...",
      "gasUsed": "0x...",
      "input": "0x...",
      "output": "0x..."
    }
  ]
}

何时使用 trace_call 与 debug_traceCall

trace_calldebug_traceCall 用于模拟交易而不将其发送到网络。它们接受一个交易对象(from、to、gas、gasPrice、value、data)和一个可选的区块号或标签。trace_call 的关键优势在于它返回类似于 trace_transaction 的结构化追踪,并且(在某些实现中)通过 stateOverrides 参数支持状态覆盖。这对于模拟合约交互以估算 gas、检查回滚或广播前检查内部调用非常理想。

debug_traceCall 是 debug 命名空间的等效方法。它也接受一个交易对象和一个追踪器参数。使用 callTracer 时,它返回与 debug_traceTransaction 相同的调用树结构。两者之间的选择通常取决于你的提供商支持哪个命名空间。一些提供商只公开其中一个。例如,如果你使用 Geth 节点,debug_traceCall 可用;如果你使用兼容 OpenEthereum 的端点,则应使用 trace_call

模拟时,你也可以使用带状态覆盖的 eth_call,但这只返回输出或回滚原因,不返回内部调用树。有关状态覆盖的详细指南,请参阅 eth_call 状态覆盖与模拟

可复现示例:使用 Node.js 测试你的端点

以下 Node.js 脚本允许你测试你的 RPC 端点支持哪些追踪方法,并在真实交易上比较 trace_transactiondebug_traceTransaction 的输出。它还运行一个带状态覆盖的 trace_call 模拟。你需要一个安装了 axios 库的 Node.js 环境(npm install axios)。将 YOUR_RPC_URL 替换为你的端点 URL,并可选择为状态覆盖测试提供交易哈希和合约地址。

该脚本执行三个请求:(1) 对给定哈希执行 trace_transaction,(2) 对同一哈希执行带 callTracerdebug_traceTransaction,(3) 对合约地址执行简单的转账 trace_call,并使用状态覆盖设置合约的余额。它打印结果以及哪些方法成功的摘要。请注意,如果方法不受支持,节点将返回错误;脚本会捕获并报告该错误。

运行脚本并填写下面的结果表。这将帮助你了解你的端点支持什么以及响应的形状。

const axios = require('axios');

const RPC_URL = 'YOUR_RPC_URL'; // e.g., https://mainnet.example.com
const TX_HASH = '0x...'; // replace with a real transaction hash
const CONTRACT_ADDRESS = '0x...'; // replace with a contract address for state override

async function rpcCall(method, params) {
  const response = await axios.post(RPC_URL, {
    jsonrpc: '2.0',
    id: 1,
    method,
    params
  });
  if (response.data.error) {
    throw new Error(response.data.error.message);
  }
  return response.data.result;
}

async function main() {
  // 1. trace_transaction
  try {
    const trace = await rpcCall('trace_transaction', [TX_HASH]);
    console.log('trace_transaction succeeded. Number of traces:', trace.length);
    console.log('First trace type:', trace[0]?.type);
  } catch (e) {
    console.log('trace_transaction failed:', e.message);
  }

  // 2. debug_traceTransaction with callTracer
  try {
    const debug = await rpcCall('debug_traceTransaction', [TX_HASH, { tracer: 'callTracer' }]);
    console.log('debug_traceTransaction succeeded. Top-level type:', debug.type);
    console.log('Calls count:', debug.calls ? debug.calls.length : 0);
  } catch (e) {
    console.log('debug_traceTransaction failed:', e.message);
  }

  // 3. trace_call with state override
  try {
    const tx = {
      from: '0x0000000000000000000000000000000000000000',
      to: CONTRACT_ADDRESS,
      value: '0x0',
      data: '0x' // change to a function call if needed
    };
    const overrides = {
      [CONTRACT_ADDRESS]: {
        balance: '0xde0b6b3a7640000' // 1 ETH
      }
    };
    const result = await rpcCall('trace_call', [tx, ['trace'], overrides]);
    console.log('trace_call succeeded. Output:', result.output);
    console.log('Gas used:', result.gasUsed);
  } catch (e) {
    console.log('trace_call failed:', e.message);
  }
}

main();

结果表:填写你的端点的行为

使用此表记录每个方法在你的目标端点上的结果。这是一个诊断工具,不是基准测试。

  • trace_transaction – 支持?(是/否) – 如有错误消息 – 返回的追踪数量 – 第一个追踪类型
  • debug_traceTransaction (callTracer) – 支持?(是/否) – 如有错误消息 – 顶层类型 – 子调用数量
  • trace_call (带状态覆盖) – 支持?(是/否) – 如有错误消息 – 输出(前 10 字节) – 使用的 Gas
  • 备注 – 观察到的任何速率限制、超时或特殊要求

常见追踪故障排查

当追踪失败时,错误消息通常指向根本原因。以下是常见问题及解决方法:

  • 方法未找到 – 端点不支持 trace_*debug_* 命名空间。请查阅提供商的文档,或切换到支持该命名空间的节点。一些提供商提供单独的追踪端点。
  • 历史状态不可用 – 追踪旧交易需要归档节点数据。如果你收到类似 'missing trie node' 或 'header not found' 的错误,你的节点可能是没有归档数据的全节点。请使用归档端点或提供历史追踪的提供商。
  • 超时 – 追踪很慢。如果你的请求超时,请尝试更具体的追踪器(例如,使用 callTracer 而不是默认的结构记录器),或使用具有更长超时的提供商。请参阅我们的指南 以太坊 RPC 超时和重试
  • 速率限制 – 提供商通常更严格地限制追踪方法。如果你遇到速率限制,请考虑批量请求或使用专用的追踪端点。查看 RPC 定价 了解典型限制。
  • 无效的追踪器名称 – 使用 debug_traceTransaction 时,请确保追踪器名称受你的客户端支持。常见的追踪器包括 callTracerprestateTracer4byteTraceropcodeLogger。请参阅 Geth 文档 获取列表。
  • 状态覆盖格式 – 在 trace_call 中,stateOverrides 参数必须是一个以地址为键的对象,每个值包含可选的 balancecodenoncestate 字段。格式不正确可能导致错误。

局限性与权衡

追踪是一项强大但昂贵的操作。它会消耗大量 CPU 和 I/O,尤其是对于复杂交易或大区块。提供商通常默认禁用追踪,或将其作为高级功能提供。请务必检查你的特定端点的文档。例如,一些提供商只支持对近期区块进行 debug_traceTransaction,而其他提供商则要求你使用专用的归档节点。

输出格式也因客户端而异。虽然 trace_* 命名空间旨在与 OpenEthereum 规范保持一致,但字段名称以及 revertReasonstateDiff 的可用性存在细微差异。同样,debug_* 追踪器可能因客户端版本而产生不同的结构。请始终根据实际响应验证你的解析逻辑。

对于生产系统,如果你需要多次重放同一交易,请考虑缓存追踪结果。此外,请注意负载大小:复杂交易的追踪可能达到数兆字节,这可能会影响网络性能。尽可能使用限制输出的过滤器或追踪器。

如果你正在构建索引器或分析管道,你可能更倾向于使用 trace_filter 来检索特定地址或区块范围的追踪,但此方法并非在所有客户端上都可用。有关监控和端点健康的更多信息,请参阅 监控 RPC 端点和节点健康

后续步骤与进一步阅读

既然你了解了 trace 和 debug 命名空间之间的区别,你就可以为你的用例选择正确的方法。为了加深理解,请探索以下资源:

有关权威参考,请参阅 以太坊执行 API 文档Geth debug 命名空间文档。如果你使用提供商,请务必查阅其特定的追踪文档,因为支持和输出可能有所不同。

永远不用担心基础设施

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

开始