交易收据是区块链交易执行后的记录:它确认执行是否成功、报告消耗的 gas,并携带执行期间发出的事件日志。与描述请求内容的交易对象不同,收据描述的是交易被打包进区块后实际发生的情况。本文解释收据数据模型,展示如何在 curl、JavaScript 和 Python 中通过 JSON-RPC 获取和验证收据,并涵盖常见故障模式:收据为 null、状态回滚、日志缺失以及链或提供商不匹配。文章还概述了可复现的收据延迟测量流程,以便你评估自己的 RPC 路径,而不是依赖供应商的说法。
交易收据代表什么
交易收据是交易被打包进区块及其执行产出的规范记录。它以交易哈希为键,只有在交易被挖出或以其他方式最终确定到区块后才会出现。在打包之前,收据不存在,收据查询返回 null。
实践中最重要的区别是交易与其收据之间的区别。交易对象描述意图:发送方、接收方、价值、gas 限额、gas 价格字段、nonce 和 calldata。收据描述结果:执行成功还是回滚、实际消耗了多少 gas、支付的有效 gas 价格、部署创建的合约地址以及发出的日志。需要知道某件事是否成功的应用应读取收据,而不是交易。
收据也是事件驱动索引的基础。你的后端响应的每个 Transfer、Swap 或自定义事件都是收据中的一条日志条目。如果你构建确认逻辑,你实际上是在等待一个拥有足够数量后代区块的收据。
- 交易:请求、签名并广播的内容。
- 收据:执行了什么、花费多少以及发出了什么。
- 收据仅在打包后存在;打包前的查询返回 null。
假设与前提条件
以下示例假设使用 EVM 兼容的 JSON-RPC 端点以及你控制或可观察的交易哈希。你需要正确网络的 RPC URL;请替换为你自己的 RPC_URL 占位符,而不是硬编码公共端点,因为公共端点在速率限制、归档深度和支持的方法上各不相同。如果你要为收据密集型工作负载(如索引器或确认服务)选择提供商,RPC 端点指南和多链 RPC 端点指南涵盖了选择标准,OnFinality 的 API 服务描述了跨支持网络的托管访问。
你还应该知道目标链。从错误链获取给定哈希的收据要么为 null,要么在罕见的跨链哈希碰撞中产生误导。始终在应用状态中将链 ID 与哈希一起固定。
收据字段及其含义
确切的字段集由 JSON-RPC 规范定义,并且在 EVM 链之间大体一致,尽管某些链会添加字段,例如 rollup 上的 L1 费用数据。核心字段足够稳定,可以在此基础上构建。
transactionHash:标识交易及其收据的哈希。blockHash和blockNumber:交易被打包的位置。transactionIndex:在区块中的位置。from和to:发送方和接收方;对于合约创建,to为 null。contractAddress:当交易创建合约时部署的地址,否则为 null。cumulativeGasUsed:区块中直到并包括此交易的所有交易使用的 gas。gasUsed:仅此交易消耗的 gas。effectiveGasPrice:每单位 gas 实际支付的价格(在支持链上为 EIP-1559 后)。status:0x1表示成功,0x0表示执行回滚。logs:事件日志数组,每个包含 address、topics、data 和 log index。logsBloom:区块日志的概率过滤器,对轻客户端和某些索引器有用。type:交易类型,例如 legacy 或 EIP-1559。
使用 curl 获取收据
标准方法是 eth_getTransactionReceipt。它接受单个参数,即交易哈希,并返回收据对象或 null。替换占位符后,以下请求即可运行。
curl -s -X POST "$RPC_URL" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getTransactionReceipt",
"params": ["0xYOUR_TRANSACTION_HASH"]
}'在 JavaScript 中获取和验证收据
在应用代码中,你很少想要单次即发即忘的调用。你想要轮询直到收据存在,然后断言成功并检查日志。下面的示例直接使用 fetch,因此无需库即可工作,并将 null 视为待处理而非错误。
const RPC_URL = process.env.RPC_URL;
const TX_HASH = process.env.TX_HASH;
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
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(json.error.message);
return json.result;
}
async function waitForReceipt(hash, timeoutMs = 120000, intervalMs = 2000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const receipt = await rpc('eth_getTransactionReceipt', [hash]);
if (receipt) return receipt;
await new Promise(r => setTimeout(r, intervalMs));
}
throw new Error('Timed out waiting for receipt');
}
const receipt = await waitForReceipt(TX_HASH);
if (receipt.status !== '0x1') {
throw new Error(`Transaction reverted in block ${receipt.blockNumber}`);
}
console.log('Gas used:', parseInt(receipt.gasUsed, 16));
console.log('Logs emitted:', receipt.logs.length);在 Python 中获取收据
Python 服务通常跨多个哈希批量检查收据。下面的代码片段仅使用标准库,并演示了相同的待处理与回滚区分。对于高容量索引,批量请求或订阅新块头的 websocket 通常比逐哈希轮询更高效。
import json, os, time, urllib.request
RPC_URL = os.environ["RPC_URL"]
TX_HASH = os.environ["TX_HASH"]
def rpc(method, params):
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
req = urllib.request.Request(RPC_URL, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as resp:
body = json.loads(resp.read())
if "error" in body:
raise RuntimeError(body["error"]["message"])
return body["result"]
def wait_for_receipt(tx_hash, timeout=120, interval=2):
deadline = time.time() + timeout
while time.time() < deadline:
receipt = rpc("eth_getTransactionReceipt", [tx_hash])
if receipt:
return receipt
time.sleep(interval)
raise TimeoutError("receipt not found before timeout")
receipt = wait_for_receipt(TX_HASH)
assert receipt["status"] == "0x1", f"reverted in block {receipt['blockNumber']}"
print("gasUsed", int(receipt["gasUsed"], 16))
print("logs", len(receipt["logs"]))预期验证结果
成功的验证有三个可观察的结果。首先,收据对象非空且其 transactionHash 与你查询的哈希匹配。其次,status 为 0x1。第三,如果你的应用依赖事件,预期的日志条目会出现,并带有正确的发出合约地址和 topic0 签名。
如果你确认的是最终性而不仅仅是打包,请将收据的 blockNumber 与来自 eth_blockNumber 的当前块头进行比较,并要求适合该链的确认深度。具有概率最终性的链需要比具有显式最终性机制的链更大的深度。记录你使用的深度;这是一个策略决策,不是协议常量。
当你运行延迟测量时,将它们记录在表格中,而不是断言一个数字。可复现的格式如下所示:
- 列:网络、RPC 路径、样本量、p50 收据延迟、p95 收据延迟、首次轮询的 null 率。
- 流程:广播或选择已知交易,以固定间隔轮询
eth_getTransactionReceipt,并记录第一个非空响应的时间戳。 - 在多个交易和一天中的不同时间重复;报告百分位数,而不仅仅是平均值。
排查 null、回滚和不匹配的收据
null 收据是最常见的问题,几乎总是意味着交易尚未被打包。它也可能意味着交易已从内存池中丢弃、被具有相同 nonce 的更高费用交易替换,或者从未广播。在得出结论交易失败之前,检查发送方的 nonce 和内存池状态。
status: 0x0 的收据意味着交易被打包并支付了费用,但执行回滚。gas 仍然被消耗。要诊断,请在父区块使用 eth_call 重放调用,或者如果你的提供商支持跟踪方法,检查交易跟踪。不要将回滚的收据视为网络错误;它是一个有效的链上结果。
存在但日志与预期不匹配的收据通常表明合约地址错误、事件签名更改或代理升级。根据你认为已部署的 ABI 解码 topics[0],并确认每条日志中的发出地址。
最后,从错误链获取的收据通常为 null,但如果你操作多个网络,请为每个请求标记链 ID 并在客户端中断言。提供商侧问题(如速率限制)通常表现为 JSON-RPC 错误而非 null 结果,因此请区分传输错误与真正的缺失。
限制与权衡
收据不是完整的审计追踪。它们不包括内部调用、回滚原因或状态差异,除非你使用跟踪 API,而许多提供商限制或单独计量这些 API。如果你需要完整的执行细节,请规划支持跟踪的基础设施并相应预算。
轮询收据很简单,但在规模上浪费。订阅新块头的 websocket 结合本地过滤可减少请求量,但增加了连接管理复杂性和重连逻辑。批量 JSON-RPC 请求是处理每区块许多哈希的索引器的折中方案。
如果你回填历史收据,归档要求很重要。最近的收据通常由全节点提供,但深层历史可能需要归档访问。在设计回填作业之前,确认提供商的保留策略,如果你需要可预测的容量,请参阅专用节点选项。更高吞吐量 RPC 访问的定价在 RPC 定价页面上概述。
后续步骤
首先对你的收据路径进行插桩:为应用提交的每笔交易记录哈希、链 ID、轮询次数和最终状态。这一项更改就能将大多数“交易卡住”的报告转化为可诊断的数据。
然后按链决定你的确认策略,并将其编码为配置,而不是分散的常量。如果你跨多个网络运营,请查看支持的网络列表并标准化你的收据客户端,以便链特定的怪癖在一个地方处理。
对于选择端点来承载收据流量的团队,具体的提供商决策最好由 RPC Assistant 处理,它根据你的延迟和可靠性需求比较端点选项。使用本文了解机制,并用自己的测量结果做出判断。