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

# 在快照数据上运行示例程序

熟悉执行事件系统的最简单方法是试用示例程序并阅读代码，尽管您可能需要先阅读解释基本概念的[快速概述](/zh/execution-events/overview)。

如果您按顺序遵循本指南，您应该已经构建了其中一个示例程序。如果还没有构建，请选择适合您所选语言的指南（[C](/zh/execution-events/getting-started/c) 或 [Rust](/zh/execution-events/getting-started/rust)），然后返回此页面。

## 实时 event ring 与快照 event ring

event ring 的共享内存数据结构通常存在于常规文件中。任何想要共享访问 event ring 的进程首先通过文件系统定位它，然后使用 [mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html) 系统调用将其共享视图映射到进程的虚拟内存映射中。

event ring 文件有两种类型：

1. **"实时" event ring 文件** —— 这些是作为实时数据源的"正常" event ring 文件。SDK 的整个目的是从这些文件中读取实时事件，但它们对于大多数日常软件开发任务并不方便。例如，假设您想为数据处理程序编写测试。SDK 主要是围绕\_读取\_事件设计的，因此要用实时 event ring 测试它，您需要编写一些虚拟事件发布代码来产生要读取的事件。对于执行事件，实时 event ring 文件由执行层守护进程填充，而在教程的这一步我们甚至还没安装它！第二种 event ring 文件解决了很多开发上的头痛。
2. **"快照" event ring 文件** —— 这些是对实时 event ring 文件在某个特定时刻的压缩快照。通常它们被"倒回"到循环事件队列中最旧的事件，用于重放一组固定的历史执行事件。快照文件对测试和开发工作流很有用，因为您不需要运行活动的发布方即可使用它们。因为它们对开发非常有用，快照是我们在实时节点上试用示例程序之前将使用的第一个数据源。

## 在快照文件上运行示例程序

### 步骤 1：下载快照文件

运行此命令以下载快照：

```shell theme={null}
$ curl https://raw.githubusercontent.com/category-labs/monad/refs/tags/release/exec-events-sdk-v1.1/rust/crates/monad-exec-events/test/data/exec-events-emn-30b-15m/snapshot.zst > /tmp/exec-events-emn-30b-15m.zst
```

文件名的 `emn-30b-15m` 部分意为"以太坊主网重放：从 1500 万区块之后的 30 个区块"。换句话说，它包含在以太坊区块链（chain ID 1）从区块 15,000,001 到区块 15,000,031 的历史重放期间发出的执行事件。

Category Labs 执行层守护进程能够执行来自 Monad 区块链（EVM chain ID 143 或其任何测试网络）的区块，也可执行来自其他 EVM 兼容网络的区块。以太坊主网的历史重放被用作执行"一致性测试"，以确保节点软件保持尽可能与以太坊兼容。

我们在教程中使用以太坊链快照，是假设许多开发者已经熟悉以太坊生态系统，但可能对 Monad 是新手。您可以检查快照文件中捕获的所有数据是否与您喜欢的以太坊数据提供商发布的数据匹配。例如，您能够检查这里显示的数据是否与 [Etherscan](https://etherscan.io/) 等网站报告的一致。

<Warning title="为什么用 `/tmp`？">
  我们的示例 `curl` 命令将快照文件放在 `/tmp` 中是有原因的。虽然文件可以放在任何地方，但我们鼓励用户不要将其放在他们当前所在的目录中，以确保他们第一次运行程序时不会遇到令人困惑的错误。

  如果文件放在当前工作目录中，且您指定为 `exec-events-emn-30b-15m.zst`，将会发生错误。如果您改为将文件引用为 `./exec-events-emn-30b-15m.zst`，错误会消失。前导 `./` 以您以前见过的方式"修复"问题：当您想在 UNIX shell 中运行一个不在 `$PATH` 中的命令时，您经常添加 `./` 以抑制默认的自动路径搜索。任何 `/` 字符将输入标记为实际文件路径，而不是要搜索的"命令名"。

  event ring 文件也发生类似情况，其中不含 `/` 的文件输入会以自动方式翻译。除非名称包含 `./` 来传达输入是路径，否则文件不会在当前目录中被"搜索"。"纯"文件名仅在称为"默认 event ring 目录"的特殊目录中被搜索。理由在 SDK 的 [event ring 文件位置](/zh/execution-events/advanced#location-of-event-ring-files) 部分中有完整解释。
</Warning>

### 步骤 2：运行您之前构建的 SDK 示例程序

命令对每种编程语言略有不同。

对于 C，运行：

```shell theme={null}
$ eventwatch /tmp/exec-events-emn-30b-15m.zst
```

对于 Rust，运行：

```shell theme={null}
cargo run -- --event-ring-path /tmp/exec-events-emn-30b-15m.zst -d
```

Rust 示例程序的输出比 C 输出更有信息量。两个程序都会"美化打印"事件描述符信息，但 C 示例程序只能十六进制转储事件负载，而 Rust 程序能够调试打印它们，这要归功于 Rust 的 `#[derive(Debug)]` 特性。Rust 命令行中的 `-d` 参数告诉程序打印这种"调试"形式。

<Note>
  C 语言家族的完整美化打印器\_确实\_存在于 SDK 中，但它们仅适用于 C++，基于标准 C++ `<format>` 库
</Note>

### 步骤 3：分析数据（仅 Rust）

如果您运行 Rust 示例程序 —— 本指南的这一步假设您在运行 —— 您将看到所有事件数据的文本转储。我们将查看几个特定事件，让您了解 SDK 生成的数据类型以及您可以用它做什么。

Rust 示例程序打印的前两行如下：

```text theme={null}
16:26:14.354056730 BLOCK_START [2 0x2] SEQ: 1 BLK: 15000001
Payload: BlockStart(monad_exec_block_start { <block-start-details> })
```

让我们分解第一行：

* `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 的一部分

第二行由此 Rust 语句产生：

```rust theme={null}
println!("Payload: {exec_event:x?}");
```

由于它是一行很长的行（Rust 的 `#[derive(Debug)]` 输出没有换行），我们在示例输出文本中对它进行了缩写。稍后我们将查看它的部分内容，但现在我们暂停一下解释关于此 `println!("Payload: {exec_event:x?}")` 语句的一些事情。

`exec_event` 是 Rust 枚举类型 `ExecEvent` 的值。以下是该枚举的定义方式：

```rust theme={null}
pub enum ExecEvent {
    RecordError(monad_event_record_error),
    BlockStart(monad_exec_block_start),
    BlockReject(monad_exec_block_reject),
    BlockPerfEvmEnter,
    BlockPerfEvmExit,
    BlockEnd(monad_exec_block_end),
    BlockQC(monad_exec_block_qc),
    BlockFinalized(monad_exec_block_finalized),
    BlockVerified(monad_exec_block_verified),
    TxnHeaderStart {
        txn_index: usize,
        txn_header_start: monad_exec_txn_header_start,
        data_bytes: Box<[u8]>,
        blob_bytes: Box<[u8]>,
    },
    // ... more enum variants follow, full definition not shown
}
```

* 调试输出以 `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`，其定义如下：

```c theme={null}
/// Event recorded at the start of EVM execution
struct monad_exec_block_start
{
    struct monad_exec_block_tag block_tag;          ///< Proposal is for this block
    uint64_t round;                                 ///< Round when block was proposed
    uint64_t epoch;                                 ///< Epoch when block was proposed
    __uint128_t proposal_epoch_nanos;               ///< UNIX epoch nanosecond timestamp
    monad_c_uint256_ne chain_id;                    ///< Blockchain we're associated with
    struct monad_c_secp256k1_pubkey author;         ///< Public key of block author
    monad_c_bytes32 parent_eth_hash;                ///< Hash of Ethereum parent block
    struct monad_c_eth_block_input eth_block_input; ///< Ethereum execution inputs
    struct monad_c_native_block_input monad_block_input; ///< Monad execution inputs
};
```

以太坊执行输入字段 `eth_block_input` 是对应于以太坊区块头中在执行开始时已知部分的字段。

此输出中的某些内容难以阅读，因为 Rust 的 `#[derive(Debug)]` 是为调试便利而设计的，并不总是以最易读的方式"美化打印"数据。但其他字段是清晰的，例如，区块的 `gas_limit` 显示为十六进制值：

```text theme={null}
monad_c_eth_block_input { <not shown...> gas_limit: 1c9c380 <...not shown> }
```

`0x1c9c380` 对应于十进制数 `30,000,000`，这是我们期望在以太坊主网 gas 限制中看到的数字。

<Info>
  事件的真正美化打印是通过一个称为 `monad-event-cli` 的开发者工具完成的，它是 SDK 的一部分。此示例旨在尽可能简单和简短，以帮助学习 API。调试真实事件程序时，您可能更倾向于使用像事件 CLI 工具这样的开发者工具。它的构建说明在"开始使用"指南的最后一步 [(此处)](/zh/execution-events/getting-started/final#optional-build-the-monad-event-cli-tool)。
</Info>

现在让我们查找一些更有趣的东西，感受一下真实的 SDK 消费者可能会用这些数据做什么。

如果您在输出中搜索字符串 `TXN_EVM_OUTPUT`，第一个匹配将是此事件（带有一些格式差异）：

```text theme={null}
16:26:14.376725676 TXN_EVM_OUTPUT [17 0x11] SEQ: 236 BLK: 15000001 TXN: 0
Payload: TxnEvmOutput { txn_index: 0, output: monad_exec_txn_evm_output {
    receipt: monad_c_eth_txn_receipt { status: false, log_count: 0, gas_used: 765c },
    call_frame_count: 1
} }
```

这是描述区块 15,000,001 中交易零的输出的第一个事件 —— 请注意描述符中的 `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`](https://github.com/ipsilon/evmc/blob/496ce0f81058378b72d0b592d1c49b935bce3302/include/evmc/evmc.h#L298) 状态码的数值。这告诉我们回滚是由合约代码本身请求的，即它执行了 [`REVERT`](https://www.evm.codes/?fork=osaka#fd) 指令。换句话说，这不是虚拟机发起的异常停止，如"gas 耗尽"或"非法指令"，而是合约本身决定做的事情。

因为这是一个 Solidity 合约，我们可以从 call frame 中解码出更丰富的错误信息。`REVERT` 指令可以将任意长度的返回数据传回调用者。这些返回数据记录在 call frame 的 `return_bytes` 数组中。

观察到 `return_bytes` 的前 4 字节是 `0x8c379a0`。这是 Solidity 表示带字符串说明的回滚的方式。此字符串的编码细节在[此处](https://docs.soliditylang.org/en/v0.8.21/control-structures.html#revert)，但要点是我们可以将此 `return_bytes` 数组的最后 32 字节解码为 ASCII 字符串。如果您自己尝试，会发现它写着：

```text theme={null}
Ownable: caller is not the owner
```

此错误字符串最终来自 [此处](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.0/contracts/access/Ownable.sol#L43)，OpenZeppelin 的抽象 "Ownable" 合约。它被用作此智能合约实现中的第三方库，以提供一些简单的访问控制。

在早前的事件（称为 `TXN_HEADER_START`）中，我们可以找到交易的 Keccak 哈希，为 `0xaedb8ef26125d8ad6e0c5f19fc9cbdd7f4a42eb82de88686b39090b8abcfeb8f`。如果我们使用该哈希在 [Etherscan](https://etherscan.io/tx/0xaedb8ef26125d8ad6e0c5f19fc9cbdd7f4a42eb82de88686b39090b8abcfeb8f) 上查询该交易的信息，可以看到 Etherscan 与我们一致。`Status:` 字段显示：

```text theme={null}
Fail with error 'Ownable: caller is not the owner'
```

欢迎使用您喜欢的探索以太坊主网数据的工具再次核实这个结果！

### 步骤 4：了解其工作原理

您刚才运行的示例程序的源代码有很多注释，旨在教您如何使用 API。学习 SDK 的最佳方式是通读它，但如果您还没读过[概述](/zh/execution-events/overview)，可能需要先读它。

您可以现在就阅读，或继续下一步，我们将安装自己的本地 [Monad 节点](/zh/execution-events/getting-started/setup-node)。一旦我们有了自己的节点，我们就可以运行同一个示例程序，但让它消费实时 Monad 区块链数据而不是快照数据。
