libmonad_event 或 Rust 包 monad-exec-events 读取事件。
本页面提供 C 和 Rust API 中使用的基本概念的概述。
event ring 与执行事件
尽管实时数据系统及其 SDK 通常被称为”执行事件”,但 SDK 有两个不同部分:- event ring API - “event ring”是一种共享内存数据结构及其读取和写入 API 的名称。event ring 是一种通用的 IPC 广播工具,用于向任意数量的读取进程发布事件。event ring API 处理_非结构化_ I/O:类似 UNIX 的
read(2)和write(2)文件 I/O 系统调用,event ring API 将所有数据视为原始字节数组 - 执行事件定义 - 实际的”执行事件”是执行层守护进程写入的用于表示特定 EVM 操作的标准化二进制格式。它可以被视为一种协议、schema 或序列化格式。继续类比,如果 event ring API 类似于 UNIX 的
read(2)和write(2)文件 API,那么”执行事件”就像定义特定文件包含内容的”文件格式”
monad-event-ring 和 monad-exec-events。
C SDK 是单个库,但两个不同部分的头文件位于不同目录:event ring 头文件位于 category/core/event 子目录,执行事件文件位于 category/execution/ethereum。
event ring 基础
什么是事件?
事件由两个组件组成:- 事件描述符 是描述已发生事件通用字段的固定大小(当前 64 字节)对象。它包含事件的类型、序列号、时间戳和一些内部簿记信息
- 事件负载 是事件的可变大小额外数据片段,特定于事件类型。例如,“交易日志”事件描述交易发出的单个 EVM 日志记录。虽然描述符告诉我们事件类型(即它是”日志事件”),但负载告诉我们所有细节:合约地址、日志主题和日志数据。事件描述符中尚未提及的一些字段用于传达负载字节在共享内存中的位置以及负载长度
请记住,在 event ring API 层面,事件负载只是非结构化字节缓冲区;读取者必须知道它们正在读取的内容的格式,并相应地解释
事件存在哪里?
当事件发生时,事件描述符被写入位于共享内存段中的环形缓冲区。此环形缓冲区是下图中的”事件描述符数组”。 事件负载存储在另一个数组中(在单独的共享内存段中),称为”负载缓冲区”。- 事件描述符是_固定大小_的,事件负载是_可变大小_的
- 事件描述符引用/“指向”其负载的位置
- 事件描述符和负载位于不同的共享内存连续数组中
- 它支持_广播_语义:多个读取者可以同时从 event ring 读取,每个读取者在 ring 内维护自己的迭代器位置
- 与典型的广播协议一样,写入者不知道读取者 —— 事件被写入,无论是否有人在读取它们。因为写入者甚至不知道读取者在做什么,它无法等待读取者,如果读取者较慢。读取者必须快速迭代事件,否则事件会丢失:描述符和负载内存可以被后续事件覆盖。概念上事件序列是一个_队列_(具有 FIFO 语义),但称为_ring_ 以强调其溢出时覆盖的语义
- 事件描述符中包含序列号以检测 gap(因读取者慢而丢失的事件),使用类似策略检测何时负载缓冲区内容被覆盖
执行事件基础
如前所述,event ring API 处理非结构化 I/O。此 API 不理解数据的含义:它了解字节,但不了解区块、交易等。 在处理特定 event ring 时,读取者假设字节具有某种已知格式来解释字节。SDK 使用的主要格式是_执行_事件:在 Monad 区块链上执行提议区块期间实时记录 EVM 发生情况的二进制格式。还有一些其他类型的格式(称为”内容类型”),但它们仅由 Category Labs 内部使用,主要用于性能分析。 对于概述的其余部分,我们将查看一个示例执行事件。示例:“交易开始”事件
一种特别重要的事件是”交易头开始”事件,它在 EVM 解码新交易后不久记录。它包含大多数交易信息(编码为 C 结构)作为其事件负载。负载结构在exec_event_ctypes.h 中定义为:
monad_c_eth_txn_header 结构包含大多数有趣的信息 —— 它在 eth_ctypes.h 中定义如下:
T_n 和 T_c)引用了以太坊黄皮书中的变量名。
类型 monad_c_uint256_ne(“native endian”,本机字节序)是一个 256 位整数,以大多数性能良好的”大整数”库使用的肢体格式存储为 uint64_t[4]。
如果您使用 Rust SDK,则在构建
monad-exec-events 包时会由 bindgen 生成具有相同名称(并且由于 #[repr(C)] 属性具有相同二进制布局)的 struct 类型。执行事件负载的决定性特征是它们依赖于简单 C 数据结构在编程语言之间的”自然”互操作性。大多数流行的编程语言都定义了与 C 代码一起工作的外部函数接口,这通常也伴随着以某种方式”自然”处理 C 结构类型。虽然 C 的数据表示不可移植,但这些对象位于共享内存中,因此读取者和写入者必须在同一主机上,并且必须遵循相同的 C ABI。可变长度尾部数组和后续事件
对于特定的”交易开始”事件,_大多数_事件负载将是struct monad_exec_txn_header_start 值的低级字节表示。几乎总是在负载字节数组中此结构_之后_会有一些额外数据:
- 交易的可变大小
data字节数组,其长度由data_length字段指定,也是事件负载的一部分,紧跟在struct monad_exec_txn_header_start对象之后 - 如果这是 EIP-4844 交易,
blob_versioned_hashes数组将紧跟在data数组之后
_length 字段在固定大小结构中的列出顺序记录。
EIP-2930 和 EIP-7702 列表也是交易中的可变长度项目,但它们_不_记录在”交易头开始”事件的负载中。
它们不作为尾部数组记录,而是为每个 EIP-2930 访问列表条目和每个 EIP-7702 授权元组记录一个唯一事件。这些事件的数量_确实_发布在”交易头开始”事件负载中(参见 access_list_count 和 auth_list_count 字段),以便读取者知道预期还有多少事件。
描述符中的执行事件属性
到目前为止我们已经讨论了”交易开始”事件的负载,但事件的通用属性直接记录在事件描述符中。最重要的是,这些包括标识事件类型的数字代码,以便我们知道我们应该首先将非结构化负载字节解释为struct monad_exec_txn_header_start。
事件描述符定义如下:
event_type 字段将设置为 C 枚举常量 MONAD_EXEC_TXN_HEADER_START 的值,即 enum monad_exec_event_type 类型的值。这告诉用户可以将指向事件负载开头的 const uint8_t * 强制转换为 const struct monad_event_txn_header_start *(或在 Rust 中执行对应的 unsafe 转换)。
所有 C 枚举常量都以 MONAD_EXEC_ 前缀开头,但文档通常在没有前缀的情况下引用事件类型,例如 TXN_HEADER_START。
请注意,交易编号不包含在负载结构中。由于它们在区块链协议中的重要性,交易编号直接编码到事件描述符中。在低级别(即 C API 中),此信息编码在 context_ext[1] 字段中。在 Rust API(更加用户友好)中,它被解码并作为结构字段 ExecEventRingFlowInfo::txn_idx 呈现。1
TXN_HEADER_START 被称为交易头的_开始_的原因是可能存在包含 EIP-2930 和 EIP-7702 信息的后续事件。看到所有交易头信息后会发出相应的 TXN_HEADER_END 事件。TXN_HEADER_END 没有负载,仅用于宣布已记录与交易输入相关的所有事件。此类事件在文档中称为”标记事件”。
最后,它之所以被称为”头”,是因为存在更多与交易相关的事件。各种”交易头”事件仅描述区块中的所有输入。大多数事件描述交易_输出_:日志、call frame、状态更改和收据。
内存布局示例
下图说明了上面解释的关于交易头的可变长度尾部数组、相关后续事件及其终止标记事件的所有内容。此示例交易的 EIP-2930 访问列表中有两个账户,并且没有 EIP-7702 条目。EIP-2930 列表中的每个地址都会记录一个单独的TXN_ACCESS_LIST_ENTRY 事件,带有可能访问的存储键的可变长度尾部数组。
执行事件序列化中的模式
为什么 EIP-2930 条目被记录为单独的TXN_ACCESS_LIST_ENTRY 事件,而不是作为 TXN_HEADER_START 中的可变长度尾部数组?因为涉及两级可变长度信息。有可变数量的 EIP-2930 账户,然后对每个账户又有可变数量的关联存储键。
事件序列化协议力求非常简单:只有当数组_元素_类型为固定大小时,才会记录可变长度尾部数组。特别地,形如锯齿状数组的数据在事件负载中是不允许的。
每当可变性有多个维度时,通过使用更多不同的事件来”分解”它们。权衡是在事件较少但编码更复杂 vs. 更多事件将数据”展开”为”更扁平”形状之间做选择。后者更适合”零拷贝 C ABI”数据模型。2
从技术上讲,EIP-7702 授权列表_可以_表示为可变长度尾部数组,因为授权元组是固定大小的。然而,作为设计决策,可变长度尾部数组仅允许具有简单元素类型(如 u8 或 uint256),且数量不能太多。
VLT 数组的解码逻辑往往容易出错;它看起来令人困惑是因为在代码中更难”看清”序列化规则究竟是什么。将数据”展开”为更多事件更具自解释性:创建不同类型的对象,而不是依赖隐式解析规则来重新解释非结构化尾部数据。
因此,VLT 数组仅在其使用看起来”显而易见”时才使用,例如每个 EIP-2930 访问列表条目中的存储键数组。

