当以太坊交易或 eth_call 回滚时,JSON-RPC 响应中的错误数据字段包含 ABI 编码的回滚原因。本文介绍如何使用 ethers 或 viem 解码 Error(string) (0x08c379a0)、Panic(uint256) (0x4e487b71) 和自定义错误(bytes4 选择器),以及如何从状态为 0x0 的交易收据中获取回滚原因。
直接回答:如何从 JSON-RPC 读取回滚原因
当以太坊交易或 eth_call 失败时,回滚原因不会以纯文本消息返回。相反,EVM 会在 JSON-RPC 错误对象内返回一个 ABI 编码的字节字符串。对于典型的 eth_call,响应看起来像 {"error":{"code":3,"data":"0x08c379a0..."}},其中 data 字段包含编码后的原因。要解码它,您必须知道回滚的类型:Error(string)(选择器 0x08c379a0)、Panic(uint256)(选择器 0x4e487b71)或自定义错误(4 字节选择器)。对于自定义错误,您需要合约 ABI 来将选择器映射到错误名称和参数。本指南将逐步介绍该机制,并提供一个可运行的脚本来解码所有三种类型。
如果您正在排查 RPC 端点问题,请参阅我们的 以太坊 RPC 节点指南 和 RPC 定价 以了解端点配置。如需更广泛的背景,OnFinality 学习中心 涵盖了相关主题,如 以太坊 RPC 超时 和 速率限制。
- 回滚原因是 ABI 编码的,不是人类可读的字符串。
- JSON-RPC 错误的
data字段包含编码后的原因。 - 自定义错误需要合约 ABI 才能解码。
- 状态为
0x0的交易收据表示回滚,但原因不会存储在链上。
EVM 回滚机制与 ABI 编码
当 Solidity 合约执行 revert()、require(false) 或遇到算术错误时,EVM 会回滚所有状态更改并向调用者返回原因。该原因根据 ABI 规范进行编码。对于 Error(string),编码是 4 字节选择器 0x08c379a0,后跟 32 字节的字符串数据偏移量,然后是字符串长度和 UTF-8 字节。对于 Panic(uint256),选择器是 0x4e487b71,后跟 32 字节的整数恐慌代码。自定义错误(在 Solidity 0.8.4 中引入)编码为错误签名的 4 字节选择器,可选后跟 ABI 编码的参数。
以太坊执行规范记录了回滚会消耗所有 gas 并将原因返回给调用者。Solidity 关于自定义错误的文档说明,自定义错误通过其选择器标识,并且可以携带参数。这就是为什么没有 ABI 就无法解码自定义错误——您只能看到像 0x9e8b2f3a 这样的 4 字节选择器。
如需更深入地了解 JSON-RPC 接口,以太坊执行 API 描述了标准错误格式。提供商可能以不同方式包装错误,但 data 字段是解码的关键。
Error(string)选择器:0x08c379a0Panic(uint256)选择器:0x4e487b71- 自定义错误:错误签名的 4 字节选择器
- 恐慌代码:
0x01(断言)、0x11(溢出/下溢)、0x12(除以零)等。
JSON-RPC 错误表面:eth_call 和 eth_sendRawTransaction
当您执行回滚的 eth_call 时,节点会返回一个 JSON-RPC 错误对象。在 geth 中,错误代码为 3,消息为 execution reverted,回滚数据在 data 字段中。其他客户端可能使用不同的代码或消息,但 data 字段是标准的。例如,失败的 eth_call 可能返回:
对于 eth_sendRawTransaction,交易被挖掘后,如果回滚,收据显示 status: '0x0'。但是,回滚原因不会存储在链上;您必须使用 eth_call 重新模拟交易以获取原因。一些提供商提供 debug_traceTransaction 方法来获取回滚原因,但这不是标准的,可能需要归档节点访问。
提供商的包装可能有所不同。例如,一些提供商可能在顶层 data 字段中包含回滚数据,或使用不同的错误代码。始终检查完整的错误对象。如果您使用专用端点,请参阅我们的 API 服务 了解详情。
eth_call在错误对象的data字段中返回回滚数据。eth_sendRawTransaction收据状态为0x0表示回滚,但不包含原因。- 使用
eth_call模拟交易以捕获回滚原因。 - 提供商错误代码可能不同;依赖
data字段。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 3,
"message": "execution reverted",
"data": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a496e73756666696369656e740000000000000000000000000000000000000000"
}
}
手动解码 Error(string) 和 Panic(uint256)
要手动解码 Error(string) 回滚,您可以解析数据:取前 4 个字节确认选择器 0x08c379a0,然后读取接下来的 32 个字节作为偏移量(通常为 0x20),再读取接下来的 32 个字节作为字符串长度,最后读取 UTF-8 字节。对于 Panic(uint256),前 4 个字节是 0x4e487b71,接下来的 32 个字节是作为 uint256 的恐慌代码。
例如,数据 0x08c379a0... 包含字符串 "Insufficient balance",解码后得到该消息。恐慌代码 0x11 表示算术溢出或下溢。Solidity 的恐慌代码在 Solidity 文档 中有记录。
虽然手动解码具有教育意义,但使用 ethers 或 viem 等库更可靠,并且能处理边缘情况。
- Error(string) 数据布局:选择器 + 偏移量 + 长度 + 字符串字节。
- Panic(uint256) 数据布局:选择器 + 32 字节恐慌代码。
- 常见恐慌代码:0x01(断言)、0x11(溢出)、0x12(除以零)。
使用 ABI 解码自定义错误
自定义错误更复杂,因为仅凭 4 字节选择器无法得知错误名称或参数。您必须拥有包含错误定义的合约 ABI。例如,如果您的合约定义了 error InsufficientBalance(uint256 available, uint256 required),则选择器是根据签名 InsufficientBalance(uint256,uint256) 计算的。要解码,您需要将选择器与 ABI 匹配,然后解码参数。
ethers.js 和 viem 等库提供了 decodeErrorResult 函数,该函数接受 ABI 和回滚数据。对于 ethers v6,您可以使用 Contract.interface.parseError(data) 或 Interface.parseError。对于 viem,使用 viem 中的 decodeErrorResult。这些函数会自动处理选择器查找和参数解码。
没有 ABI,您只能看到选择器。这是一个根本限制:自定义错误不是自描述的。调试时请始终保留合约 ABI。
- 自定义错误选择器是回滚数据的前 4 个字节。
- 使用合约 ABI 将选择器映射到错误名称和参数。
- ethers v6:
contract.interface.parseError(data) - viem:
decodeErrorResult({ abi, data })
可复现示例:使用 viem 的 Node.js 脚本
以下脚本演示了如何使用 viem 解码回滚原因。它接受 RPC URL、合约地址和触发回滚的 calldata(或函数调用)。脚本执行 eth_call 并解码错误数据。请将占位符替换为您自己的值。
对于 Error(string) 回滚,预期输出为 Error message: Insufficient balance。对于自定义错误,它将打印错误名称和参数。该脚本假定您有一个会回滚的合约;您也可以使用已知的示例,例如因余额不足而失败的代币转账。
要使用真实合约进行测试,您可以使用 OnFinality 提供的公共端点。有关公共端点列表,请参阅我们的 以太坊网络页面。
- 该脚本使用 viem 的
decodeErrorResult处理所有错误类型。 - 您必须为自定义错误提供 ABI。
- 脚本打印解码后的错误或未知选择器的原始选择器。
import { createPublicClient, http, decodeErrorResult } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http('YOUR_RPC_URL')
});
// Example ABI fragment for a custom error
const abi = [
{
type: 'error',
name: 'InsufficientBalance',
inputs: [
{ name: 'available', type: 'uint256' },
{ name: 'required', type: 'uint256' }
]
}
];
async function decodeRevert() {
try {
// This call will revert; replace with your own contract call
await client.call({
address: '0xContractAddress',
data: '0x...' // calldata that triggers revert
});
} catch (error) {
const data = error.data; // or error.cause.data depending on viem version
if (data) {
const selector = data.slice(0, 10); // 0x + 4 bytes
if (selector === '0x08c379a0') {
// Decode Error(string) using viem's decodeErrorResult with a generic ABI
const decoded = decodeErrorResult({ abi: ['error Error(string)'], data });
console.log('Error message:', decoded.args[0]);
} else if (selector === '0x4e487b71') {
const decoded = decodeErrorResult({ abi: ['error Panic(uint256)'], data });
console.log('Panic code:', decoded.args[0]);
} else {
// Try custom error with provided ABI
try {
const decoded = decodeErrorResult({ abi, data });
console.log('Custom error:', decoded.errorName, decoded.args);
} catch (e) {
console.log('Unknown custom error selector:', selector);
}
}
} else {
console.log('No revert data in error:', error);
}
}
}
decodeRevert();
结果表:错误类型到修复的映射
下表总结了常见的回滚场景和推荐的修复方法。调试时可用作快速参考。
| 错误类型 | 选择器 | 常见原因 | 修复 |
|---|---|---|---|
| Error(string) | 0x08c379a0 | require/revert 带消息 | 检查消息;修复条件 |
| Panic(0x01) | 0x4e487b71 | 断言失败 | 检查不变量 |
| Panic(0x11) | 0x4e487b71 | 算术溢出/下溢 | 使用 SafeMath 或 Solidity 0.8+ 检查 |
| Panic(0x12) | 0x4e487b71 | 除以零 | 检查除数 |
| 自定义错误 | 变化 | 业务逻辑错误 | 使用 ABI 解码;检查参数 |
| 燃气耗尽 | 不适用 | 燃气限制过低 | 增加 gasLimit |
| 无数据回滚 | 不适用 | 低级回滚 | 检查合约逻辑 |
对于燃气耗尽错误,JSON-RPC 错误可能不包含回滚数据。在这种情况下,您需要增加燃气限制并重试。有关超时和燃气的更多信息,请参阅我们的 以太坊 RPC 超时 文章。
局限性与权衡
解码回滚原因有几个局限性。首先,自定义错误需要合约 ABI;没有它,您只能看到选择器。其次,一些提供商可能会截断或包装回滚数据,尤其是对于大字符串。第三,并非所有失败模式都会返回回滚原因——例如,燃气耗尽错误可能不包含数据。第四,状态为 0x0 的交易收据不包含回滚原因;您必须重新模拟交易。
此外,JSON-RPC 错误格式可能因客户端和提供商而异。虽然 data 字段是标准的,但错误代码和消息可能不同。调试时始终记录完整的错误对象。
对于生产调试,请考虑使用具有归档数据的专用 RPC 端点来重放交易。有关更多信息,请参阅我们的 公共与专用 RPC 端点 指南。
- 自定义错误需要 ABI;否则只能看到选择器。
- 提供商可能包装或截断回滚数据。
- 燃气耗尽错误可能不包含回滚数据。
- 状态为 0x0 的收据不包含原因。
后续步骤与进一步阅读
既然您了解了如何解码回滚原因,就可以应用这些知识来调试您的智能合约和 RPC 调用。有关更多以太坊特定的故障排查,请探索我们的 以太坊 RPC 节点指南 以及关于 速率限制 和 超时 的相关文章。如果您需要查询历史数据,请参阅 查询历史区块链数据。
要全面了解 RPC 端点和定价,请访问我们的 API 服务 和 RPC 定价 页面。OnFinality 学习中心 提供了更多指南。
请记住,在依赖公共端点之前,始终先在本地或测试网节点上进行测试。祝调试愉快!