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

# Rust API

## Modules

The Rust execution events API is split across two library packages:

1. **`monad-event-ring`** - this package provides the core event ring
   functionality. Recall that event rings are a generic broadcast utility
   based on shared memory communication, and are agnostic about what kind
   of event data they contain. Consequently, this package does *not* include
   the definitions of the execution event types (nor any other event types)

2. **`monad-exec-events`** - the execution event data types are defined in this
   library, along with some helpful utilities for writing real-time data
   applications

These libraries fit together in a more structured way than they do in C.

In the C API, the event ring API works with unstructured data, e.g., event
numerical codes are `uint16_t` values and event payloads are raw byte arrays.
The reader performs unchecked type casts to reinterpret the meaning of those
bytes. There are some
[safety mechanisms](/execution-events/event-ring#binary-schema-versioning-the-schema_hash-field)
to check if an event ring file appears to contain the right kind of data,
but the content types are not strongly represented in the type system.

In the Rust API, the event ring is not just "generic" in the general sense
of the word; it is a literal generic type:

```rust theme={null}
struct EventRing<D: EventDecoder>
```

Event rings are explicitly parameterized by a "decoder". The decoder knows
how to interpret the raw bytes for a particular event content type, e.g.,
execution events.

## Core concepts

### Event enumeration type

Consider how decoding works in the C API: it's typically a "giant switch
statement" pattern, where we examine the numerical code of an event and
reinterpret the raw bytes as the appropriate payload type via an unchecked
type cast:

```c theme={null}
const void *payload = monad_event_ring_payload_peek(&exec_ring, &event);

switch (event.event_type) {
case BLOCK_START:
    handle_block_start((const struct monad_exec_block_start *)payload);
    break;

case BLOCK_END:
    handle_block_end((const struct monad_exec_block_end *)payload);
    break;

// ... more event types handled here
}
```

The Rust way of expressing this is to use an `enum` type: the different kinds
of event payloads become the enum's variants, and the `switch` logic is
replaced by a more-powerful `match`.

In Rust, decoding produces a value of enumeration type `ExecEvent`, which is
defined like this:

```rust theme={null}
#[derive(Clone, Debug)]
pub enum ExecEvent {
    BlockStart(monad_exec_block_start),
    BlockReject(monad_exec_block_reject),
    BlockEnd(monad_exec_block_end),
    // more variants follow
```

Notice that each variant of `ExecEvent` holds a value whose type name resembles
the C event payload structures. For example, `struct monad_exec_block_start` is
the event payload structure definition in the C API. It's recorded when a new
block starts, and is defined in the file `exec_event_ctypes.h`.

The use of these exact same C structure names -- including the `monad_exec`
prefix and  lower-case, snake-case spelling -- is designed to alert you to the
fact that the payload types have *exactly* the same in-memory representation
as their C API counterparts. They are generated by
[bindgen](https://rust-lang.github.io/rust-bindgen/) and are layout-compatible
(via a `#[repr(C)]` attribute) with the C types of the same names.

### Event rings and the `'ring` reference lifetime

`EventRing` is an RAII-handle type: when you create an `EventRing` instance,
new shared memory mapping are added to your process for that event ring file.
Likewise, when `EventRing::drop` is called, those shared memory mappings are
removed. Any pointers or references pointing into shared memory would need to
be invalidated at that point.

We rely on Rust's builtin reference lifetime analysis framework to express
this. References to data that lives in event ring shared memory always carries
a reference lifetime called `'ring`. This lifetime corresponds to the lifetime
of the `EventRing` object itself.  Since an `EventRing` pins the shared memory
mappings in place by being alive, the true meaning of `'ring` can usually be
thought of as the "shared memory lifetime", which is the same.

### Zero-copy APIs and the "event reference" enumeration type

In a previous section, we discussed the decoded execution event type,
`enum ExecEvent`. There is a second type with a similar design called
`enum ExecEventRef<'ring>`; it is used for the zero copy API.

To compare the two, here is the `ExecEvent` type:

```rust theme={null}
#[derive(Clone, Debug)]
pub enum ExecEvent {
    BlockStart(monad_exec_block_start),
    BlockReject(monad_exec_block_reject),
    BlockEnd(monad_exec_block_end),
    // more variants follow
```

And here is the `ExecEventRef<'ring>` type:

```rust theme={null}
#[derive(Clone, Debug)]
pub enum ExecEventRef<'ring> {
    BlockStart(&'ring monad_exec_block_start),
    BlockReject(&'ring monad_exec_block_reject),
    BlockEnd(&'ring monad_exec_block_end),
    // more variants follow
```

The former contains *copies* of event payloads, whereas the latter directly
references the bytes living in the shared memory payload buffer. By working
with `ExecEventRef<'ring>`, you avoid avoid copying a potentially large amount
of data, e.g., especially large EVM logs or call frames. This is valuable
if you are filtering out most events anyway.

The "event reference" enum type offers better performance, but it comes with
two drawbacks:

1. Because it has a reference lifetime as a generic parameter, it can be more
   difficult to work with (i.e., more running afoul of the borrow checker)

2. Data that lives directly in the payload buffer can be overwritten at any
   time, so you shouldn't rely on it still being there long after you first
   look at it

### Copying vs. zero-copy payload APIs

The copy vs. zero-copy decision only applies to event payloads; event
descriptors are small, and are always copied. There are two ways to read an
event's payload once you have its descriptor:

1. *Copying style* `EventDescriptor::try_read` - this will return an
   `EventPayloadResult` enum type, which either contains the "success" variant
   (`EventPayloadResult::Ready`) or the "failure" variant
   (`EventPayloadResult::Expired`); the former contains a `ExecEvent` payload
   value, and the latter indicates that the payload was lost

2. *Zero-copy style* `EventDescriptor::try_filter_map` - you pass a
   non-capturing closure to this method, and it is called back with an
   `ExecEventRef<'ring>` reference pointing to the event payload in shared
   memory; since your closure can't capture anything, the only way for you to
   react to the event payload is to return some value `v` of type `T`;
   `EventDescriptor::try_filter_map` itself returns an `Option<T>`, which is
   used in the following way:

   * If the payload has expired prior to calling your closure, then your
     closure is never called, and the `try_filter_map` returns `Option::None`

   * Otherwise your closure is run and its return value `v: T` is moved into
     the `try_filter_map` function

   * If the payload is still valid after your closure has run, then the value
     is transferred to the caller by returning `Option::Some(v)`, otherwise
     `Option::None` is returned

#### Why non-capturing closures?

The pattern of zero-copy APIs generally works like this:

* Create a reference to the data in the event ring payload buffer
  (`e: &'ring E`) and check for expiration; if not expired ...

* ... compute something based on the event payload value, i.e., compute
  `let v = f(&e)`

* Once `f` finishes, check again if the payload expired; if it is expired
  *now*, then it *may have* become expired sometime during the computation
  of `v = f(&e)`; the only safe thing we can do is discard the computed value
  `v`, since we have no way of knowing exactly when the expiration happened

If you were permitted to capture variables in the zero-copy closure, you could
"smuggle out" computations out-of-band from the library's payload expiration
detection checks. That is, if the library later detects that the payload was
overwritten sometime during when your closure was running, it would have no
guaranteed way to "poison" your smuggled out value. It could only *advise* you
not to trust it, but that is error prone.

Idiomatic Rust tends to follow a "correct by default" style, and guards against
these kinds of unsafe patterns. In the zero-copy API, you can communicate only
through return values since you cannot capture anything. This way, the library
can decide not to propagate the return value back to you at all, if it later
discovers that the payload it gave you as input has expired.

## Important types in the Rust API

There are six core types in the API:

1. **Event ring** `EventRing<D: EventDecoder>` - given a path to an event
   ring file, you create one of these to gain access to the shared memory
   segments of the event ring in that file; you typically use the type alias
   `ExecEventRing`, which is syntactic sugar for `EventRing<ExecEventDecoder>`

2. **Event reader** `EventReader<'ring, D: EventDecoder>` - this is the
   iterator-like type that is used to read events; it's called a "reader" rather
   than an "iterator" because [Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html)
   already has a specific meaning in Rust; the event reader has a more complex
   return type than a Rust iterator because it has a "polling" style: its
   equivalent  of `next()` -- called `next_descriptor()` -- can return an
   event descriptor, report a gap, or indicate that no new event is ready
   yet

3. **Event descriptor** `EventDescriptor<'ring, D: EventDecoder>` - the event
   reader produces one of these if the next event is read successfully; recall
   that the event descriptor contains the common fields of the event, and stores
   the necessary data to read the event payload and check if it's expired; in
   the Rust API, reading payloads is done using methods defined on the event
   descriptor

4. **Event decoder** `trait EventDecoder` - you don't use this directly, but
   a type that implements this trait -- `ExecEventDecoder` in the case of an
   execution event ring -- contains all the logic for how to decode event
   payloads

5. **Event enumeration types** (associated types `EventDecoder::Event` and
   `EventDecoder::EventRef`) - these give the "copy" and "zero-copy" decoded
   forms of events; in the case of the `ExecEventDecoder`, `ExecEvent` is the
   "copy" type and `ExecEventRef<'ring>` is the zero-copy (shared-memory
   reference) type

6. **Execution event payload types** (`monad_exec_block_start`, and others) -
   these are bindgen-generated, `#[repr(C)]` event payload types that match
   their C API counterparts

## Block-level utilities

### `ExecutedBlockBuilder`

Execution events are granular: most actions taken by the EVM will publish a
single event describing that one action, e.g., every EVM log emitted is
published as as a separate `ExecEvent::TxnLog` event. The events are streamed
to consumers almost as soon as they are available, so the real-time data of a
block comes in "a piece at a time."

A utility called the `ExecutedBlockBuilder` will aggregate these events back
into a single, block-oriented update, if the user prefers working with complete
blocks. The data types in the block representation are also
[alloy\_primitives types](https://docs.rs/alloy-primitives/latest/alloy_primitives/)
which are more ergonomic to work with in Rust.

### `CommitStateBlockBuilder`

As explained in the section on
[speculative real-time data](/monad-arch/realtime-data/spec-realtime),
the EVM publishes execution events as soon as it is able to, which means it
is usually publishing data about blocks that are speculatively executed.
We do not know if these blocks will be appended to the blockchain or not,
since the consensus decision is occurring in parallel with (and will finish
later than) the block's execution.

`CommitStateBlockBuilder` builds on the `ExecutedBlockBuilder` by also
tracking the commit state of the block as it moves through the consensus
life cycle. The block update itself is passed around via an
`Arc<ExecutedBlock>`, so that it is cheap to copy references to it. As the
block commit state changes, you receive updates describing the new state,
along with another reference to the `Arc<ExecutedBlock>` itself.

The speculative real-time data guide often points out that block abandonment
is not explicitly communicated by the event system (e.g.,
[here](/monad-arch/realtime-data/spec-realtime#third-commit-state-finalized) and
[here](/reference/websockets#monadnewheads-and-monadlogs)).
The `CommitStateBlockBuilder` however, *does* report explicit abandonment of
failed proposals, because it is a higher level, user-friendly utility.
