Skip to main content

模块

Rust 执行事件 API 分为两个库包:
  1. monad-event-ring - 此包提供核心 event ring 功能。回忆一下,event ring 是基于共享内存通信的通用广播工具,与其包含的事件数据类型无关。因此,此包_不_包含执行事件类型(或任何其他事件类型)的定义
  2. monad-exec-events - 执行事件数据类型在此库中定义,还有一些用于编写实时数据应用的实用工具
这些库比在 C 中更结构化地组合在一起。 在 C API 中,event ring API 处理非结构化数据,例如事件数字代码是 uint16_t 值,事件负载是原始字节数组。读取者执行未检查的类型强制转换以重新解释这些字节的含义。有一些安全机制来检查 event ring 文件是否包含正确类型的数据,但内容类型在类型系统中没有强表示。 在 Rust API 中,event ring 不仅是一般意义上的”泛型”;它是一种字面上的泛型类型:
event ring 由”解码器”显式参数化。解码器知道如何解释特定事件内容类型(例如执行事件)的原始字节。

核心概念

事件枚举类型

考虑 C API 中的解码工作方式:这通常是”巨大 switch 语句”模式,我们检查事件的数字代码并通过未检查的类型转换将原始字节重新解释为适当的负载类型:
Rust 表达这一点的方式是使用 enum 类型:不同类型的事件负载成为枚举的变体,switch 逻辑被更强大的 match 替代。 在 Rust 中,解码产生一个枚举类型 ExecEvent 的值,定义如下:
请注意 ExecEvent 的每个变体都持有一个值,其类型名称类似于 C 事件负载结构。例如,struct monad_exec_block_start 是 C API 中的事件负载结构定义。它在新区块开始时被记录,定义于文件 exec_event_ctypes.h 使用完全相同的 C 结构名称 —— 包括 monad_exec 前缀和小写、蛇形拼写 —— 旨在提醒您负载类型与其 C API 对应物具有_完全_相同的内存表示。它们由 bindgen 生成,并且(通过 #[repr(C)] 属性)与同名 C 类型布局兼容。

event ring 和 'ring 引用生命周期

EventRing 是一种 RAII 句柄类型:当您创建 EventRing 实例时,会为您的进程添加该 event ring 文件的新共享内存映射。同样,当调用 EventRing::drop 时,这些共享内存映射被移除。此时任何指向共享内存的指针或引用都需要失效。 我们依靠 Rust 的内置引用生命周期分析框架来表达这一点。指向 event ring 共享内存中数据的引用始终携带称为 'ring 的引用生命周期。此生命周期对应于 EventRing 对象本身的生命周期。由于 EventRing 通过存活将共享内存映射固定在原位,'ring 的真正含义通常可以视为”共享内存生命周期”,两者相同。

零拷贝 API 和”事件引用”枚举类型

在前面的部分中,我们讨论了解码后的执行事件类型 enum ExecEvent。有第二种具有类似设计的类型称为 enum ExecEventRef<'ring>;用于零拷贝 API。 为了比较两者,这里是 ExecEvent 类型:
这是 ExecEventRef<'ring> 类型:
前者包含事件负载的_拷贝_,而后者直接引用共享内存负载缓冲区中的字节。通过使用 ExecEventRef<'ring>,您可以避免拷贝可能大量的数据,例如特别大的 EVM 日志或 call frame。如果您反正要过滤掉大多数事件,这是有价值的。 “事件引用”枚举类型提供更好的性能,但有两个缺点:
  1. 因为它有引用生命周期作为泛型参数,可能更难使用(即更多地违反借用检查器)
  2. 直接存在于负载缓冲区中的数据可能随时被覆盖,因此您不应依赖它在您首次查看后长时间仍然存在

拷贝与零拷贝负载 API

拷贝与零拷贝的决定仅适用于事件负载;事件描述符很小,始终被拷贝。有两种方式在获得事件描述符后读取其负载:
  1. 拷贝风格 EventDescriptor::try_read - 这将返回 EventPayloadResult 枚举类型,它包含”成功”变体(EventPayloadResult::Ready)或”失败”变体(EventPayloadResult::Expired);前者包含 ExecEvent 负载值,后者表明负载已丢失
  2. 零拷贝风格 EventDescriptor::try_filter_map - 您向此方法传递一个非捕获闭包,它以指向共享内存中事件负载的 ExecEventRef<'ring> 引用被回调;由于您的闭包不能捕获任何东西,您对事件负载做出反应的唯一方式是返回类型 T 的某个值 vEventDescriptor::try_filter_map 本身返回 Option<T>,按以下方式使用:
    • 如果在调用您的闭包之前负载已过期,则从不调用您的闭包,try_filter_map 返回 Option::None
    • 否则运行您的闭包,其返回值 v: T 被移动到 try_filter_map 函数中
    • 如果您的闭包运行后负载仍有效,则通过返回 Option::Some(v) 将值传递给调用者,否则返回 Option::None

为什么使用非捕获闭包?

零拷贝 API 的模式通常是这样工作的:
  • 创建对 event ring 负载缓冲区中数据的引用(e: &'ring E)并检查是否过期;如果未过期…
  • … 基于事件负载值计算一些东西,即计算 let v = f(&e)
  • 一旦 f 完成,再次检查负载是否过期;如果_现在_已过期,那么它_可能_在计算 v = f(&e) 期间的某个时刻过期;我们唯一能做的安全事情是丢弃计算的值 v,因为我们无法知道过期究竟何时发生
如果允许您在零拷贝闭包中捕获变量,您可以从库的负载过期检测检查之外”偷运”计算结果。也就是说,如果库后来检测到负载在您的闭包运行期间某处被覆盖,它无法保证的方式”污染”您偷运出去的值。它只能_建议_您不要信任它,但这容易出错。 惯用 Rust 倾向遵循”默认正确”风格,防范此类不安全模式。在零拷贝 API 中,您只能通过返回值通信,因为您不能捕获任何东西。这样,如果库后来发现它作为输入给您的负载已过期,它可以决定根本不将返回值传递回给您。

Rust API 中的重要类型

API 中有六种核心类型:
  1. event ring EventRing<D: EventDecoder> - 给定 event ring 文件的路径,您创建其中一个以获取对该文件中 event ring 共享内存段的访问;您通常使用类型别名 ExecEventRing,它是 EventRing<ExecEventDecoder> 的语法糖
  2. 事件读取器 EventReader<'ring, D: EventDecoder> - 这是类似迭代器的类型,用于读取事件;它称为”读取器”而非”迭代器”是因为 Iterator 在 Rust 中已有特定含义;事件读取器有比 Rust 迭代器更复杂的返回类型,因为它具有”轮询”风格:其 next() 的等价物 —— 称为 next_descriptor() —— 可以返回事件描述符、报告 gap 或指示尚无新事件就绪
  3. 事件描述符 EventDescriptor<'ring, D: EventDecoder> - 如果下一个事件成功读取,事件读取器产生其中一个;回忆一下,事件描述符包含事件的通用字段,并存储读取事件负载和检查是否过期所需的数据;在 Rust API 中,读取负载是通过在事件描述符上定义的方法完成的
  4. 事件解码器 trait EventDecoder - 您不会直接使用它,但实现此特征的类型 —— 在执行 event ring 情况下为 ExecEventDecoder —— 包含如何解码事件负载的所有逻辑
  5. 事件枚举类型(关联类型 EventDecoder::EventEventDecoder::EventRef)- 这些提供事件的”拷贝”和”零拷贝”解码形式;在 ExecEventDecoder 情况下,ExecEvent 是”拷贝”类型,ExecEventRef<'ring> 是零拷贝(共享内存引用)类型
  6. 执行事件负载类型monad_exec_block_start 等)- 这些是 bindgen 生成的 #[repr(C)] 事件负载类型,与其 C API 对应物匹配

区块级实用工具

ExecutedBlockBuilder

执行事件是细粒度的:EVM 采取的大多数操作都会发布一个描述该操作的事件,例如,每个 EVM 日志都作为单独的 ExecEvent::TxnLog 事件发布。事件几乎在可用时立即流式传输给消费者,因此区块的实时数据”逐片”到来。 一个名为 ExecutedBlockBuilder 的实用工具会将这些事件聚合回单个以区块为中心的更新,如果用户偏好处理完整区块。区块表示中的数据类型也是 alloy_primitives 类型,在 Rust 中处理更符合人体工学。

CommitStateBlockBuilder

推测实时数据章节所述,EVM 在能够发布时立即发布执行事件,这意味着它通常发布关于推测执行区块的数据。我们不知道这些区块是否会附加到区块链,因为共识决策与区块的执行并行发生(并且将比其晚完成)。 CommitStateBlockBuilderExecutedBlockBuilder 的基础上还跟踪区块在共识生命周期中的提交状态。区块更新本身通过 Arc<ExecutedBlock> 传递,因此拷贝对它的引用很廉价。当区块提交状态更改时,您会收到描述新状态的更新以及对 Arc<ExecutedBlock> 本身的另一个引用。 推测实时数据指南经常指出区块放弃不由事件系统显式传达(例如此处此处)。然而,CommitStateBlockBuilder _确实_报告失败提议的显式放弃,因为它是更高级别、用户友好的实用工具。