追踪以太坊交易:如何逐步读取和理解EVM执行过程

本文介绍了如何使用Foundry工具和debug_traceCall方法来模拟以太坊交易,检查opcode级别的执行过程,以及调试成功和失败的交易。通过这些技术,开发者可以深入了解智能合约在EVM中的实际运行方式,包括分析gas消耗、定位revert发生的位置,并最终优化智能合约。

一旦你按照这里所学,设置好你的本地 Foundry 环境,是时候深入了解以太坊交易的内部机制,看看当 EVM 执行你的代码时实际发生了什么。

从 calldata 解码到存储写入的每一个操作,都可以使用 debug_traceCall 或 Foundry 内置的调试器进行追踪和检查。这些追踪揭示了每个执行的 opcode,每一步的堆栈和内存,以及发生回滚或 gas 峰值的确切位置。

在本文中,我们将逐步介绍如何使用 Foundry 追踪和解释交易:模拟调用、查看原始 opcode 追踪、识别成功和失败的执行,以及在 opcode 级别调试脚本。到最后,你将理解如何阅读 EVM 逐步执行流程,并利用这种洞察力像专业人士一样调试和优化你的智能合约。

理解 debug_traceCall

要检查 EVM 实际上如何执行一个交易,我们依赖于 debug 命名空间下的特殊 JSON-RPC 方法。这些方法不是标准以太坊 JSON-RPC API 的一部分,它们通常只在完整节点(如 Geth 或 Erigon,稍后会介绍)或本地开发节点(如 Foundry 使用的 Anvil)上可用。

两个最有用的方法是:

  • debug_traceTransaction — 通过哈希追踪已挖出的交易
  • debug_traceCall — 模拟一个交易而不发送它,并返回完整的追踪

debug_traceCall :它的作用

这个 RPC 调用让我们模拟一个交易在给定状态下的执行方式,并为我们提供每一个 EVM 步骤的详细追踪,包括:

  • 在每个程序计数器 (pc) 执行的 opcode
  • 堆栈内存存储访问
  • 深度(用于内部调用)
  • 每一步使用的 Gas

这是我们将用来详细追踪交易的工具,而无需实际挖掘它或修改区块链状态。

为什么debug_traceCall 对开发者有用

与仅返回返回值的 eth_call(稍后会介绍)不同,debug_traceCall 会告诉你返回值是如何计算的以及 EVM 在内部做了什么。这使得它对于以下方面非常宝贵:

  • 调试 reverts 并准确理解它们发生在哪里
  • 在 opcode 级别分析 gas 使用情况
  • 构建需要理解底层行为的 测试框架索引器浏览器
  • 学习 EVM 如何处理 calldata、堆栈操作和存储更改

追踪一个成功的交易

前提:

使用--debug 运行以查看 EVM 追踪

这是 EVM 的交互式原始执行流程。它显示:

  • 堆栈和内存指令
  • 输入解码
  • 存储写入
  • 最终退出

应该看起来像这样:

按 Enter 或点击查看完整尺寸的图片

从追踪确认成功

如果我们只有访问原始追踪 opcodes 的权限,我们仍然可以验证调用的成功:

我们的 store() 函数接受一个结构体 (uint256 number, string owner)。让我们用值 25"bob" 相应地调用它。

正如我们从之前的系列中记得的那样,我们需要编码我们的合约方法和参数,让我们探索如何使用 anvil 完成:

cast calldata "store((uint256,string))" "(25,"bob")"

// 输出应该是:0xddd356b30000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003626f620000000000000000000000000000000000000000000000000000000000

现在当我们编码了数据,我们将模拟智能合约 store 方法:

cast rpc debug_traceCall \
  '{"to":"<your-contract-address>", "data":"0xddd356b30000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003626f620000000000000000000000000000000000000000000000000000000000"}' \
  latest | jq '.' > trace.json

注意:确保jq已安装在你的系统中,以便漂亮地打印追踪输出。

你将看到如下输出:

{
  "failed": false,
  "gas": 52080,
  "returnValue": "",
  ...
}
  1. 检查 failed: false
  2. 查找最终 Opcode:STOPRETURN

这意味着函数干净地退出了。

追踪一个失败的交易

在这个智能合约示例中,大于 100 的数字将导致智能合约失败。因此,如果我们调用 store((101, "too much")),它应该 revert。

添加一个失败的测试

使用另一个测试方法更新测试文件:test/Storage.t.sol

function testStoreStructReverts() public {
    Storage.my_storage_struct memory input = Storage.my_storage_struct({
        number: 101,
        owner: "too much"
    });

    storageContract.store(input);
}

在不期望 Revert 的情况下运行它

forge test --match-test testStoreStructReverts

输出将是:

按 Enter 或点击查看完整尺寸的图片

要使测试通过(并且仍然追踪它),请使用 Foundry 的 cheatcode

function testStoreStructReverts() public {
    Storage.my_storage_struct memory input = Storage.my_storage_struct({
        number: 101,
        owner: "too much"
    });

    vm.expectRevert("Number too large");
    storageContract.store(input);
}

输出将是:

按 Enter 或点击查看完整尺寸的图片

可以在 debug 模式下运行它,以查看逐步执行

从原始追踪确认失败

执行与之前显示的数字 101 相同的过程,并将此保存到 trace_fail.json 将输出下一个文件:

{
  "failed": true,
  "gas": 23190,
  "returnValue": "08c379a0..."  // ABI-encoded error string
  ...
}
  1. 检查 failed: true
  2. 查找最终 Opcode:REVERT

这意味着函数失败了。

使用 Foundry 脚本追踪和调试交易

在我们之前的测试中,我们使用 forge test --debug 来追踪交易。虽然这让你逐步执行测试函数,但它不会像我们想要检查的 store() 函数那样进入合约逻辑本身

Foundry 的调试器可以通过 forge testforge scriptcast run 访问。在本节中,我们将重点关注 forge script,它让你逐步执行内部合约逻辑,否则这些逻辑将隐藏在测试包装器中。

什么是 Forge 脚本

在 Foundry 中,Forge 脚本 是一种在 Solidity 本身中编写部署或交互逻辑的方法,而不是使用像 forge create 这样的 CLI 命令或像 Hardhat 这样的外部 JavaScript 工具。

你可以把它想象成:

一个小的 Solidity 程序,让你可以在一个 run() 函数中部署合约、调用函数或模拟交易。

默认情况下,脚本通过调用名为 run 的函数来执行

它类似于你使用其他语言编写的脚本,但不是用 JavaScript 或 Python 编写它们,而是直接用 Solidity 编写它们。

调试脚本

当你使用脚本时,Foundry 会像完整交易一样执行你的代码。这意味着包括你的合约逻辑在内的每个函数调用都是顶级执行的一部分,你可以逐个 opcode 地逐步执行它。

让我们编写一个脚本并将其保存在 script/DebugStore.s.sol

pragma solidity ^0.8.12;

import "forge-std/Script.sol";
import "../src/Storage.sol";
contract DebugStore is Script {
    function run() external {
        Storage s = new Storage();
        s.store(Storage.my_storage_struct({ number: 101, owner: "bob" }));
    }
}

要调试脚本,我们将在终端中运行:

forge script script/DebugStore.s.sol:DebugStore \
  --fork-url http://localhost:8545 \
  --debug

注意:确保anvil fork 在不同的终端中运行并暴露 url:http://localhost:8545

你现在将进入一个交互式 opcode 调试器,你可以在其中:

  • 使用 ↑ 和 ↓ 逐步执行每个 EVM 指令
  • 准确了解 store() 在幕后是如何工作的
  • 观察 calldata 解析 (CALLDATALOAD)、条件检查 (GT) 和存储写入 (SSTORE)

例子:

按 Enter 或点击查看完整尺寸的图片

在这里我们可以看到,内部合约将 revert,因为我们传递了 number > 100

注意:我们可以像之前在测试中所做的那样添加vm.expectRevert(...) **在我们调用 store 方法之前。

总结

EVM 追踪揭示了交易在以太坊虚拟机内部采取的每一步。在本文中,你学习了如何使用 Foundry 和 debug_traceCall 来模拟调用、检查 opcode 级别的执行以及调试成功和失败的交易,从而使你完全了解你的智能合约的实际运行方式。

  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。