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

以太坊回滚原因与自定义错误:解码交易或 eth_call 失败的原因

了解如何从 JSON-RPC 响应中解码以太坊回滚原因和自定义错误,包括 Error(string)、Panic(uint256) 和自定义错误选择器。

TL;DR

当以太坊交易或 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) 选择器:0x08c379a0
  • Panic(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)0x08c379a0require/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 学习中心 提供了更多指南。

请记住,在依赖公共端点之前,始终先在本地或测试网节点上进行测试。祝调试愉快!

永远不用担心基础设施

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

开始