把合约执行的每一步打印成账单:EIP-3155 的 EVM 追踪格式 图 1
把合约执行的每一步打印成账单:EIP-3155 的 EVM 追踪格式 · 图 1

这份格式在记什么

一笔合约交易执行时,EVM 会按顺序执行成千上万条指令:压栈、加算、读存储、跳进另一个合约。追踪(trace)就是把执行过程逐条记下来的流水账:每执行一条操作码,就输出一个 JSON 对象,描述这一刻机器的状态。EIP-3155 要标准化的正是每一行该写哪些字段。

按这份规范,逐行输出的必备字段包括:pc,程序计数器,指出当前执行到字节码的第几个字节;op,这条操作码的数字编号;gas,执行这条指令前还剩多少燃料,用十六进制字符串写;gasCost,这条指令本身要扣多少;memSize,当前分配出的内存大小;stack,操作数栈上每个值的十六进制串;depth,调用深度,从外部进一层合约就加一;returnData,这次调用的返回数据;refund,截至当前累计的燃料返还不满额。可选字段里有 opName(把操作码编号翻译成人能读的助记名)、error(出错原因,规范建议尽量带上回滚原因)、memory(整段内存内容)、storage(本次触碰过的存储键值)。执行结束后还要补一行汇总:状态树根、输出数据、总耗气量、以及这笔交易在测试里算不算通过。

为什么要给流水账立规矩

追踪不是共识层的东西,全链没有任何节点有义务输出它。它是给人和工具看的:合约开发者调试逻辑、测试框架比对状态、研究人员复盘一笔链上事故。问题在于,早年的追踪格式只是几个客户端各自实现形成的默契,Go-Ethereum、Nethermind、Besu 这些实现输出的字段名、数字写法、空值处理都不完全一致。规范文本里明确写了这份文件的动机:把事实标准挪到明面上,让新客户端愿意实现,并让各家实现可以做严格对比。

最有价值的场景是对拍:拿同一笔交易喂给不同客户端,把两边的逐行轨迹并排比。只要第几万个字节之后两边行为出现分叉,轨迹立刻暴露分歧——这对硬分叉前的实现一致性测试、以及链分裂后的事故定位都是利器。合约测试跑出来的轨迹如果跨客户端逐行一致,说明各家对这个操作码的计价和语义理解相同。

读轨迹的正确姿势

拿到一份轨迹,先看总量再看局部。几千行的正常执行不需要通读,通常的排法是:从末尾的汇总行看这笔交易总耗气和最终状态根;遇到失败,用文本工具筛出带 error 字段的那一行,它往往是整个调用链回滚的起点;往上翻几行,看 depth 在哪一层从 2 掉回 1,就能定位问题发生在被调合约内部还是入口检查;再看触发失败那行的 pcop,对照反汇编就能指出是哪一个检查把交易挡回来的。

gasCostgas 这对字段也常藏线索:某一行前后剩余燃料突然暴跌,说明那步触发了昂贵操作,比如第一次写存储槽或者跨合约的冷地址访问;而整笔交易明明只转一笔小额代币却耗气异常高,多半是循环里藏着重复的存储写入。

它和别种追踪有什么不同

以太坊生态里能看到执行过程的方式有好几种。轻量日志只列调用层级、地址和每层耗气,适合快速看结构;结构日志把逐行记录包成事件流;再往前还有一类基于通知机制的追踪,输出可读性更好但结构不同。EIP-3155 这一份的特点是极简与可机读:每行独立成 JSON、数字编码规则写死、数组必须初始化成空数组而不是空值,为的就是让不同语言的工具无需特判就能解析。工具选型时按需求对号:看结构选摘要式,逐字节复盘选逐行式。

一条实操线

把这份格式当成事后黑匣子最省心:先在测试网复现,再开逐行追踪,用末尾汇总锁定耗气规模,用报错行锁定回滚位置,用 depth 变化锁定肇事合约,最后带着定位结果去读源码。绝大多数”交易为什么失败”的问题,用这套顺序十分钟之内能定位到具体语句。若涉及生产资产,排查阶段只读即可,不要为了验证猜测去主网反复广播——重复提交失败交易同样要付燃料费。

风险提示:本文仅作技术科普,不构成任何投资建议;调试工具输出可能因客户端版本不同存在差异,重要结论应以多客户端交叉验证为准。