> ## 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.

# event ring 详解

## event ring 文件和内容类型

event ring 由四个共享内存段组成。其中两个 —— 事件描述符数组和负载缓冲区 —— 在[概述文档](/zh/execution-events/overview)中已描述。第三个共享内存段包含描述 event ring 元数据的头部。第四个（"context area"）是执行事件不需要的特殊功能。

共享内存段通过 [mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html) 映射到进程的地址空间。这意味着 event ring 的数据结构存放在某个文件中，通过创建该文件的共享内存映射来获得对它的共享访问。

大多数时候，event ring 是位于名为 [hugetlbfs](https://lwn.net/Articles/375096/) 的特殊内存文件系统上的常规文件。hugetlbfs 类似于 [tmpfs](https://man7.org/linux/man-pages/man5/tmpfs.5.html) 内存文件系统，但支持创建由大页支持的文件。使用大页只是一种[优化](https://lwn.net/Articles/374424/)：event ring 文件可以创建在任何文件系统上。如果告知执行层守护进程在不支持 hugetlb mmap 的文件系统上创建 event ring 文件，它会记录一条性能警告但仍会创建文件。要了解 hugetlbfs 及其使用方式，请阅读[此页面](/zh/execution-events/advanced#location-of-event-ring-files)。

### event ring 配置

要使用执行事件，必须使用如下命令行参数启动执行层守护进程：

```text theme={null}
--exec-event-ring [<event-ring-configuration-string>]
```

若无此命令行参数，执行将不会发布任何事件。此命令行参数（以及挂载 hugetlbfs 文件系统）不属于执行层守护进程的默认配置说明。一份[单独的指南](https://validator-docs.vercel.app/docs/full_node/events-and-websockets)涵盖了挂载 hugetlbfs 文件系统以及修改 systemd 单元配置文件中的命令行。

请注意，配置字符串是可选的；如果传递不带参数的 `--exec-event-ring`（这是推荐做法），则等同于传递 `--exec-event-ring monad-exec-events`，其中 `monad-exec-events` 是默认的执行 event ring 文件名。

event ring 配置字符串具有以下形式：

```text theme={null}
<ring-name-or-file-path>[:<descriptor-shift>:<payload-buffer-shift>]
```

换句话说，配置字符串由三个用 `:` 分隔的字段组成；第一个字段是必需的，但后两个是可选的。以下是仅带第一个字段的命令行参数示例：

```text theme={null}
--exec-event-ring /var/lib/hugetlbfs/user/monad/pagesize-2MB/event-rings/monad-exec-events
```

以下是包含所有三个字段的另一个示例：

```text theme={null}
--exec-event-ring monad-exec-events:21:29
```

第一个字段是 event ring 文件的名称。执行层守护进程以两种不同方式解释此字段：

* 如果它纯粹是一个文件名 —— 即路径不包含任何 `/` 字符 —— 则将其解释为存在于默认 event ring 文件目录中的文件；这是由 API 函数 `monad_event_open_ring_dir_fd` 返回的目录；它使用 [libhugetlbfs](https://github.com/libhugetlbfs/libhugetlbfs) 定位最合适的 hugetlbfs 文件系统挂载点，并自动在其下创建一个名为 `event-rings` 的子目录（如果尚不存在）（有关更多信息，请参见[此处](/zh/execution-events/advanced#location-of-event-ring-files)）；event ring 文件将在该 `event-rings` 子目录中创建
* 如果路径有多个路径组件 —— 即包含至少一个 `/` 字符 —— 则将按原样使用此路径，\_即使\_它不驻留在 hugetlbfs 文件系统上；下面解释了有人可能希望这样做的理由

"shift"参数是决定 event ring 大小的 2 的幂指数。[^1] `<descriptor-shift>` 为 21 意味着 ring 的事件描述符数组中将有 2^21 个描述符。这意味着在描述符环形缓冲区回卷并覆盖旧的事件描述符之前，可以写入大约 200 万个事件。

`<payload-buffer-shift>` 为 29 意味着负载缓冲区数组中将有 2^29 字节。这意味着在负载缓冲区回卷并覆盖旧事件的负载之前，可以记录相当于 512 MiB 的事件负载。

如果未指定 event ring 大小参数，则使用默认值。您为什么要将这些值从默认值调高？如果您的读取程序崩溃但执行未崩溃，那么在您的程序未运行期间您很可能会错过一些事件。您的应用程序也许不关心旧区块的丢失事件，但如果关心，您需要以某种方式检索它们。

如果没有过去太长时间，很有可能您错过的事件仍然在 event ring 内存中，即还未被覆盖。默认大小足以容纳 10k TPS 下几分钟的区块。增大这些值使您能够回溯到更远的过去。

请注意，大页池的大小是固定的。可以通过修改系统的配置来更改池大小，参见[此处](https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt)对 `/proc/sys/vm/nr_hugepages` 的讨论。

如果 event ring 创建在 hugetlbfs 挂载上，且其大小超出可用大页数量，则执行层守护进程将退出并显示报告"no space left on device"错误的错误消息。例如，此处我们尝试分配一个具有 1TB 负载缓冲区的 event ring：

```text theme={null}
LOG_ERROR  event library error -- monad_event_ring_init_simple@event_ring_util.c:78:
posix_fallocate failed for event ring file `/dev/hugepages/monad-exec-events`, size 1099647942656: No space left on device (28)
```

如果您恰好有几 TB 可用主内存，您可以传递一个包含 `/` 字符的文件路径，指向 tmpfs 挂载上的文件，这样也能工作，例如 `--exec-event-ring /my-giant-tmpfs/monad-exec-events`。

如果您需要回溯特别远 —— 或如果因执行本身崩溃而错过事件 —— 则需要使用其他地方描述的替代恢复方法。[^2]

[^1]: 之所以称为 "shifts"，是因为 `1UL << x` 等于 `2^x`

[^2]: 替代恢复方法仍在开发中，将在下一版 SDK 中提供

### event ring 文件格式

event ring 文件格式很简单：所有四个部分按顺序排列并对齐到大页边界，头部描述每个部分的大小。

```text theme={null}
╔═Event ring file══╗
║ ┌──────────────┐ ║
║ │              │ ║
║ │    Header    │ ║
║ │              │ ║
║ ├──────────────┤ ║
║ │              │ ║
║ │    Event     │ ║
║ │  Descriptor  │ ║
║ │    Array     │ ║
║ │              │ ║
║ ├──────────────┤ ║
║ │              │ ║
║ │              │ ║
║ │              │ ║
║ │              │ ║
║ │   Payload    │ ║
║ │    Buffer    │ ║
║ │              │ ║
║ │              │ ║
║ │              │ ║
║ │              │ ║
║ │              │ ║
║ ├──────────────┤ ║
║ │              │ ║
║ │   Context    │ ║
║ │     Area     │ ║
║ │              │ ║
║ └──────────────┘ ║
╚══════════════════╝
```

描述符数组只是 `struct monad_event_descriptor` 对象的数组，负载缓冲区是扁平字节数组（即类型为 `uint8_t[]`）。头部结构定义如下：

```c theme={null}
/// Event ring shared memory files start with this header structure
struct monad_event_ring_header
{
    char magic[6];                           ///< 'RINGvv', vv = version number
    enum monad_event_content_type
        content_type;                        ///< Kind of events in this ring
    uint8_t schema_hash[32];                 ///< Ensure event definitions match
    struct monad_event_ring_size size;       ///< Size of following structures
    struct monad_event_ring_control control; ///< Tracks ring's state/status
};
```

### 事件内容类型

需要 `content_type` 头字段是因为 event ring 库（读取和写入 API）执行非结构化 I/O：函数读取和写入原始 `uint8_t[]` 事件负载，事件描述符包含普通的 `uint16_t` 数字事件代码。与 UNIX 的 `read(2)` 和 `write(2)` 文件 I/O 系统调用非常相似，event ring API 函数本身并不知道它们所处理数据的格式。这也是事件描述符中 `event_type` 字段是通用整数类型 `uint16_t` 而不是 `enum monad_exec_event_type` 的原因。

这里的假设是读取者和写入者都知道所处理数据的二进制格式，并在需要时通过类型强制转换将原始数据视为该格式，例如：

```c theme={null}
const struct monad_exec_block_start *block_start = nullptr;

// We assume that the event ring file we opened contains execution events,
// and thus further assume that it makes sense to compare `event->event_type`
// to a value of type `enum monad_exec_event_type`
if (event->event_type == MONAD_EXEC_BLOCK_START) {
    // Since this is MONAD_EXEC_BLOCK_START, we can cast the `const void *`
    // payload to a `const struct monad_exec_block_start *` payload.
    // Note: implicit type-casting from `void *` is allowed in C, but not C++
    block_start = monad_event_ring_payload_peek(event_ring, event);
}
```

我们需要某种错误检测机制来确保这样做是安全的。event ring 文件头包含一个"content type"枚举常量，说明它包含哪种事件数据：

```c theme={null}
enum monad_event_content_type : uint16_t
{
    MONAD_EVENT_CONTENT_TYPE_NONE,  ///< An invalid value
    MONAD_EVENT_CONTENT_TYPE_TEST,  ///< Used in simple automated tests
    MONAD_EVENT_CONTENT_TYPE_EXEC,  ///< Core execution events
    MONAD_EVENT_CONTENT_TYPE_PERF,  ///< Performance tracer events
    MONAD_EVENT_CONTENT_TYPE_COUNT  ///< Total number of known event rings
};
```

执行事件始终记录到 `content_type` 等于 `MONAD_EVENT_CONTENT_TYPE_EXEC` 的 ring 中。

### 二进制 schema 版本控制：`schema_hash` 字段

如果 `content_type` 等于 `MONAD_EVENT_CONTENT_TYPE_EXEC`，那么我们知道一个 ring 应该包含执行事件，但如果事件负载定义发生变化怎么办？或如果 `enum monad_exec_event_type` 中的枚举常量发生变化怎么办？

假设某用户使用某个特定版本的 `exec_event_ctypes.h`（定义执行事件负载和事件类型枚举的文件）编译了他们的应用程序。

现在想象一段时间后，该用户部署了新版本的执行节点，它使用不同版本的 `exec_event_ctypes.h` 编译，导致事件负载的内存表示形式不同。

如果读取者忘记用新头文件重新编译他们的应用程序，就可能误解事件负载中的字节，假设它们按其旧（编译时）版本的 `exec_event_ctypes.h` 中的旧布局排列。

为防止此类错误，所有事件负载的二进制布局通过一个哈希值汇总，只要对该内容类型的任何事件负载进行了更改，该哈希值就会变化。除负载更改外，对 `enum monad_exec_event_type` 的任何更改也会生成新哈希。

此机制称为"schema hash"，哈希值以全局只读字节数组形式存在于库代码内部（定义于 `exec_event_ctypes_metadata.c`）。

如果此数组中的哈希值与 event ring 文件头中的哈希值不匹配，则二进制格式不兼容。

一个名为 `monad_event_ring_check_content_type` 的辅助函数用于检查 event ring 文件是否具有预期的内容类型，以及该内容类型的预期 schema 哈希。以下是 `eventwatch.c` 示例程序中调用它的示例：

```c theme={null}
struct monad_event_ring exec_ring;

/* initialization of `exec_ring` not shown */

if (monad_event_ring_check_content_type(
        &exec_ring,
        MONAD_EVENT_CONTENT_TYPE_EXEC,
        g_monad_exec_event_schema_hash) != 0) {
    errx(EX_SOFTWARE, "event library error -- %s",
         monad_event_ring_get_last_error());
}
```

如果 event ring 类型不是 `MONAD_EVENT_CONTENT_TYPE_EXEC`，或者文件头中的 `schema_hash` 与全局数组 `uint8_t g_monad_exec_event_schema_hash[32]` 中的值不匹配，则此函数将返回 `errno(3)` 域代码 `EPROTO`。

## 事件描述符详解

### 二进制格式

事件描述符定义如下：

```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 ring 中的 `content_ext` 字段

对于每种内容类型，我们可能希望直接在事件描述符中发布额外数据，例如，如果该数据对每种负载类型都是通用的，或者它可以帮助读取者快速过滤掉他们不感兴趣的事件而无需检查事件负载。这些额外数据存储在 `content_ext`（"content extensions"）数组中，其含义由 `content_type` 定义。

对于执行 event ring，有时会填充 `content_ext` 数组的前三个值。数组中每个索引处的值具有下列枚举类型描述的语义含义，该枚举类型定义于 `exec_event_ctypes.h`：

```c theme={null}
/// Stored in event descriptor's `content_ext` array to tag the
/// block & transaction context of event
enum monad_exec_flow_type : uint8_t
{
    MONAD_FLOW_BLOCK_SEQNO = 0,
    MONAD_FLOW_TXN_ID = 1,
    MONAD_FLOW_ACCOUNT_INDEX = 2,
};
```

例如，如果我们有一个事件描述符

```c theme={null}
struct monad_event_descriptor event;
```

其内容通过对 `monad_event_iterator_try_next` 的调用初始化，那么 `event.content_ext[MONAD_FLOW_TXN_ID]` 将包含该事件的"交易 ID"。交易 ID 等于交易索引加一，如果事件没有关联交易（例如新区块的开始），则为零。

"flow"标签背后的思想是用其所属的上下文来标记事件。例如，当交易访问特定的账户存储键时，会发出 `STORAGE_ACCESS` 事件。

通过查看 `STORAGE_ACCESS` 事件描述符的 `content_ext` 数组，读取者可以判断它是（1）由索引为 `event.content_ext[MONAD_FLOW_TXN_ID] - 1` 的交易进行的存储访问，以及（2）访问的账户索引为 `event.content_ext[MONAD_FLOW_ACCOUNT_INDEX]`（该索引与之前已经看到的 `ACCOUNT_ACCESS` 事件系列相关）。

使用流标签有两个原因：

1. **快速过滤** - 如果我们每秒处理 10,000 笔交易，每笔交易至少有十几个事件，那么我们只有大约 10 微秒来处理每个事件，否则最终会落后并产生 gap。在这种时间尺度上，即便是接触包含事件负载的内存也是相对昂贵的。事件负载位于不同的缓存行 —— 一个尚未在读取者 CPU 中变热的行 —— 并且必须首先在缓存一致性协议中更改缓存行的所有权（因为它最近由写入者独占，现在必须与读取 CPU 共享，导致跨核总线流量）。对于大多数应用，用户可以在 `TXN_HEADER_START` 事件时识别他们感兴趣的交易 ID，然后可以忽略任何没有感兴趣 ID 的事件。由于 ID 是稠密的整数集合，可以使用简单的 `bool[TXN_COUNT + 1]` 类型数组来高效查找与该交易关联的后续事件是否感兴趣（可以通过每笔交易一位而非一个完整 `bool` 使其更高效）
2. **压缩** - `STORAGE_ACCESS` 的账户通过索引引用（该索引指向早前的 `ACCOUNT_ACCESS` 事件），因为账户地址是 20 字节：太大以至于无法容纳在剩余的两个 `content_ext` 数组槽位中

压缩技术还用于在 `event.content_ext[MONAD_FLOW_BLOCK_SEQNO]` 中存储与事件相关的区块。此情况下的 flow 标签是启动关联区块的 `BLOCK_START` 事件的\_序列号\_。关于此流标签的几点说明：

* 有时它为零（无效序列号），这意味着该事件不与任何区块相关联；虽然大多数事件的作用域限定在某个区块内，但共识状态更改事件（`BLOCK_QC`、`BLOCK_FINALIZED` 和 `BLOCK_VERIFIED`）不在区块内发生
* 请注意，区块流标签\_不是\_区块编号。这是因为在看到事件时，区块处于 "proposed" 状态，共识算法尚未完成对该区块是否会被纳入规范区块链的投票（这将在下一节中详细讨论）。在区块变为 finalized 之前，唯一明确引用它的方式是通过其唯一 ID，即 32 字节哈希值（可从 `BLOCK_START` 负载中读取）；因此区块流标签也是一种压缩形式
* 拥有序列号使我们能够将迭代器倒回到区块的开始，如果我们在区块中间开始观察事件序列（例如，如果读取者在执行层守护进程之后启动）。可以在 `eventwatch.c` 示例程序中的 `find_initial_iteration_point` 函数中找到此示例（及其详细说明）
