Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 13 分钟

以太坊交易对象与收据:解析交易哈希

了解已签名交易对象与执行收据的区别,以及如何通过原始 JSON-RPC 将任意哈希解析为未找到、待处理、已打包、已回滚或已替换。

TL;DR

交易对象是发送给 EVM 的已签名输入:发送方、接收方、金额、nonce、gas 参数、calldata 和签名。收据是执行该交易后产生的输出记录,区块的 receiptsRoot 承诺的正是收据。实践中关键的不对称在于:eth_getTransactionByHash 可以在交易打包前就返回交易,此时 blockNumber、blockHash 和 transactionIndex 为 null;而 eth_getTransactionReceipt 在交易被包含进区块之前会一直返回 null。这一对结果足以将哈希分类为待处理、已打包、已回滚或已替换,但两个调用都返回 null 时含义模糊,必须结合链高度、发送方的待处理 nonce 或第二个端点来区分。由于重组可能导致收据消失,对账时应同时以 blockHash 和 transactionHash 为键。

交易对象与收据是两种不同的记录

交易对象是某个密钥签名并广播的内容。其中的每个字段都是协议验证签名或执行载荷所必需的:from(从签名恢复,不直接传输)、to(合约创建时为 null)、value、nonce、gas、费用字段(传统交易为 gasPrice,类型 2 交易为 maxFeePerGas 和 maxPriorityFeePerGas)、input 中的 calldata(data 作为遗留别名保留)、type,以及三个签名分量 v、r 和 s。eth_getTransactionByHash 的以太坊执行 API 规范定义了此返回对象,ethereum.org JSON-RPC 文档在其方法参考中描述了相同的结构。

交易对象的关键特性是它描述意图,而非结果。它说明应该发生什么以及谁授权了它。它不说明执行是否成功、实际消耗了多少 gas、发出了哪些事件,或部署创建了哪个地址。这些都是执行结果,存在于另一个对象中,只有区块包含该交易后才会产生。将返回的交易对象视为成功证明的应用,读错了协议的另一半。

另一个对象就是收据。它包含 status(0x1 成功或 0x0 回滚)、cumulativeGasUsed(区块中截至并包括此交易的所有交易消耗的 gas)、gasUsed(仅此交易消耗的 gas)、effectiveGasPrice、logs 和 logsBloom、contractAddress(仅成功部署时非 null)、type,以及定位字段 transactionHash、blockHash、blockNumber 和 transactionIndex。eth_getTransactionReceipt 的执行 API 条目列出了这些字段及其编码。

区块头通过 receiptsRoot 承诺其所有交易的收据。这是收据主导对账设计的结构性原因:区块哈希锚定了确切的收据内容,因此与区块哈希一起获取的收据日后可以验证,而裸交易对象则不能。这也解释了为什么回滚的交易仍有收据并仍消耗 gas。执行发生了,其效果被回滚,协议仍以一定代价记录了这次尝试。

交易与收据的逐字段映射

两个对象仅在少数几个字段上重叠。其余字段都只属于其中一个,这在决定针对某个问题该调用哪个方法时是一个有用的捷径。

两者共有:hash/transactionHash(相同的 32 字节值)、blockNumber、blockHash、transactionIndex 和 type。仅交易对象有:from、to、value、nonce、gas(上限)、gasPrice、maxFeePerGas、maxPriorityFeePerGas、input、data、accessList、v、r、s 以及客户端包含时的 chainId。仅收据有:status、gasUsed、cumulativeGasUsed、effectiveGasPrice、logs、logsBloom、contractAddress,以及携带 blob 的类型 3 交易上的 blobGasUsed/blobGasPrice。

重叠部分恰好是定位字段集。这是有意为之:定位字段让你能将交易与其收据及区块关联起来,并且两个对象在交易被包含前都报告这些字段为 null。对于待处理交易,交易对象中的 blockNumber、blockHash 和 transactionIndex 为 null,且根本没有收据。任何假设 transactionIndex 是整数的代码,在待处理行上都有潜在的 null 解引用风险。

实践中待处理与已打包的不对称性

最有用的操作差异在于两个调用在交易被包含前的行为。eth_getTransactionByHash 在节点知晓交易后立即返回完整交易,无论它来自内存池还是已打包区块。eth_getTransactionReceipt 在交易打包前返回 null。因此(交易存在、收据为 null)意味着待处理,而收据本身是包含的唯一正面证明。

这不是提供商的怪癖。它源于 JSON-RPC 2.0 语义,其中 null 结果是有效的成功响应,而非错误。不知道哈希的节点会回答 {"result": null} 且不带错误对象,这意味着同一个 null 可能表示待处理、未知、已修剪,或由落后节点提供。下面的分类器将 null 视为节点视图的事实,而非对链的裁决。

一个边缘情况值得注意:合约创建时 to 为 null,而收据上的 contractAddress 仅在部署成功时才有值。构造函数中回滚会产生 status: 0x0 且 contractAddress: null 的收据,因此仅凭任一字段都无法判断部署地址是否存在。

一个可运行的六种哈希状态分类器

下面的脚本针对一个端点解析一个哈希,并打印单一分类状态。它刻意避免 JSON-RPC 批量数组,因为某些提供商会拒绝或限流它们,并且并发发出两个查询,使待处理检查只需一次往返而非两次。用真实哈希在你自己的端点上运行它。

nonce 比较是区分 pending 与 replaced_or_dropped 的关键。如果发送方的待处理 nonce 已超过此交易的 nonce,则另一个交易填充了该槽位,此交易将永远不会被打包。该检查并不完美:内存池较浅的节点可能报告尚未推进的待处理 nonce,而交易被丢弃后待处理 nonce 会稳定下来而非保持领先。将结果视为强信号,并在采取行动前用第二个端点确认。

  • pending — 交易已知,收据为 null,发送方的待处理 nonce 尚未超过交易 nonce。
  • replaced_or_dropped — 交易已知,收据为 null,发送方的待处理 nonce 已超过交易 nonce。
  • mined_success / mined_reverted — 收据存在,status 为 0x1 / 0x0。
  • not_found — 两个调用都返回 null。
  • inconsistent_view — 两个调用不一致,例如有收据但没有交易对象。
// classify-tx.mjs — Node.js 18+
// Usage: node classify-tx.mjs <RPC_URL> <TX_HASH>
// Classifies a transaction hash into: not_found | pending | replaced_or_dropped |
// mined_success | mined_reverted | inconsistent_view
const [rpcUrl, txHash] = process.argv.slice(2);
if (!rpcUrl || !txHash) {
  console.error('Usage: node classify-tx.mjs <RPC_URL> <TX_HASH>');
  process.exit(1);
}

// Single JSON-RPC call. Batch arrays are not supported by every provider,
// so the classifier stays on plain single requests and runs them in parallel.
async function rpc(method, params) {
  const res = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(`${method}: ${json.error.code} ${json.error.message}`);
  return json.result;
}

const toInt = (hex) => (hex === null || hex === undefined ? null : Number(BigInt(hex)));

const [tx, receipt] = await Promise.all([
  rpc('eth_getTransactionByHash', [txHash]),
  rpc('eth_getTransactionReceipt', [txHash])
]);

if (!tx && !receipt) {
  console.log(JSON.stringify({ state: 'not_found', hash: txHash }));
  process.exit(0);
}

if (tx && receipt) {
  const state = receipt.status === '0x1' ? 'mined_success'
    : receipt.status === '0x0' ? 'mined_reverted'
    : 'mined_unknown_status';
  console.log(JSON.stringify({
    state, hash: txHash, blockNumber: toInt(receipt.blockNumber),
    blockHash: receipt.blockHash, transactionIndex: toInt(receipt.transactionIndex),
    gasUsed: toInt(receipt.gasUsed), effectiveGasPrice: toInt(receipt.effectiveGasPrice),
    logs: (receipt.logs || []).length, contractAddress: receipt.contractAddress
  }));
  process.exit(0);
}

if (tx && !receipt) {
  // Transaction is known but not yet mined: blockNumber/blockHash/transactionIndex are null.
  if (tx.blockNumber !== null) {
    console.log(JSON.stringify({ state: 'inconsistent_view', hash: txHash,
      detail: 'transaction has a blockNumber but no receipt yet' }));
    process.exit(0);
  }
  // Compare the account's pending nonce with the transaction nonce. If the account
  // has moved past this nonce, the slot was filled by another transaction.
  const pendingNonce = toInt(await rpc('eth_getTransactionCount', [tx.from, 'pending']));
  const txNonce = toInt(tx.nonce);
  const state = pendingNonce !== null && pendingNonce > txNonce ? 'replaced_or_dropped' : 'pending';
  console.log(JSON.stringify({ state, hash: txHash, from: tx.from, nonce: txNonce, pendingNonce }));
  process.exit(0);
}

console.log(JSON.stringify({ state: 'inconsistent_view', hash: txHash,
  detail: 'receipt present without transaction object' }));

针对公共以太坊端点的实测运行

分类器于 2026-10-18 针对一个公共以太坊主网端点进行了测试。一个从未广播的语法有效哈希返回 not_found,两个结果均为 null。从区块 26,218,014 中取出的真实交易返回 mined_success,blockNumber 为 26218014,transactionIndex 为 3,gasUsed 为 21000,effectiveGasPrice 为 2036108547,日志为零,与普通 ETH 转账相符。两次运行在已预热连接上均在一秒内完成。

同一脚本针对公共 Base 端点时失败而非误报:收据调用返回 JSON-RPC 错误 -32602,消息为归档请求需要个人令牌,而交易调用成功。这正是错误分支存在的意义。将 null 或错误视为待处理的朴素客户端会报告从未观察到的状态;脚本本身抛出异常并以非零退出,下面的记录逐字展示了这一点。

下面的原始运行除哈希参数外未经编辑。将其视为调用形式的证据,而非对任何提供商性能的声明。

$ node classify-tx.mjs https://ethereum-rpc.publicnode.com 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060
{"state":"not_found","hash":"0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"}

$ node classify-tx.mjs https://ethereum-rpc.publicnode.com 0x9d07584175e77cc5c7ba7249a6b5322ff171b71208ec4966975f5d7c5483a97c
{"state":"mined_success","hash":"0x9d07584175e77cc5c7ba7249a6b5322ff171b71208ec4966975f5d7c5483a97c","blockNumber":26218014,"blockHash":"0x52e4f099f235ce78bc66bf51a5a98d8c7a7ec53bbe274bbf41385799948aba22","transactionIndex":3,"gasUsed":21000,"effectiveGasPrice":2036108547,"logs":0,"contractAddress":null}

$ node classify-tx.mjs https://base-rpc.publicnode.com <base-tx-hash>
Error: eth_getTransactionReceipt: -32602 Archive requests require a personal token.
    at rpc (classify-tx.mjs:20:25) ... exit code 1

分类 null 结果、错误与重组竞争

三个记录性质不同,这种差异正是设计教训。null 结果是有效的 JSON-RPC 成功,说明节点视图的某些情况。JSON-RPC 错误对象表示请求被拒绝,通常由于能力或访问限制。传输失败则完全不能说明链上情况。在应用中将其映射为三种不同结果,绝不要让错误或超时落入待处理桶。

批量数组同样需要小心。如果在一个数组中发送多个查询,提供商可能返回比发送更少的结果,或在每个请求槽位返回错误对象。在将结果与哈希关联前,按 id 而非位置索引结果,并将缺失 id 视为仅该哈希的错误。

分类的第四个输入是时间。一个哈希可能经历 pending、mined_success、重组、再次 pending。分类器回答的是当前真相,因此应持久化状态及其观察时的区块高度,并在确认扫描期间重新运行分类,而非假设单调进展。

选择端点与分类结果是两个独立决策:一个响应一致、暴露所需命名空间且不对扫描限流的节点,比偶尔延迟更低的节点更有价值。RPC Assistant 中的以太坊 RPC 节点指南涵盖了这一选择。

区分未找到的哈希

当两个调用都返回 null 时,诚实的答案是此节点看不到该交易。在得出结论前分四步解决。首先检查哈希本身:32 字节、0x 前缀、66 个字符,且没有空白或 UI 复制导致的中间截断。其次,将节点的 eth_blockNumber 与第二个来源比较;落后数小时的节点不会有近期交易,即使网络有。第三,询问第二个独立端点。修剪或不索引所需范围的提供商可能对其他节点持有的交易返回 null。

第四,对于你认为刚发送的交易,查看发送方在 pending 和 latest 标签下的 eth_getTransactionCount。如果 pending 等于你分配的 nonce,则交易尚未被此节点可见的任何池接受。如果 pending 大于你的 nonce,则该槽位已被其他东西占用。如果 latest 等于你的 nonce 且 pending 未移动,则账户完全没有推进。

只有经过这些检查后,结论才有意义,即便如此,正确的陈述也应有范围:在此端点此高度未找到,而非网络中不存在。如果需要内存池视图,启用命名空间的节点上的 txpool_content 可直接回答,详见使用 txpool 命名空间读取以太坊内存池。

收据日志、logsBloom,以及为何不应手动过滤

收据的 logs 数组保存执行期间发出的事件记录,每个包含 address、topics 和 data。logsBloom 是该收据中地址和主题的 256 字节概率摘要,其存在是为了让节点能廉价地跳过不可能匹配过滤器的收据。区块头有自己的 logsBloom,区块收据的 bloom 是 eth_getLogs 请求在打开任何收据前测试的对象。

实用规则是绝不在应用代码中实现 bloom 测试。Bloom 设计上存在误报,因此正匹配不能证明事件存在,你仍须扫描收据。如果需要事件,用 eth_getLogs 请求,其过滤语义见eth_getLogs 事件与主题过滤。仅将 logs.length 和 logsBloom 作为已获取收据上的廉价元数据,例如跳过对未发出任何事件的交易获取事件体。

一个相关陷阱是假设回滚交易存在日志。它们不存在:回滚交易的收据 status: 0x0 且 logs 数组为空,因为状态变更和事件一起被回滚。仅以交易哈希为键且从不检查 status 的索引器会静默错过无事件与回滚事件之间的区别。

重组行为与以 BlockHash 为键的对账

收据的持久性仅与产生它的区块相当。如果该区块被孤立,交易可能返回内存池并被重新包含在不同区块中,或完全消失。在该窗口内,eth_getTransactionReceipt 可能从有值收据变回 null,之后又变为具有不同 blockHash 和 blockNumber 的收据。任何仅存储 transactionHash 和 status 的对账都会将这些视为同一行而永远注意不到。

将 blockHash 与收据一起存储,并在确认扫描期间重新验证,尤其是确认深度内的区块。将已知交易 blockHash 的变化视为重组信号而非更新,并在应用新收据前回滚派生状态。停止检查的深度是产品决策,通过 RPC 深度检测以太坊区块重组涵盖了如何测量。

注意交易对象本身携带相同的定位字段,因此如果你从交易对象对账,同样的规则适用:交易中的 blockHash 是承诺,即该区块的收据应存在。如果不存在,你看到的是重组中的读取或不一致的节点。

逐哈希查询与区块级批量检索

逐哈希查询是当你已持有哈希并想要一个答案时的正确工具:用户提交了它、webhook 触发了,或钱包正在轮询。每个哈希一次请求并线性扩展,在故障后对账数千个哈希时会成为限流问题。

处理区块时,反转方向。获取一次区块,然后在单个 eth_getBlockReceipts 调用中获取其所有收据,并按 transactionIndex 或哈希与交易列表关联。这将 N 次逐哈希调用替换为每区块一两次调用,并保证同一区块的一致快照,而逐哈希调用在两者之间发生重组时无法做到。机制(包括对未实现该方法的节点的回退)见eth_getBlockReceipts 批量获取收据。

实践中混合方式效果良好:前向索引使用批量检索,用户发起的检查使用逐哈希查询,并将上述分类器作为将原始结果对转换为应用可操作状态的唯一位置。

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

提供商行为在分类最关键细节上各不相同,因此应测量而非假设。针对你依赖的每个端点、同一组固定样本运行以下四个探测,并记录答案。空白单元格表示未测量,而非错误。

  • 待处理交易:eth_getTransactionByHash 是否返回 blockNumber: null 的对象,eth_getTransactionReceipt 是否返回 null?记录 HTTP 状态和原始 result。
  • 未知哈希:响应是 {"result": null} 且无错误,还是 JSON-RPC 错误对象?记录代码。
  • 超出节点保留范围的旧交易:null 结果还是提及修剪或归档的错误?两者都记录。
  • 重组窗口:重组后,孤立区块的收据返回 null、旧收据还是错误?记录观察到的行为及发生变化时的区块深度。

限制、故障排除与后续步骤

分类器是起点,而非完整的交易服务。它本身不处理重组,信任单一提供商,并且当提供商内存池视图过时或发送方使用多个节点时,其 nonce 启发式可能误报 replaced_or_dropped。它也不告诉你交易为何回滚:收据给出 status: 0x0 和消耗的 gas,诊断原因需要 trace,那是另一个命名空间,有其自身成本特征。

常见失败及其含义:任一调用上的 -32601 Method not found 意味着端点未暴露该命名空间,而非交易缺失。收据上的 -32602 通常意味着请求需要归档访问,如上面的 Base 运行所示。以异常结束的超时应归类为错误,绝不要映射为 pending。而出现意外 blockHash 的收据是重组事件,而非数据刷新。

下一步,将分类器接入你实际使用的端点,运行结果表,然后才决定是否需要区块级批量检索。eth_getTransactionReceipt 返回 null涵盖了应置于其上的轮询循环,以太坊网络页面和 RPC 定价涵盖了基础设施方面,如果你更愿意使用托管端点而非公共端点。更多类似指南见 OnFinality Learn 中心,API 服务涵盖了需要跨链一致行为的团队的托管访问。

永远不用担心基础设施

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

开始