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=0builds 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.0ofcategory-labs/monad-bftand the matchingcategory-labs/monadheaders. 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) andgithub.com/klauspost/compress(zstd snapshots).
go get github.com/nodejumper-org/monad-exec-events-go
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.
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.
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)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
}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).
- Live rings are a Linux/x86-64 feature — the ring is produced by a
Monad node into hugetlbfs (see the node's
--exec-event-ringflag). 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).
eventringis the low-level ring reader (mmap, iterator, payload liveness) with no schema knowledge — the port of upstreammonad-event-ring/category/core/event.execis the execution event schema — the port ofmonad-exec-events/exec_event_ctypes.h. Depend onexecfor decoding; useeventringdirectly 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.
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).
GPLv3 (the upstream headers this port derives from are GPLv3). See LICENSE.