Live event rings vs. snapshot event rings
The event ring’s shared memory data structures typically live inside of a regular file. Any process that wants shared access to an event ring, first locates it via the filesystem, then maps a shared view of it into the process’ virtual memory map using the mmap(2) system call. Event ring files come in two flavors:- “Live” event ring files — these are the “normal” event ring files that are the source of real-time data. The whole point of the SDK is to read real-time events from these files, but they’re not very convenient for most day-to-day software development tasks. Suppose, for example, you wanted to write a test for your data processing program. The SDK is mostly designed around reading events, so to test it with a live event ring, you’d need to write some dummy event publishing code just to have events to read. For execution events, the live event ring file is populated by the execution daemon, which we have not even installed at this point in the tutorial! A lot of development headaches are solved by the second kind of event ring file.
- “Snapshot” event ring files — these are compressed snapshots taken of a live event ring file as it existed at a particular moment in time. Typically they are “rewound” to the oldest event in the circular event queue, and are used to replay a fixed set of historical execution events. Snapshot files are useful for testing and development workflows, because you do not need to be running an active publisher to use them. Because they’re so useful for development, snapshots are the first data source we’ll use, before trying the example program on a live node.
Running the example program on a snapshot file
Step 1: download a snapshot file
Run this command to download a snapshot:emn-30b-15m part of the filename means “Ethereum mainnet replay for 30
blocks starting after block 15 million”. In other words, this contains the
execution events emitted during a historical replay of the Ethereum blockchain
(chain ID 1), from block 15,000,001 to block 15,000,031.
The Category Labs execution daemon is able to execute blocks from Monad
blockchains (the EVM chain ID 143 or any of its test networks), but also from
other EVM-compatible networks. Historical replay of the Ethereum mainnet is
used as an execution “conformance test”, to make sure the node software remains
as Ethereum compatible as possible.
We use an Ethereum chain snapshot in the tutorial under the assumption that
many developers are already familiar with the Ethereum ecosystem, but might be
new to Monad. You can check that all of the data captured in the snapshot file
matches the data published by your favorite Ethereum data provider. For example,
you’ll be able to check that the data shown here matches what is reported by
websites like Etherscan.
Step 2: run the SDK example program you built previously
The command is slightly different for each programming language. For C, run:#[derive(Debug)] feature. The -d
parameter in the Rust command line tells the program to print this “debug”
form.
Full pretty-printers do exist in the SDK for the C language family, but they
are only available for C++ and are based on the standard C++
<format> libraryStep 3: analyze the data (Rust only)
If you’re running the Rust example program — and this step of the guide assumes you are — you will see a text dump of all event data. We’ll look at a few specific events to give you a sense of what kind of data the SDK produces, and what you can do with it. The first two lines printed by the Rust example program look like this:-
16:26:14.354056730— this is the nanosecond-resolution timestamp when the original event was recorded; since we’re looking at a snapshot and not live data, this will always be the same number, and it’s from a long time ago; the actual “date” portion of the timestamp is omitted when we print it, since the typical use-case for the SDK is for real-time data (where the date is usually “today”) -
BLOCK_START- this is the type of the event that occurred inside of the EVM; aBLOCK_STARTevent is recorded when a new block is first seen by the execution daemon, and its payload describes all the execution inputs that are known at the start of execution processing; this mostly corresponds to the fields in the Ethereum block header which are known prior to execution -
[2 0x2]- this is the numerical code that corresponds to theBLOCK_STARTevent type, in both decimal and hexidecimal -
SEQ: 1- the sequence number (a monotonic counter of the number of events published so far) is 1; in a live event ring, these are used for gap / overwrite detection -
BLK: 15000001- this event is part of block number 15,000,001
#[derive(Debug)] output) it was abbreviated in our example output text. We’ll
look at parts of it in a moment, but we’ll pause here to explain some things
about this println!("Payload: {exec_event:x?}") statement.
exec_event is a value of Rust enum type ExecEvent. Here is how that
enum is defined:
-
The debug output starts with
BlockStart(...), soexec_eventhas theExecEvent::BlockStartenum variant -
It seems like we already knew that from the earlier
BLOCK_START [2 0x2]print-out, but there’s a subtle difference. The first line prints information found in the event descriptor, which is like a header containing the the common fields of an event. At the point in the program where the descriptor line is printed, it has not yet decoded the event payload to construct theexec_eventvariant. Suppose we were only interested in block 15,000,002. In that case, we could look at just the descriptor, notice it relates to block 15,000,001, and skip over this event (and all other events for that block), i.e., we would not bother decoding it -
The value associated with an
ExecEvent::BlockStartvariant if of typestruct monad_exec_block_start; notice that this type does not follow the normal Rust code-formatting style: it useslower_case_snake_caseinstead ofUpperCamelCaseand has a seemingly-unnecessary prefix (all the variant value types start withmonad_exec_). This is because the payload types are defined as C language structures, and their Rust equivalents are generated using bindgen. The C-style spelling helps indicate that. The definition ofmonad_exec_block_startcomes from the C header fileexec_event_ctypes.h, where it is defined like this:
eth_block_input is the field that
corresponds to the parts of the Ethereum block header which are known at
the start of execution.
Some of this output is difficult to read, since Rust’s #[derive(Debug)] is
meant for ease of debugging and doesn’t always “pretty-print” data in the best
way for readability. Other fields are clear though, for example, the gas_limit
of the block is shown as a hexidecimal value:
0x1c9c380 corresponds to the decimal number 30,000,000, a number we expect
to see for a mainnet Ethereum gas limit.
Real pretty-printing of events is done with a developer tool called
monad-event-cli, which is part of the SDK. This example is meant to be as
simple and short as possible, to help with learning the API. When debugging
real event programs, you will probably prefer developer tools like the event
CLI tool. The build instructions for it are in the final step of the
“Getting start” guide (here).TXN_EVM_OUTPUT, the first match
will be this event (with some formatting differences):
TXN: 0 in the descriptor and txn_index: 0 in the
payload. We say “first event” because the output for any particular transaction
usually spans several events: each log, call frame, state change, and state
access is recorded as a separate event.
The first event is always of type TXN_EVM_OUTPUT. It contains a basic summary
of what happened, and an indication of how many more output-related events will
follow. You can see that this particular transaction emitted zero logs, and one
call frame trace. The call frame information is recorded in the next event,
on the line below this one.
As it turns out, the very first transaction is also somewhat interesting: it
failed to execute after using 30,300 gas (0x765c). The transaction’s failure is
recorded by status field. As you can see, it is set to false.
Why did it fail? To figure it out, we’ll use the information in the
TXN_CALL_FRAME event that follows this one. The evmc_status_code field in
that event has the value 2, which is the numeric value of the
EVMC_REVERT
status code. This tells us that the revert was requested by the contract code
itself, i.e., it executed a
REVERT instruction. In other words,
this was not a VM-initiated exceptional halt such as “out of gas” or “illegal
instruction, but something the contract itself decided to do.
Because this is a Solidity contract, we can decode richer error information
from the call frame. The REVERT instruction can pass arbitrary-length return
data back to the caller. This return data is recorded in the call frame, in the
return_bytes array.
Observe that the first 4 bytes of return_bytes are 0x8c379a0. This is how
Solidity represents a revert that carries a string explanation. The details of
how this string is encoded is
here,
but the upshot is that we can decode the last 32 bytes of this return_bytes
array as an ASCII string. If you try this yourself, you’ll discover that it
says:
TXN_HEADER_START) we can find the transaction’s
Keccak hash, which is
0xaedb8ef26125d8ad6e0c5f19fc9cbdd7f4a42eb82de88686b39090b8abcfeb8f. If
we look up information about this transaction on
Etherscan,
using the hash, we can see that Etherscan agrees. The Status: field reads:

