> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monad.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 执行事件概述

执行层守护进程包含一个用于记录交易处理期间发生事件的系统。"执行事件"是关于 EVM 执行了某种操作的通知，例如"账户余额已更新"或"新区块已开始执行"。这些 EVM 事件可以由外部第三方应用通过高性能进程间通信（IPC）通道观察。

执行层守护进程将事件数据发布到共享内存，外部应用从同一共享内存区域读取以观察事件。您的应用程序可以使用 C 库 `libmonad_event` 或 Rust 包 `monad-exec-events` 读取事件。

本页面提供 C 和 Rust API 中使用的基本概念的概述。

## event ring 与执行事件

尽管实时数据系统及其 SDK 通常被称为"执行事件"，但 SDK 有两个不同部分：

1. **event ring API** - "event ring"是一种共享内存数据结构及其读取和写入 API 的名称。event ring 是一种通用的 IPC 广播工具，用于向任意数量的读取进程发布事件。event ring API 处理\_非结构化\_ I/O：类似 UNIX 的 [`read(2)`](https://pubs.opengroup.org/onlinepubs/009604599/functions/read.html) 和 [`write(2)`](https://pubs.opengroup.org/onlinepubs/009695099/functions/write.html) 文件 I/O 系统调用，event ring API 将所有数据视为原始字节数组
2. **执行事件定义** - 实际的"执行事件"是执行层守护进程写入的用于表示特定 EVM 操作的标准化二进制格式。它可以被视为一种协议、schema 或序列化格式。继续类比，如果 event ring API 类似于 UNIX 的 `read(2)` 和 `write(2)` 文件 API，那么"执行事件"就像定义特定文件包含内容的"文件格式"

在 Rust SDK 中，这两部分位于不同的包中：`monad-event-ring` 和 `monad-exec-events`。

C SDK 是单个库，但两个不同部分的头文件位于不同目录：event ring 头文件位于 `category/core/event` 子目录，执行事件文件位于 `category/execution/ethereum`。

## event ring 基础

### 什么是事件？

事件由两个组件组成：

1. *事件描述符* 是描述已发生事件通用字段的固定大小（当前 64 字节）对象。它包含事件的类型、序列号、时间戳和一些内部簿记信息
2. *事件负载* 是事件的可变大小额外数据片段，特定于事件类型。例如，"交易日志"事件描述交易发出的单个 EVM 日志记录。虽然描述符告诉我们事件类型（即它是"日志事件"），但负载告诉我们所有细节：合约地址、日志主题和日志数据。事件描述符中尚未提及的一些字段用于传达负载字节在共享内存中的位置以及负载长度

<Note>
  请记住，在 event ring API 层面，事件负载只是非结构化字节缓冲区；读取者必须知道它们正在读取的内容的格式，并相应地解释
</Note>

### 事件存在哪里？

当事件发生时，事件描述符被写入位于共享内存段中的环形缓冲区。此环形缓冲区是下图中的"事件描述符数组"。

事件负载存储在另一个数组中（在单独的共享内存段中），称为"负载缓冲区"。

```text theme={null}
  ╔═Event descriptor array══════════════...═════════════════════════════════════╗
  ║                                                                             ║
  ║ ┌───────────────┐ ┌───────────────┐     ┌───────────────┐ ┌───────────────┐ ║
  ║ │     Event     │ │     Event     │     │     Event     │ │░░░░░░░░░░░░░░░│ ║
  ║ │  descriptor   │ │  descriptor   │     │  descriptor   │ │░░░░ empty ░░░░│ ║
  ║ │       1       │ │       2       │     │       N       │ │░░░░░░░░░░░░░░░│ ║
  ║ └┬──────────────┘ └┬──────────────┘     └┬──────────────┘ └───────────────┘ ║
  ╚══╬═════════════════╬════════════════...══╬══════════════════════════════════╝
     │                 │                     │
     │                 │                     │
     │         ┌───────┘                     └─┐
     │         │                               │
     │         │                               │
   ╔═╬═════════╬═══════════════════════════...═╬═══════════════════════════════╗
   ║ │         │                               │                               ║
   ║ ▼───────┐ ▼─────────────────────────┐     ▼─────────────┐ ┌─────────────┐ ║
   ║ │Event 1│ │         Event 2         │     │   Event N   │ │░░░░free░░░░░│ ║
   ║ │payload│ │         payload         │     │   payload   │ │░░░░space░░░░│ ║
   ║ └───────┘ └─────────────────────────┘     └─────────────┘ └─────────────┘ ║
   ╚═Payload buffer════════════════════════...═════════════════════════════════╝
```

请记住，即使在这个简单的示意图中它们看起来不是那样，真实事件负载通常（就字节数而言）比事件描述符大得多。此图主要试图展示：

* 事件描述符是\_固定大小\_的，事件负载是\_可变大小\_的
* 事件描述符引用/"指向"其负载的位置
* 事件描述符和负载位于不同的共享内存连续数组中

尽管此系统中有两个不同的环形缓冲区 —— 描述符数组和负载字节缓冲区 —— 但我们称整个组合数据结构为"event ring"。

关于所选通信风格的一些特性：

* 它支持\_广播\_语义：多个读取者可以同时从 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` 中定义为：

```c theme={null}
/// First event recorded when transaction processing starts
struct monad_exec_txn_header_start {
    monad_c_bytes32 txn_hash;     ///< Keccak hash of transaction RLP
    monad_c_address sender;       ///< Recovered sender address
    struct monad_c_eth_txn_header
        txn_header;               ///< Transaction header
};
```

嵌套的 `monad_c_eth_txn_header` 结构包含大多数有趣的信息 —— 它在 `eth_ctypes.h` 中定义如下：

```c theme={null}
/// Fields of an Ethereum transaction that are recognized by the monad EVM
/// implementation.
///
/// This type contains the fixed-size fields present in any supported
/// transaction type. If a transaction type does not support a particular field,
/// it will be zero-initialized.
struct monad_c_eth_txn_header {
    enum monad_c_transaction_type
        txn_type;                        ///< EIP-2718 transaction type
    monad_c_uint256_ne chain_id;         ///< T_c: EIP-155 blockchain identifier
    uint64_t nonce;                      ///< T_n: num txns sent by this sender
    uint64_t gas_limit;                  ///< T_g: max usable gas (upfront xfer)
    monad_c_uint256_ne max_fee_per_gas;  ///< T_m in EIP-1559 txns or T_p (gasPrice)
    monad_c_uint256_ne
        max_priority_fee_per_gas;        ///< T_f in EIP-1559 txns, 0 otherwise
    monad_c_uint256_ne value;            ///< T_v: wei xfered or contract endowment
    monad_c_address to;                  ///< T_t: recipient
    bool is_contract_creation;           ///< True -> interpret T_t == 0 as null
    monad_c_uint256_ne r;                ///< T_r: r value of ECDSA signature
    monad_c_uint256_ne s;                ///< T_s: s value of ECDSA signature
    bool y_parity;                       ///< Signature Y parity (see YP App. F)
    monad_c_uint256_ne
        max_fee_per_blob_gas;            ///< EIP-4844 contribution to max fee
    uint32_t data_length;                ///< Length of trailing `data` array
    uint32_t blob_versioned_hash_length; ///< Length of trailing `blob_versioned_hashes` array
    uint32_t access_list_count;          ///< # of EIP-2930 AccessList entries
    uint32_t auth_list_count;            ///< # of EIP-7702 AuthorizationList entries
};
```

注释中的正式命名法（例如 `T_n` 和 `T_c`）引用了[以太坊黄皮书](https://ethereum.github.io/yellowpaper/paper.pdf)中的变量名。

类型 `monad_c_uint256_ne`（"native endian"，本机字节序）是一个 256 位整数，以大多数性能良好的"大整数"库使用的[肢体格式](https://gmplib.org/manual/Integer-Internals)存储为 `uint64_t[4]`。

<Note title="Rust 中的事件负载类型">
  如果您使用 Rust SDK，则在构建 `monad-exec-events` 包时会由 [bindgen](https://docs.rs/bindgen/latest/bindgen/) 生成具有相同名称（并且由于 `#[repr(C)]` 属性具有相同二进制布局）的 `struct` 类型。执行事件负载的决定性特征是它们依赖于简单 C 数据结构在编程语言之间的"自然"互操作性。

  大多数流行的编程语言都定义了与 C 代码一起工作的外部函数接口，这通常也伴随着以某种方式"自然"处理 C 结构类型。虽然 C 的数据表示不可移植，但这些对象位于共享内存中，因此读取者和写入者必须在同一主机上，并且必须遵循相同的 C ABI。
</Note>

### 可变长度尾部数组和后续事件

对于特定的"交易开始"事件，\_大多数\_事件负载将是 `struct monad_exec_txn_header_start` 值的低级字节表示。几乎总是在负载字节数组中此结构\_之后\_会有一些额外数据：

* 交易的可变大小 `data` 字节数组，其长度由 `data_length` 字段指定，也是事件负载的一部分，紧跟在 `struct monad_exec_txn_header_start` 对象之后
* 如果这是 EIP-4844 交易，`blob_versioned_hashes` 数组将紧跟在 `data` 数组之后

这两者都是"可变长度尾部"（VLT）数组负载数据的示例；"尾部"意味着简单的可变长度数组在固定大小负载结构之后记录，该负载结构（除其他事项外）必须包含描述该数组长度的字段；如果有多个 VLT 数组，则按其对应的 `_length` 字段在固定大小结构中的列出顺序记录。

EIP-2930 和 EIP-7702 列表也是交易中的可变长度项目，但它们\_不\_记录在"交易头开始"事件的负载中。

它们不作为尾部数组记录，而是为每个 EIP-2930 访问列表条目和每个 EIP-7702 授权元组记录一个唯一事件。这些事件的数量\_确实\_发布在"交易头开始"事件负载中（参见 `access_list_count` 和 `auth_list_count` 字段），以便读取者知道预期还有多少事件。

### 描述符中的执行事件属性

到目前为止我们已经讨论了"交易开始"事件的负载，但事件的通用属性直接记录在事件描述符中。最重要的是，这些包括标识事件类型的数字代码，以便我们知道我们应该首先将非结构化负载字节解释为 `struct monad_exec_txn_header_start`。

事件描述符定义如下：

```c theme={null}
struct monad_event_descriptor
{
    alignas(64) uint64_t seqno;  ///< Sequence number, for gap/liveness check
    uint16_t event_type;         ///< What kind of event this is
    uint16_t : 16;               ///< Unused tail padding
    uint32_t payload_size;       ///< Size of event payload
    uint64_t record_epoch_nanos; ///< Time event was recorded
    uint64_t payload_buf_offset; ///< Unwrapped offset of payload in p. buf
    uint64_t content_ext[4];     ///< Extensions for particular content types
};
```

对于"交易头开始"事件，`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]

[^1]: 此编码及其存储在描述符中的理由在文档的其他地方描述，即描述[流标签](/zh/execution-events/event-ring#flow-tags-the-content_ext-fields-in-execution-event-rings)的章节。

`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` 事件，带有可能访问的存储键的可变长度尾部数组。

```text theme={null}
                                      ╔═Payload buffer══════════════════════════════╗
                                      ║                                             ║
                                      ║  ┏━━━━━━━TXN_HEADER_START payload━━━━━━━━┓  ║
                                      ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
                                  ┌───╬──╋─▶─monad_exec_txn_header_start───────┐░┃  ║
                                  │   ║  ┃░│                                   │░┃  ║
                                  │   ║  ┃░│ monad_c_bytes32 txn_hash;         │░┃  ║
                                  │   ║  ┃░│ monad_c_address sender;           │░┃  ║
                                  │   ║  ┃░│ struct monad_c_eth_txn_header     │░┃  ║
  ╔═Event descriptor array════╗   │   ║  ┃░│     txn_header;                   │░┃  ║
  ║                           ║   │   ║  ┃░├───────────────────────────────────┤░┃  ║
  ║ ┌───────────────────────┐ ║   │   ║  ┃░│                                   │░┃  ║
  ║ │ seqno: 1              □─╬───┘   ║  ┃░│     Transaction data variable     │░┃  ║
  ║ │ TXN_HEADER_START      │ ║       ║  ┃░│       length trailing array       │░┃  ║
  ║ └───────────────────────┘ ║       ║  ┃░│                                   │░┃  ║
  ║                           ║       ║  ┃░│                                   │░┃  ║
  ║ ┌───────────────────────┐ ║       ║  ┃░└───────────────────────────────────┘░┃  ║
  ║ │ seqno: 2              □─╬────┐  ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
  ║ │ TXN_ACCESS_LIST_ENTRY │ ║    │  ║  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛  ║
  ║ └───────────────────────┘ ║    │  ║                                             ║
  ║                           ║    │  ║  ┏━━━━━TXN_ACCESS_LIST_ENTRY payload━━━━━┓  ║
  ║ ┌───────────────────────┐ ║    │  ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
  ║ │ seqno: 3              │ ║    └──╬──╋─▶─monad_exec_txn_access_list_entry──┐░┃  ║
  ║ │ TXN_ACCESS_LIST_ENTRY □─╬────┐  ║  ┃░│                                   │░┃  ║
  ║ └───────────────────────┘ ║    │  ║  ┃░│ uint32_t index;                   │░┃  ║
  ║                           ║    │  ║  ┃░│ struct monad_c_access_list_entry  │░┃  ║
  ║ ┌───────────────────────┐ ║    │  ║  ┃░│     entry;                        │░┃  ║
  ║ │ seqno: 4              │ ║    │  ║  ┃░├───────────────────────────────────┤░┃  ║
  ║ │ TXN_HEADER_END        │ ║    │  ║  ┃░│       Storage key variable        │░┃  ║
  ║ └───────────────────────┘ ║    │  ║  ┃░│       length trailing array       │░┃  ║
  ║                           ║    │  ║  ┃░└───────────────────────────────────┘░┃  ║
  ║                           ║    │  ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
  ║                           ║    │  ║  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛  ║
  ║                           ║    │  ║                                             ║
  ║                           ║    │  ║  ┏━━━━━TXN_ACCESS_LIST_ENTRY payload━━━━━┓  ║
  ║                           ║    │  ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
  ║                           ║    └──╬──╋─▶─monad_exec_txn_access_list_entry──┐░┃  ║
  ║                           ║       ║  ┃░│                                   │░┃  ║
  ║                           ║       ║  ┃░│ uint32_t index;                   │░┃  ║
  ╚═══════════════════════════╝       ║  ┃░│ struct monad_c_access_list_entry  │░┃  ║
                                      ║  ┃░│     entry;                        │░┃  ║
                                      ║  ┃░├───────────────────────────────────┤░┃  ║
                                      ║  ┃░│                                   │░┃  ║
                                      ║  ┃░│       Storage key variable        │░┃  ║
                                      ║  ┃░│       length trailing array       │░┃  ║
                                      ║  ┃░│      (this has more storage       │░┃  ║
                                      ║  ┃░│        keys and is larger)        │░┃  ║
                                      ║  ┃░│                                   │░┃  ║
                                      ║  ┃░└───────────────────────────────────┘░┃  ║
                                      ║  ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃  ║
                                      ║  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛  ║
                                      ║                                             ║
                                      .                                             .
                                      .                                             .
                                      .                                             .
                                      ║                                             ║
                                      ╚═════════════════════════════════════════════╝
```

### 执行事件序列化中的模式

为什么 EIP-2930 条目被记录为单独的 `TXN_ACCESS_LIST_ENTRY` 事件，而不是作为 `TXN_HEADER_START` 中的可变长度尾部数组？因为涉及两级可变长度信息。有可变数量的 EIP-2930 账户，然后对每个账户又有可变数量的关联存储键。

事件序列化协议力求非常简单：只有当数组\_元素\_类型为固定大小时，才会记录可变长度尾部数组。特别地，形如[锯齿状数组](https://en.wikipedia.org/wiki/Jagged_array)的数据在事件负载中是不允许的。

每当可变性有多个维度时，通过使用更多不同的事件来"分解"它们。权衡是在事件较少但编码更复杂 vs. 更多事件将数据"展开"为"更扁平"形状之间做选择。后者更适合"零拷贝 C ABI"数据模型。[^2]

[^2]: 锯齿状数组的自然 C 编码需要指针数组。除非我们显式控制 event ring 文件被映射到虚拟地址空间的地址（例如使用 `MAP_FIXED`），否则这在共享内存结构中无法工作。

从技术上讲，EIP-7702 授权列表\_可以\_表示为可变长度尾部数组，因为授权元组是固定大小的。然而，作为设计决策，可变长度尾部数组仅允许具有简单元素类型（如 `u8` 或 `uint256`），且数量不能太多。

VLT 数组的解码逻辑往往容易出错；它看起来令人困惑是因为在代码中更难"看清"序列化规则究竟是什么。将数据"展开"为更多事件更具自解释性：创建不同类型的对象，而不是依赖隐式解析规则来重新解释非结构化尾部数据。

因此，VLT 数组仅在其使用看起来"显而易见"时才使用，例如每个 `EIP-2930` 访问列表条目中的存储键数组。
