实时 event ring 与快照 event ring
event ring 的共享内存数据结构通常存在于常规文件中。任何想要共享访问 event ring 的进程首先通过文件系统定位它,然后使用 mmap(2) 系统调用将其共享视图映射到进程的虚拟内存映射中。 event ring 文件有两种类型:- “实时” event ring 文件 —— 这些是作为实时数据源的”正常” event ring 文件。SDK 的整个目的是从这些文件中读取实时事件,但它们对于大多数日常软件开发任务并不方便。例如,假设您想为数据处理程序编写测试。SDK 主要是围绕_读取_事件设计的,因此要用实时 event ring 测试它,您需要编写一些虚拟事件发布代码来产生要读取的事件。对于执行事件,实时 event ring 文件由执行层守护进程填充,而在教程的这一步我们甚至还没安装它!第二种 event ring 文件解决了很多开发上的头痛。
- “快照” event ring 文件 —— 这些是对实时 event ring 文件在某个特定时刻的压缩快照。通常它们被”倒回”到循环事件队列中最旧的事件,用于重放一组固定的历史执行事件。快照文件对测试和开发工作流很有用,因为您不需要运行活动的发布方即可使用它们。因为它们对开发非常有用,快照是我们在实时节点上试用示例程序之前将使用的第一个数据源。
在快照文件上运行示例程序
步骤 1:下载快照文件
运行此命令以下载快照:emn-30b-15m 部分意为”以太坊主网重放:从 1500 万区块之后的 30 个区块”。换句话说,它包含在以太坊区块链(chain ID 1)从区块 15,000,001 到区块 15,000,031 的历史重放期间发出的执行事件。
Category Labs 执行层守护进程能够执行来自 Monad 区块链(EVM chain ID 143 或其任何测试网络)的区块,也可执行来自其他 EVM 兼容网络的区块。以太坊主网的历史重放被用作执行”一致性测试”,以确保节点软件保持尽可能与以太坊兼容。
我们在教程中使用以太坊链快照,是假设许多开发者已经熟悉以太坊生态系统,但可能对 Monad 是新手。您可以检查快照文件中捕获的所有数据是否与您喜欢的以太坊数据提供商发布的数据匹配。例如,您能够检查这里显示的数据是否与 Etherscan 等网站报告的一致。
步骤 2:运行您之前构建的 SDK 示例程序
命令对每种编程语言略有不同。 对于 C,运行:#[derive(Debug)] 特性。Rust 命令行中的 -d 参数告诉程序打印这种”调试”形式。
C 语言家族的完整美化打印器_确实_存在于 SDK 中,但它们仅适用于 C++,基于标准 C++
<format> 库步骤 3:分析数据(仅 Rust)
如果您运行 Rust 示例程序 —— 本指南的这一步假设您在运行 —— 您将看到所有事件数据的文本转储。我们将查看几个特定事件,让您了解 SDK 生成的数据类型以及您可以用它做什么。 Rust 示例程序打印的前两行如下:16:26:14.354056730—— 这是原始事件记录时的纳秒精度时间戳;由于我们查看的是快照而不是实时数据,此数字始终相同,且时间已久远;打印时省略了时间戳的实际”日期”部分,因为 SDK 的典型用例是实时数据(其中日期通常是”今天”)BLOCK_START—— 这是发生在 EVM 内部的事件类型;当执行层守护进程首次看到新区块时会记录一个BLOCK_START事件,其负载描述了在执行处理_开始_时已知的所有执行输入;这大致对应于以太坊区块头中在执行之前已知的字段[2 0x2]—— 这是对应于BLOCK_START事件类型的数字代码,以十进制和十六进制表示SEQ: 1—— 序列号(迄今为止已发布事件数的单调计数器)为 1;在实时 event ring 中,这些用于 gap / 覆盖检测BLK: 15000001—— 此事件是区块编号 15,000,001 的一部分
#[derive(Debug)] 输出没有换行),我们在示例输出文本中对它进行了缩写。稍后我们将查看它的部分内容,但现在我们暂停一下解释关于此 println!("Payload: {exec_event:x?}") 语句的一些事情。
exec_event 是 Rust 枚举类型 ExecEvent 的值。以下是该枚举的定义方式:
- 调试输出以
BlockStart(...)开头,因此exec_event具有ExecEvent::BlockStart枚举变体 - 看起来我们从前面的
BLOCK_START [2 0x2]打印输出中已经知道了这一点,但有一个微妙的差异。第一行打印在_事件描述符_中找到的信息,它像是包含事件通用字段的头部。在程序打印描述符行的那个点,它尚未解码事件负载来构造exec_event变体。假设我们只对区块 15,000,002 感兴趣。在这种情况下,我们可以只查看描述符,注意到它与区块 15,000,001 相关,然后跳过此事件(以及该区块的所有其他事件),即我们不会费力解码它 - 与
ExecEvent::BlockStart变体关联的值类型是struct monad_exec_block_start;请注意,此类型_不_遵循正常的 Rust 代码格式化风格:它使用lower_case_snake_case而不是UpperCamelCase,并有一个看似不必要的前缀(所有变体值类型都以monad_exec_开头)。这是因为负载类型定义为 C 语言结构,其 Rust 等价物是使用 bindgen 生成的。C 风格拼写有助于表明这一点。monad_exec_block_start的定义来自 C 头文件exec_event_ctypes.h,其定义如下:
eth_block_input 是对应于以太坊区块头中在执行开始时已知部分的字段。
此输出中的某些内容难以阅读,因为 Rust 的 #[derive(Debug)] 是为调试便利而设计的,并不总是以最易读的方式”美化打印”数据。但其他字段是清晰的,例如,区块的 gas_limit 显示为十六进制值:
0x1c9c380 对应于十进制数 30,000,000,这是我们期望在以太坊主网 gas 限制中看到的数字。
事件的真正美化打印是通过一个称为
monad-event-cli 的开发者工具完成的,它是 SDK 的一部分。此示例旨在尽可能简单和简短,以帮助学习 API。调试真实事件程序时,您可能更倾向于使用像事件 CLI 工具这样的开发者工具。它的构建说明在”开始使用”指南的最后一步 (此处)。TXN_EVM_OUTPUT,第一个匹配将是此事件(带有一些格式差异):
TXN: 0 和负载中的 txn_index: 0。我们说”第一个事件”是因为任何特定交易的输出通常跨越_多个_事件:每个日志、call frame、状态更改和状态访问都作为单独的事件记录。
第一个事件始终是 TXN_EVM_OUTPUT 类型。它包含发生了什么的基本汇总,以及后续将有多少与输出相关的事件的指示。您可以看到这笔特定交易发出了零条日志和一个 call frame 追踪。call frame 信息记录在下一个事件中,位于此行下方。
事实证明,第一笔交易本身也相当有趣:它在使用 30,300 gas(0x765c)后执行失败。交易失败由 status 字段记录。如您所见,它设置为 false。
为什么失败?为了弄清楚,我们将使用紧随其后的 TXN_CALL_FRAME 事件中的信息。该事件中的 evmc_status_code 字段的值为 2,即 EVMC_REVERT 状态码的数值。这告诉我们回滚是由合约代码本身请求的,即它执行了 REVERT 指令。换句话说,这不是虚拟机发起的异常停止,如”gas 耗尽”或”非法指令”,而是合约本身决定做的事情。
因为这是一个 Solidity 合约,我们可以从 call frame 中解码出更丰富的错误信息。REVERT 指令可以将任意长度的返回数据传回调用者。这些返回数据记录在 call frame 的 return_bytes 数组中。
观察到 return_bytes 的前 4 字节是 0x8c379a0。这是 Solidity 表示带字符串说明的回滚的方式。此字符串的编码细节在此处,但要点是我们可以将此 return_bytes 数组的最后 32 字节解码为 ASCII 字符串。如果您自己尝试,会发现它写着:
TXN_HEADER_START)中,我们可以找到交易的 Keccak 哈希,为 0xaedb8ef26125d8ad6e0c5f19fc9cbdd7f4a42eb82de88686b39090b8abcfeb8f。如果我们使用该哈希在 Etherscan 上查询该交易的信息,可以看到 Etherscan 与我们一致。Status: 字段显示:

