本文介绍 evm tracing。
概述
跟踪(tracing)允许用户精确检查 EVM 在某些特定交易或交易集合中执行了哪些操作。以太坊有两种不同类型的交易:价值转移和合约执行。价值转移只是将 ETH 从一个账户转移到另一个账户。合约交互执行的是存储在合约地址中的代码,其中包括更改 storage data 以及与其他合约和外部账户进行多次交易。因此,一个合约执行交易可能是一个难以分解的复杂的交互网络。交易收据包含显示交易成功或失败的状态码,但难以获得更详细的信息,这意味着很难知道合约执行到底做了什么,修改了哪些数据,触及了哪些地址。这正是 EVM 跟踪所要解决的问题。Geth 通过在本地重新运行交易并收集有关 EVM 执行内容的准确数据来跟踪交易。
也可以查看 Devcon 2022 关于 Geth 跟踪的演讲。
状态可用性(State availability)
以最简单的形式来说,跟踪交易需要请求以太坊节点重新执行所需的交易,并收集不同程度的数据,然后返回汇总摘要。为了让 Geth 节点重新执行交易,交易访问的所有历史状态都必须可用。这包括
- Balance、nonce、bytecode、recipient 的 storage 以及所有内部调用的合约。
- 外部交易和所有内部创建的交易在执行过程中引用的 Block metadata。
- 与正在跟踪的交易位于同一区块内的所有前置交易生成的中间状态。
这意味着节点的同步和剪枝配置对可跟踪的交易有限制:
存档节点 (archive node) 保留所有历史数据,可追溯到创世。因此,它可以跟踪链历史上任意点的任意交易。跟踪单个交易需要重新执行同一区块中所有之前的交易。
从 genesis 节点同步的节点只在内存中保留最近的 128 个区块状态。较早的状态由一系列偶尔出现的检查点表示,中间状态可以从这些检查点重新生成。这意味着,最近 128 个区块内的状态可以立即使用,而较旧的状态则必须实时从快照中重新生成。如果请求的交易与最近的检查点之间的距离很大,重建状态可能需要很长时间。跟踪单个交易需要重新执行同一数据块中的所有前面的交易,以及上一个存储快照之前的所有前面的数据块。
快照同步节点(snap synced node)会在内存中保存最近的 128 个块,因此该范围内的交易总是可以访问的。不过,快照同步只从相对较近的区块开始处理(而不是 full node 的 genesis block)。在初始同步块和 128 个最新块之间,节点会偶尔存储检查点,用于即时重建状态。这意味着交易可以追溯到用于初始同步的区块。跟踪单个交易需要重新执行同一数据块中的所有前置交易,以及直到上一个存储快照为止的所有前置数据块。
理论上,按需检索数据的轻同步节点 (light synced node) 可以跟踪网络中随时可用的所有所需历史状态的交易。这是因为,生成跟踪所需的数据是向服务较差的全节点请求的。实际上,数据可用性是无法合理假设的。
该图显示了每种同步模式存储的状态–红色表示存储状态。每条线的长度代表从原点到现在。
有关同步的更多详细信息,请访问 同步模式页面。
执行特定交易的跟踪时,会通过从数据库中获取父块的状态来准备状态。如果没有可用状态,Geth 会及时向后抓取以查找下一个可用状态,但只能达到 reexec
参数中定义的上限,默认值为 128 个区块。如果在 reexec
窗口内没有可用的状态,则跟踪失败,提示错误:所需的历史状态不可用,必须增加 reexec
参数。如果在重新执行窗口中找到有效状态,Geth 会按顺序重新执行最后可用状态和目标块之间每个块中的交易。reexec
的值越大,跟踪所需的时间就越长,因为必须重新执行更多的区块才能重新生成目标状态。
debug_getAccessibleStates
RPC 接口 是估算 reexec
适当值的有用工具。向该 RPC 解耦传递包含目标交易的块的编号和搜索距离,就会返回当前头部后方存在最新可用状态的块的数目。该值可作为 reexec
传递给跟踪器。
还可以通过停止 Geth,并在一段时间内再次使用 --gcmode archive
运行,强制 Geth 为特定序列的区块存储状态,这样可以防止在 Geth 使用 --gcmode archive
运行时对到达的区块进行状态修剪。
在对整个区块或链段进行批量跟踪时,上述规则也有例外。稍后将详细说明。
跟踪类型
基本跟踪
Geth 能生成的最简单的交易跟踪类型是原始 EVM 操作码跟踪。对于交易执行的每一条 EVM 指令,都会生成一个结构化日志条目,其中包含所有有用的上下文元数据。其中包括程序计数器、操作码名称、操作码成本、剩余 gas、执行深度和任何发生的错误。结构化日志还可选择包含执行堆栈、执行内存和合约存储的内容。
有关 Geth 基本跟踪的更多信息,请访问 基本跟踪。
内建跟踪器
跟踪 API 接受一个可选的tracer
参数,用于定义如何处理 API 调用返回的数据。如果省略该参数,则使用默认跟踪器。默认为结构(或 “操作码”)记录器。这些原始操作码跟踪有时很有用,但返回的数据级别很低,对于许多用例来说,读取的范围太广,读起来很费劲。完整的操作码跟踪可轻松达到数百兆字节,因此从节点中获取并从外部处理这些数据非常耗费资源。出于这些原因,有一组非默认的内置跟踪器可以在 API 调用中使用,以便从方法中返回不同的数据。这些跟踪器是 Go 或 Javascript 函数,它们会在跟踪数据返回前对其进行一些特定的预处理。
有关 Geth 内置跟踪器的更多信息,请参阅 内置跟踪器。
自定义跟踪器
除内置跟踪器外,还可提供定制代码,与 EVM 中的事件挂钩,以可消耗格式处理和返回数据。在底层,自定义跟踪器可以用 Javascript 或 Go 语言编写。Javascript 跟踪器适用于快速原型开发和实验,也适用于不太密集的应用。Go 跟踪器性能优越,但需要将跟踪器与 Geth 源代码一起编译。这意味着开发人员只需收集实际需要的数据,并在源代码中进行任何处理。
有关自定义跟踪器的更多信息,请访问 自定义跟踪器。
总结
本文介绍了跟踪的概念,并解释了与状态可用性有关的问题。有关 Geth 内置跟踪器和自定义跟踪器的更多详细信息,请参阅其专用页面。
评论