Skip to main content

How to Consume Execution Events in Rust: A Basic Example

This guide walks through building a minimal Rust application that reads execution events from a live Monad node. By the end of this guide, you’ll have a working event consumer that prints each EVM action as it happens.

Prerequisites

This guide assumes you have:
  • A running Monad node with execution events enabled (setup guide)

Project Setup

Here is the repo with the code. Create a new Rust project:
cargo new --bin exec-events-demo
cd exec-events-demo
Replace Cargo.toml with:
[package]
name = "exec-events-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
monad-exec-events = { git = "https://github.com/category-labs/monad-bft", tag = "release/exec-events-sdk-v1.0" }
monad-event-ring = { git = "https://github.com/category-labs/monad-bft", tag = "release/exec-events-sdk-v1.0" }

The Code

Replace src/main.rs with:
src/main.rs
use std::time::Duration;

use monad_event_ring::{DecodedEventRing, EventNextResult, EventPayloadResult, EventRingPath};
use monad_exec_events::{ExecEvent, ExecEventReaderExt, ExecEventRing, ExecEventType};

/// Default path for the execution event ring
const EVENT_RING_PATH: &str =
    "/var/lib/hugetlbfs/user/monad/pagesize-2MB/event-rings/monad-exec-events";

fn main() {
    // Resolve the event ring path (full path bypasses hugetlbfs lookup)
    let event_ring_path =
        EventRingPath::resolve(EVENT_RING_PATH).expect("Failed to resolve path");

    // Open the live event ring
    let ring = ExecEventRing::new(&event_ring_path).expect("Failed to open event ring");

    println!("Connected to event ring");
    println!("Waiting for events...\n");

    // Create a reader and rewind to the start of the current block
    let mut reader = ring.create_reader();
    reader.consensus_prev(Some(ExecEventType::BlockStart));

    loop {
        match reader.next_descriptor() {
            EventNextResult::Ready(event) => {
                let seqno = event.info().seqno;

                // Try to read the event payload
                match event.try_read() {
                    EventPayloadResult::Ready(exec_event) => {
                        print_event(&exec_event, seqno);
                    }
                    EventPayloadResult::Expired => {
                        eprintln!("[{}] Payload expired!", seqno);
                        reader.reset();
                    }
                }
            }
            EventNextResult::Gap => {
                eprintln!("Warning: event gap occurred (reader too slow)");
                reader.reset();
            }
            EventNextResult::NotReady => {
                // No events available, wait briefly
                std::thread::sleep(Duration::from_millis(10));
            }
        }
    }
}

fn print_event(event: &ExecEvent, seqno: u64) {
    match event {
        ExecEvent::BlockStart(block) => {
            println!(
                "[{}] BLOCK_START: number={}",
                seqno, block.block_tag.block_number
            );
        }
        ExecEvent::BlockEnd(_) => {
            println!("[{}] BLOCK_END", seqno);
        }
        ExecEvent::TxnHeaderStart { txn_index, .. } => {
            println!("[{}] TXN_START: index={}", seqno, txn_index);
        }
        ExecEvent::TxnEvmOutput { txn_index, output } => {
            println!(
                "[{}] TXN_OUTPUT: index={} gas_used={}",
                seqno, txn_index, output.receipt.gas_used
            );
        }
        ExecEvent::TxnLog { txn_index, txn_log, .. } => {
            println!(
                "[{}] LOG: txn={} topics={}",
                seqno, txn_index, txn_log.topic_count
            );
        }
        ExecEvent::TxnEnd => {
            println!("[{}] TXN_END", seqno);
        }
        // Silently ignore other events (call frames, storage access, etc.)
        _ => {}
    }
}

Build and Run

cargo build --release
./target/release/exec-events-demo

Expected Output

You’ll see a stream of events as your node processes transactions:
Connected to event ring
Waiting for events...

[508183802] BLOCK_START: number=45199825
[508183811] TXN_START: index=0
[508183822] TXN_OUTPUT: index=0 gas_used=0
[508183823] LOG: txn=0 topics=3
[508183838] TXN_END
[508183942] BLOCK_END

Key Concepts

Event Ring

The event ring is a shared memory buffer where the execution daemon writes events. Multiple readers can consume from it simultaneously, each maintaining their own position.

EventNextResult

  • Ready: An event is available to process
  • NotReady: No new events yet (daemon hasn’t written any)
  • Gap: Reader fell behind and events were overwritten - call reset() to recover

Event Types

Common events you’ll see:
  • BlockStart / BlockEnd - Block boundaries
  • TxnHeaderStart / TxnEnd - Transaction boundaries
  • TxnEvmOutput - Transaction execution result (contains receipt with gas_used)
  • TxnLog - EVM log emission (Solidity events)
  • AccountAccess - Account touched during execution
  • StorageAccess - Contract storage read/write

Next Steps