Skip to content

nodejumper-org/monad-exec-events-go

Repository files navigation

monad-exec-events-go

Pure-Go reader for the Monad Execution Events shared-memory ring buffer.

Monad's official Execution Events SDK ships bindings for C, C++, and Rust only, leaving Go programs (indexers, bots, monitors, analytics) on JSON-RPC polling. This library removes that gap: it maps the event ring, iterates it under the lock-free single-producer protocol, and decodes the full event schema — including the complete block lifecycle that RPC feeds hide — into idiomatic Go types.

  • Pure Go, no cgo. CGO_ENABLED=0 builds and passes all tests. The reader algorithm is ported from the upstream C headers, not linked.
  • Faithful port, pinned and audited. Everything is ported against the tag release/exec-events-sdk-v1.0 of category-labs/monad-bft and the matching category-labs/monad headers. Exact commits, the files read, and every spec-vs-source discrepancy are recorded in PROVENANCE.md. Struct offsets are locked by a golden layout file dumped from the actual C headers (testdata/layout_golden.json) and cross-checked in CI.
  • Verified against upstream data. The test suite replays a snapshot captured by the upstream SDK (15M-era Ethereum mainnet blocks) and compares every decoded field — block headers, execution outputs, transactions, logs, consensus lifecycle — against the upstream-generated reference trace.
  • Minimal dependencies: golang.org/x/sys (mmap) and github.com/klauspost/compress (zstd snapshots).

Install

go get github.com/nodejumper-org/monad-exec-events-go

Quick start

Follow a live ring

import (
    "time"

    "github.com/nodejumper-org/monad-exec-events-go/eventring"
    "github.com/nodejumper-org/monad-exec-events-go/exec"
)

ring, err := eventring.Open("/var/lib/hugetlbfs/user/monad/pagesize-2MB/event-rings/monad-exec-events")
if err != nil { /* ... */ }
defer ring.Close()

// Refuse to decode a ring produced with a different event schema.
if err := ring.CheckContentType(eventring.ContentTypeExec, &exec.SchemaHash); err != nil { /* ... */ }

it := ring.Iterate() // positioned at the latest event

for {
    ev, res := it.TryNext()
    switch res {
    case eventring.Success:
        payload, ok := ev.Payload() // safe copy + liveness re-check
        if !ok {
            continue // writer overwrote this payload while we read it
        }
        decoded, err := exec.Decode(exec.EventType(ev.Type()), payload)
        if err != nil { /* ... */ }
        switch v := decoded.(type) {
        case *exec.BlockStart:
            _ = v.BlockTag.ID // 32-byte consensus id: the fork-tracking primitive
        case *exec.BlockFinalized:
            // ...
        }
    case eventring.NotReady:
        time.Sleep(time.Millisecond) // or spin, on a latency-critical path
    case eventring.Gap:
        it.Reset() // we fell behind and events were overwritten; resync
    }
}

The writer never blocks: if a reader falls behind, unread events are overwritten and the reader observes Gap (descriptor overwritten) or Payload() == !ok (payload bytes expired). Both are normal backpressure signals — resync with Reset and, if you need history, backfill elsewhere.

Replay a snapshot

Snapshots are zstd-compressed ring captures; they make everything deterministic and offline (tests, batch analysis):

snap, err := eventring.OpenSnapshot("snapshot.zst")
if err != nil { /* ... */ }
defer snap.Close()

for ev := range snap.All() { // oldest → newest, ends cleanly
    payload, _ := ev.Payload()
    decoded, _ := exec.Decode(exec.EventType(ev.Type()), payload)
    _ = decoded
}

eventring.Open auto-detects snapshots, so tools built on this library accept either input transparently.

Block lifecycle and forks

A block appears on the ring through its full consensus lifecycle:

BLOCK_START (proposed) → BLOCK_QC (voted) → BLOCK_FINALIZED → BLOCK_VERIFIED
                       ↘ BLOCK_REJECT

Multiple competing proposals can exist at the same height; only one is finalized. BlockTag.ID uniquely identifies a proposal across states — this stream is the only place forks and reorgs are observable.

Every event carries flow tags linking it to its context:

flow := exec.FlowOf(ev.ContentExt)
flow.BlockSeqno // ring seqno of the owning BLOCK_START (chase with ring.TryCopy)
flow.TxnIndex   // transaction index within the block (flow.HasTxn)

Zero-copy reads (advanced)

For hot paths, ev.PayloadPeek() returns a slice aliasing the shared memory. The contract is peek → use → check:

b := ev.PayloadPeek()
result := process(b)     // must not retain b
if !ev.PayloadCheck() {  // writer overwrote b while we processed it
    result = discard
}

monad-events-tail

A small demo CLI printing the block lifecycle from a ring or snapshot:

go run github.com/nodejumper-org/monad-exec-events-go/cmd/monad-events-tail@latest testdata/exec-events-emn-30b-15m/snapshot.zst

20:26:14.354 PROPOSED  block=15000001 id=0x00000000…00e4e1c1 round=15000001 txns=229 gasLimit=30000000
20:26:14.599 EXECUTED  block=15000001 hash=0x7a7d7c23…a374295e gasUsed=22516578
20:26:14.599 VOTED     block=15000001 id=0x00000000…00e4e1c1 round=15000002
20:26:14.599 FINALIZED block=15000001 id=0x00000000…00e4e1c1
20:26:14.599 VERIFIED  block=15000001

Flags: -all (live rings: start from the oldest available event), -txns (print transactions), -ignore-schema-hash (downgrade schema drift to a warning).

Platform notes

  • Live rings are a Linux/x86-64 feature — the ring is produced by a Monad node into hugetlbfs (see the node's --exec-event-ring flag). Run your consumer on the same host.
  • The package also builds and is fully tested on macOS (amd64/arm64), so snapshot-based development and CI need no Linux box.
  • Hosts must be little-endian (the on-disk format is little-endian and the reader is an mmap view, enforced by build constraints).

Design notes

  • eventring is the low-level ring reader (mmap, iterator, payload liveness) with no schema knowledge — the port of upstream monad-event-ring / category/core/event. exec is the execution event schema — the port of monad-exec-events / exec_event_ctypes.h. Depend on exec for decoding; use eventring directly if you define your own content type.
  • All shared-memory reads of sequence numbers and control registers go through sync/atomic (Go's seq-cst loads subsume the upstream acquire loads). The payload buffer is mapped twice back-to-back — the upstream trick that lets payloads wrap the buffer edge while staying contiguous in memory.
  • Payload copies deliberately race with a live writer, exactly as upstream: bracketing sequence-number and buffer-window checks detect any overwrite after the fact. See eventring/payload.go.

Testing

CGO_ENABLED=0 go test ./...        # includes golden replays of upstream fixtures
go test -race ./...                # atomics/liveness protocol under the race detector
go test -fuzz=FuzzDecode ./exec    # payload decoder fuzzing

The fixtures under testdata/ are copied unmodified from the upstream repository at the pinned tag (GPLv3, like this library — see PROVENANCE.md).

License

GPLv3 (the upstream headers this port derives from are GPLv3). See LICENSE.

About

Pure-Go library for reading Monad Execution Events straight from the shared-memory ring buffer — no cgo, no RPC polling

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Contributors