diff --git a/category/core/CMakeLists.txt b/category/core/CMakeLists.txt
index f69c84550f..87879b9bc3 100644
--- a/category/core/CMakeLists.txt
+++ b/category/core/CMakeLists.txt
@@ -116,12 +116,16 @@ add_library(
"event/event_iterator.h"
"event/event_iterator_inline.h"
"event/event_metadata.h"
+ "event/event_recorder.cpp"
"event/event_recorder.h"
+ "event/event_recorder.hpp"
"event/event_recorder_inline.h"
"event/event_ring.c"
"event/event_ring.h"
"event/event_ring_util.c"
"event/event_ring_util.h"
+ "event/owned_event_ring.cpp"
+ "event/owned_event_ring.hpp"
"event/test_event_ctypes.h"
"event/test_event_ctypes_metadata.c"
# fiber
diff --git a/category/core/event/event_recorder.cpp b/category/core/event/event_recorder.cpp
new file mode 100644
index 0000000000..eca9702e86
--- /dev/null
+++ b/category/core/event/event_recorder.cpp
@@ -0,0 +1,120 @@
+// Copyright (C) 2025 Category Labs, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+MONAD_NAMESPACE_BEGIN
+
+std::tuple
+EventRecorder::setup_record_error_event(
+ uint16_t event_type, monad_event_record_error_type error_type,
+ size_t header_payload_size,
+ std::span const> trailing_payload_bufs,
+ size_t original_payload_size)
+{
+ monad_event_record_error *error_payload;
+ size_t error_payload_size;
+
+ switch (error_type) {
+ case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
+ [[fallthrough]];
+ case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE:
+ // When an event cannot be recorded due to its payload size, we still
+ // record the first 8 KiB of that payload; it may help with diagnosing
+ // the cause of the overflow, which is a condition that is not expected
+ // in normal operation
+ error_payload_size = RECORD_ERROR_TRUNCATED_SIZE;
+ break;
+ default:
+ error_payload_size = sizeof *error_payload;
+ break;
+ }
+
+ uint64_t seqno;
+ uint8_t *payload_buf;
+ monad_event_descriptor *const event = monad_event_recorder_reserve(
+ &recorder_, error_payload_size, &seqno, &payload_buf);
+ MONAD_ASSERT(event != nullptr, "non-overflow reservation must succeed");
+
+ event->event_type = 1;
+ error_payload = reinterpret_cast(payload_buf);
+ error_payload->error_type = error_type;
+ error_payload->dropped_event_type = event_type;
+ error_payload->requested_payload_size = original_payload_size;
+
+ switch (error_type) {
+ case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
+ [[fallthrough]];
+ case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE: {
+ // In these cases, the payload area is set up like this:
+ //
+ // .----------------.-----------------------.---------------.
+ // | *error_payload | event header (type T) | truncated VLT |
+ // .----------------.-----------------------.---------------.
+ //
+ // The intention here is for the reader to be able to see some of
+ // the event that was discarded; we never expect these errors to
+ // happen, so recording as much information as possible may be
+ // important for debugging how our assumptions were wrong.
+ //
+ // The event header is written by the call site: we pass a pointer
+ // to it in the return value, and the caller writes to it as though
+ // the recording did not fail. The code below is responsible for
+ // writing the "variable-length trailing" (VLT) data, which is the
+ // only part that is truncated; an earlier assertion ensures that
+ // RECORD_ERROR_TRUNCATED_SIZE is large enough that the event
+ // header is never truncated. We do include the event header's size
+ // in the `error_payload->truncated_payload_size` field, however.
+ error_payload->truncated_payload_size =
+ RECORD_ERROR_TRUNCATED_SIZE - sizeof(*error_payload);
+ size_t const truncated_vlt_offset =
+ sizeof(*error_payload) + header_payload_size;
+ size_t residual_size =
+ RECORD_ERROR_TRUNCATED_SIZE - truncated_vlt_offset;
+ void *p = payload_buf + truncated_vlt_offset;
+ for (std::span buf : trailing_payload_bufs) {
+ size_t const copy_len = std::min(residual_size, size(buf));
+ p = mempcpy(p, data(buf), copy_len);
+ residual_size -= copy_len;
+ if (residual_size == 0) {
+ break;
+ }
+ }
+ } break;
+
+ default:
+ error_payload->truncated_payload_size = 0;
+ break;
+ }
+
+ return {
+ event,
+ reinterpret_cast(payload_buf) + sizeof *error_payload,
+ seqno};
+}
+
+MONAD_NAMESPACE_END
diff --git a/category/core/event/event_recorder.hpp b/category/core/event/event_recorder.hpp
new file mode 100644
index 0000000000..8a7755e54d
--- /dev/null
+++ b/category/core/event/event_recorder.hpp
@@ -0,0 +1,226 @@
+// Copyright (C) 2025 Category Labs, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#pragma once
+
+/**
+ * @file
+ *
+ * This file defines a C++ interface for recording to event rings
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+MONAD_NAMESPACE_BEGIN
+
+/// C++ event recording works in three steps: (1) reserving descriptor and
+/// payload buffer space in an event ring, then (2) the user performs zero-copy
+/// typed initialization of the payload directly in ring memory, then (3) the
+/// result is committed to the event ring; this type connects all three steps
+template
+struct ReservedEvent
+{
+ monad_event_descriptor *event;
+ T *payload;
+ uint64_t seqno;
+};
+
+/// An interface to an event recorder that represents "reserve" and "commit"
+/// semantics as the ReservedEvent template type, and has convenient support
+/// for capturing recording errors
+class EventRecorder
+{
+public:
+ explicit EventRecorder(monad_event_recorder const &recorder) noexcept
+ : recorder_{recorder}
+ {
+ }
+
+ /// Reserve resources to record an event; T is the type of the "header"
+ /// payload, and U... is a variadic sequence of trailing payload buffers
+ /// of type `std::span`, e.g., TXN_LOG records the log
+ /// header structure `struct monad_exec_txn_log` and two variadic byte
+ /// sequences (for topics and log data)
+ template <
+ typename T, typename EventEnum,
+ std::same_as>... U>
+ requires std::is_enum_v
+ [[nodiscard]] ReservedEvent reserve_event(EventEnum, U...);
+
+ /// Commit the previously reserved event resources to the event ring
+ template
+ void commit(ReservedEvent const &);
+
+ static constexpr size_t RECORD_ERROR_TRUNCATED_SIZE = 1UL << 13;
+
+protected:
+ alignas(64) monad_event_recorder recorder_;
+
+ /// Helper for creating a RECORD_ERROR event in place of the requested
+ /// event, which could not be recorded
+ std::tuple
+ setup_record_error_event(
+ uint16_t event_type, monad_event_record_error_type,
+ size_t header_payload_size,
+ std::span const> payload_bufs,
+ size_t original_payload_size);
+};
+
+template <
+ typename T, typename EventEnum,
+ std::same_as>... U>
+ requires std::is_enum_v
+ReservedEvent
+EventRecorder::reserve_event(EventEnum event_type, U... trailing_bufs)
+{
+ // This is checking that, in the event of a recorder error, we could still
+ // fit the entire header event type T and the error reporting type in the
+ // maximum "truncated buffer" size allocated to report errors
+ static_assert(
+ sizeof(T) + sizeof(monad_event_record_error) <=
+ RECORD_ERROR_TRUNCATED_SIZE);
+
+ // This function does the following:
+ //
+ // - Reserves an event descriptor
+ //
+ // - Reserves payload buffer space to hold the event payload data type,
+ // which is a fixed-size, C-layout-compatible structure of type `T`;
+ // the caller will later initialize this memory, constructing their T
+ // instance within it
+ //
+ // - Also reserves (as part of the above allocation) payload buffer space
+ // for variable-length arrays that follow the `T` object in the event
+ // payload. For example, the topics and log data arrays for TXN_LOG
+ // are variable-length data that is copied immediately following the
+ // main `T = monad_c_eth_txn_log` payload structure; in this kind of
+ // event, the payload type `monad_c_eth_txn_log` is called the "header"
+ //
+ // All variable-length trailing data segments are passed to this function
+ // via the variadic list of arguments. They are treated as unstructured
+ // data and have type `std::span`. After payload space is
+ // reserved for these byte arrays, they are also memcpy'd immediately.
+ //
+ // Events that do not have variable-length trailing data also use this
+ // function, with an empty `U` parameter pack.
+ //
+ // The reason variable-length data is memcpy'd immediately but the fixed
+ // sized part of the event payload (of type `T`) is not, is best explained
+ // by example. Consider this C++ type that models an Ethereum log:
+ //
+ // struct Log
+ // {
+ // byte_string data{};
+ // std::vector topics{};
+ // Address address{};
+ // }
+ //
+ // This type is not trivially copyable, but the underlying array elements
+ // in the `data` and `topics` array can be trivially copied.
+ //
+ // The corresponding C-layout-compatible type describing the log,
+ // `T = monad_c_eth_txn_log`, has to be manually initialized by the caller,
+ // so this function returns a `monad_c_eth_txn_log *` pointing to the
+ // payload buffer space for the caller to perform zero-copy initialization.
+ //
+ // We need to know the total size of the variable-length trailing data in
+ // order to reserve enough space for it; since the caller always knows what
+ // this data is, this function asks for the complete span rather than just
+ // the size, and also does the memcpy now. This simplifies the recording
+ // calls, and also the handling of the RECORD_ERROR type, which writes
+ // diagnostic truncated payloads on overflow
+
+ size_t const payload_size = (size(trailing_bufs) + ... + sizeof(T));
+ if (payload_size > std::numeric_limits::max()) [[unlikely]] {
+ std::array, sizeof...(trailing_bufs)> const
+ trailing_bufs_array = {trailing_bufs...};
+ auto const [event, header_buf, seqno] = setup_record_error_event(
+ std::to_underlying(event_type),
+ MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB,
+ sizeof(T),
+ trailing_bufs_array,
+ payload_size);
+ return {event, reinterpret_cast(header_buf), seqno};
+ }
+ if (payload_size >=
+ recorder_.payload_buf_mask + 1 - 2 * MONAD_EVENT_WINDOW_INCR) {
+ // The payload is smaller than the maximum possible size, but still
+ // cannot fit entirely in the event ring's payload buffer. For example,
+ // suppose we tried to allocate 300 MiB from a 256 MiB payload buffer.
+ //
+ // The event ring C API does not handle this as a special case;
+ // instead, the payload buffer's normal ring buffer expiration logic
+ // allows the allocation to "succeed" but it appears as expired
+ // immediately upon allocation (for the expiration logic, see the
+ // "Sliding window buffer" section of event_recorder.md).
+ //
+ // We treat this as a formal error so that the operator will know
+ // to allocate a (much) larger event ring buffer.
+ std::array, sizeof...(trailing_bufs)> const
+ trailing_bufs_array = {trailing_bufs...};
+ auto const [event, header_buf, seqno] = setup_record_error_event(
+ std::to_underlying(event_type),
+ MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE,
+ sizeof(T),
+ trailing_bufs_array,
+ payload_size);
+ return {event, reinterpret_cast(header_buf), seqno};
+ }
+
+ uint64_t seqno;
+ uint8_t *payload_buf;
+ monad_event_descriptor *const event = monad_event_recorder_reserve(
+ &recorder_, payload_size, &seqno, &payload_buf);
+ MONAD_DEBUG_ASSERT(event != nullptr);
+ if constexpr (sizeof...(trailing_bufs) > 0) {
+ // Copy the variable-length trailing buffers; GCC issues a false
+ // positive warning about this memcpy that must be disabled
+#if !defined(__clang__)
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wstringop-overflow"
+ #pragma GCC diagnostic ignored "-Warray-bounds"
+#endif
+ void *p = payload_buf + sizeof(T);
+ ((p = mempcpy(p, data(trailing_bufs), size(trailing_bufs))), ...);
+#if !defined(__clang__)
+ #pragma GCC diagnostic pop
+#endif
+ }
+ event->event_type = std::to_underlying(event_type);
+ return {event, reinterpret_cast(payload_buf), seqno};
+}
+
+template
+void EventRecorder::commit(ReservedEvent const &r)
+{
+ monad_event_recorder_commit(r.event, r.seqno);
+}
+
+MONAD_NAMESPACE_END
diff --git a/category/core/event/owned_event_ring.cpp b/category/core/event/owned_event_ring.cpp
new file mode 100644
index 0000000000..90b67b33fd
--- /dev/null
+++ b/category/core/event/owned_event_ring.cpp
@@ -0,0 +1,45 @@
+// Copyright (C) 2025 Category Labs, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#include
+#include
+#include
+
+#include
+
+#include
+
+MONAD_NAMESPACE_BEGIN
+
+OwnedEventRing::OwnedEventRing(
+ int ring_fd, std::string_view ring_path, monad_event_ring const &event_ring)
+ : event_ring_{event_ring}
+ , ring_path_{ring_path}
+ , ring_fd_{ring_fd}
+{
+}
+
+OwnedEventRing::~OwnedEventRing()
+{
+ // Check if we have a path before trying to unlink it; the path will be
+ // empty in the memfd_create(2) case
+ if (!ring_path_.empty()) {
+ (void)unlink(ring_path_.c_str());
+ }
+ (void)close(ring_fd_);
+ monad_event_ring_unmap(&event_ring_);
+}
+
+MONAD_NAMESPACE_END
diff --git a/category/core/event/owned_event_ring.hpp b/category/core/event/owned_event_ring.hpp
new file mode 100644
index 0000000000..0bd72e1da4
--- /dev/null
+++ b/category/core/event/owned_event_ring.hpp
@@ -0,0 +1,55 @@
+// Copyright (C) 2025 Category Labs, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#pragma once
+
+#include
+#include
+
+#include
+#include
+
+MONAD_NAMESPACE_BEGIN
+
+/// An RAII helper class for exclusive writers to event rings; it is assumed
+/// that the ring_fd file descriptor passed into this object will be owned by
+/// it (i.e., will be closed by this class' destructor, and not by the caller).
+/// This is so an instance of this class can hold open a BSD-style advisory
+/// exclusive file lock (see flock(2)) which marks the event ring as "owned";
+/// another aspect of the "ownership" contract is that the destructor of this
+/// class will also unlink the file at `ring_path` from the filesystem
+class OwnedEventRing
+{
+public:
+ explicit OwnedEventRing(
+ int ring_fd, std::string_view ring_path, monad_event_ring const &);
+
+ OwnedEventRing(OwnedEventRing const &) = delete;
+ OwnedEventRing(OwnedEventRing &&) = delete;
+
+ ~OwnedEventRing();
+
+ monad_event_ring const *get_event_ring() const
+ {
+ return &event_ring_;
+ }
+
+private:
+ monad_event_ring event_ring_;
+ std::string ring_path_;
+ int ring_fd_;
+};
+
+MONAD_NAMESPACE_END
diff --git a/category/core/event/test_event_ctypes.h b/category/core/event/test_event_ctypes.h
index bfdc663f58..74311e9696 100644
--- a/category/core/event/test_event_ctypes.h
+++ b/category/core/event/test_event_ctypes.h
@@ -36,6 +36,7 @@ enum monad_test_event : uint16_t
MONAD_TEST_EVENT_NONE,
MONAD_TEST_EVENT_RECORD_ERROR,
MONAD_TEST_EVENT_COUNTER,
+ MONAD_TEST_EVENT_VLT,
};
/// Event payload for MONAD_TEST_EVENT_COUNTER
@@ -45,7 +46,14 @@ struct monad_test_event_counter
uint64_t counter;
};
-extern struct monad_event_metadata const g_monad_test_event_metadata[3];
+/// Event payload for MONAD_TEST_EVENT_VLT
+struct monad_test_event_vlt
+{
+ uint32_t vlt_1_length;
+ uint32_t vlt_2_length;
+};
+
+extern struct monad_event_metadata const g_monad_test_event_metadata[4];
extern uint8_t const g_monad_test_event_schema_hash[32];
#define MONAD_EVENT_DEFAULT_TEST_FILE_NAME "event-recorder-test"
diff --git a/category/core/event/test_event_ctypes_metadata.c b/category/core/event/test_event_ctypes_metadata.c
index 927465eec7..2fa788c981 100644
--- a/category/core/event/test_event_ctypes_metadata.c
+++ b/category/core/event/test_event_ctypes_metadata.c
@@ -24,7 +24,7 @@ extern "C"
{
#endif
-struct monad_event_metadata const g_monad_test_event_metadata[3] = {
+struct monad_event_metadata const g_monad_test_event_metadata[4] = {
[MONAD_TEST_EVENT_NONE] =
{.event_type = MONAD_TEST_EVENT_NONE,
@@ -39,7 +39,13 @@ struct monad_event_metadata const g_monad_test_event_metadata[3] = {
[MONAD_TEST_EVENT_COUNTER] =
{.event_type = MONAD_TEST_EVENT_COUNTER,
.c_name = "TEST_COUNTER",
- .description = "A special event emitted only by the test suite"},
+ .description = "A test suite event that counts"},
+
+ [MONAD_TEST_EVENT_VLT] =
+ {.event_type = MONAD_TEST_EVENT_VLT,
+ .c_name = "TEST_VLT",
+ .description =
+ "A test suite event that records variable-length trailing arrays"},
};
diff --git a/category/core/test/event_recorder.cpp b/category/core/test/event_recorder.cpp
index dcae1372cd..21decf84d7 100644
--- a/category/core/test/event_recorder.cpp
+++ b/category/core/test/event_recorder.cpp
@@ -40,6 +40,7 @@
#include
#include
+#include
#include
#include
#include
@@ -479,3 +480,131 @@ TEST_F(EventRecorderDefaultFixture, LargePayloads)
data(big_buffer_bytes),
size(big_buffer_bytes)));
}
+
+TEST_F(EventRecorderDefaultFixture, CxxInterface)
+{
+ using namespace monad;
+
+ constexpr unsigned VLT_ARRAY_1[] = {12345678U, 87654321U, 0xBEEFCAFE};
+ constexpr char VLT_ARRAY_2[] = "Hello world!";
+ constexpr uint64_t CONTENT_EXT_0 = 0x7F56938801020304UL;
+
+ monad_event_recorder c_recorder;
+ ASSERT_EQ(0, monad_event_ring_init_recorder(&event_ring_, &c_recorder));
+ EventRecorder recorder{c_recorder};
+
+ // Note: the subspan(0) calls are there to make the spans into dynamic
+ // extent spans, rather than compile-time fixed-sized spans. Normally this
+ // API is never given fixed-sized spans, because the trailing buffer
+ // variadic args are by definition used for recording variably-sized
+ // trailing data. `std::span{x}` evaluates to a fixed-sized span because
+ // our testing data has a compile-time-known extent.
+ ReservedEvent const vlt_event =
+ recorder.reserve_event(
+ MONAD_TEST_EVENT_VLT,
+ as_bytes(std::span{VLT_ARRAY_1}).subspan(0),
+ as_bytes(std::span{VLT_ARRAY_2}).subspan(0));
+ ASSERT_NE(vlt_event.event, nullptr);
+ ASSERT_NE(vlt_event.payload, nullptr);
+ ASSERT_NE(vlt_event.seqno, 0);
+
+ *vlt_event.payload = monad_test_event_vlt{
+ .vlt_1_length = static_cast(std::size(VLT_ARRAY_1)),
+ .vlt_2_length = static_cast(std::size(VLT_ARRAY_2))};
+ vlt_event.event->content_ext[0] = CONTENT_EXT_0;
+ recorder.commit(vlt_event);
+
+ monad_event_descriptor event;
+ ASSERT_TRUE(
+ monad_event_ring_try_copy(&event_ring_, vlt_event.seqno, &event));
+ ASSERT_EQ(event.event_type, MONAD_TEST_EVENT_VLT);
+ ASSERT_EQ(event.content_ext[0], CONTENT_EXT_0);
+
+ auto const *const vlt_payload = static_cast(
+ monad_event_ring_payload_peek(&event_ring_, &event));
+ ASSERT_EQ(memcmp(vlt_payload, vlt_event.payload, sizeof *vlt_payload), 0);
+ ASSERT_EQ(memcmp(vlt_payload + 1, VLT_ARRAY_1, sizeof VLT_ARRAY_1), 0);
+ ASSERT_EQ(
+ memcmp(
+ reinterpret_cast(vlt_payload + 1) +
+ sizeof VLT_ARRAY_1,
+ VLT_ARRAY_2,
+ sizeof VLT_ARRAY_2),
+ 0);
+}
+
+TEST_F(EventRecorderDefaultFixture, CxxOverflowError)
+{
+ using namespace monad;
+ std::vector truncated;
+
+ // Make some data to put in the truncated buffer region. We will also pass
+ // in a giant buffer after this one, to cause the > 4GiB overflow. The
+ // giant buffer may not point to valid memory, but because we will have
+ // copied up the maximum truncation size from this smaller buffer first,
+ // the library won't try to access the giant buffer
+ truncated.reserve(EventRecorder::RECORD_ERROR_TRUNCATED_SIZE);
+ for (unsigned i = 0; i < EventRecorder::RECORD_ERROR_TRUNCATED_SIZE; ++i) {
+ truncated.push_back(static_cast(i));
+ }
+
+ monad_event_recorder c_recorder;
+ ASSERT_EQ(0, monad_event_ring_init_recorder(&event_ring_, &c_recorder));
+ EventRecorder recorder{c_recorder};
+
+ constexpr size_t OverflowSize = 1UL << 32;
+ ReservedEvent const vlt_event =
+ recorder.reserve_event(
+ MONAD_TEST_EVENT_VLT,
+ std::as_bytes(std::span{truncated}),
+ std::span{
+ reinterpret_cast(truncated.data()),
+ OverflowSize});
+ ASSERT_NE(vlt_event.event, nullptr);
+ ASSERT_NE(vlt_event.payload, nullptr);
+ ASSERT_NE(vlt_event.seqno, 0);
+
+ // The user will typically not know that error has happened; they will
+ // write into the payload area as though this is the real payload, but
+ // it's really part of the MONAD_EVENT_RECORD_ERROR layout
+ *vlt_event.payload =
+ monad_test_event_vlt{.vlt_1_length = 0, .vlt_2_length = 0};
+
+ recorder.commit(vlt_event);
+
+ monad_event_descriptor event;
+ ASSERT_TRUE(
+ monad_event_ring_try_copy(&event_ring_, vlt_event.seqno, &event));
+ ASSERT_EQ(event.event_type, 1);
+
+ size_t const expected_requested_payload_size =
+ sizeof(*vlt_event.payload) + std::size(truncated) + OverflowSize;
+ auto const *const written_error =
+ static_cast(
+ monad_event_ring_payload_peek(&event_ring_, &event));
+
+ size_t const expected_truncation_size =
+ EventRecorder::RECORD_ERROR_TRUNCATED_SIZE - sizeof(*written_error);
+ ASSERT_EQ(written_error->error_type, MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB);
+ ASSERT_EQ(written_error->dropped_event_type, MONAD_TEST_EVENT_VLT);
+ ASSERT_EQ(written_error->truncated_payload_size, expected_truncation_size);
+ ASSERT_EQ(
+ written_error->requested_payload_size, expected_requested_payload_size);
+
+ // `*vlt_event.payload` is still copied into the error event, into the
+ // truncation area
+ ASSERT_EQ(
+ memcmp(
+ written_error + 1, vlt_event.payload, sizeof(*vlt_event.payload)),
+ 0);
+
+ // Part of the VLT (as much as will fit) is also copied
+ size_t const vlt_offset =
+ sizeof(*written_error) + sizeof(*vlt_event.payload);
+ ASSERT_EQ(
+ memcmp(
+ reinterpret_cast(written_error) + vlt_offset,
+ std::data(truncated),
+ written_error->truncated_payload_size - sizeof(*vlt_event.payload)),
+ 0);
+}
diff --git a/category/execution/ethereum/event/exec_event_recorder.cpp b/category/execution/ethereum/event/exec_event_recorder.cpp
index d649e9ea60..b10543c701 100644
--- a/category/execution/ethereum/event/exec_event_recorder.cpp
+++ b/category/execution/ethereum/event/exec_event_recorder.cpp
@@ -13,144 +13,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-#include
#include
-#include
-#include
-#include
+#include
#include
-#include
-#include
-#include
#include
-#include
-#include
-#include
-
-#include
-#include
-#include
MONAD_NAMESPACE_BEGIN
-ExecutionEventRecorder::ExecutionEventRecorder(
- int ring_fd, std::string_view ring_path, monad_event_ring const &exec_ring)
- : exec_recorder_{}
- , exec_ring_{exec_ring}
- , cur_block_start_seqno_{0}
- , ring_path_{ring_path}
- , ring_fd_{dup(ring_fd)}
-{
- MONAD_ASSERT_PRINTF(
- ring_fd_ != -1,
- "dup(2) of ring_fd failed: %s (%d)",
- strerror(errno),
- errno);
-
- int const rc = monad_event_ring_init_recorder(&exec_ring_, &exec_recorder_);
- MONAD_ASSERT_PRINTF(
- rc == 0, "init recorder failed: %s", monad_event_ring_get_last_error());
-}
-
-ExecutionEventRecorder::~ExecutionEventRecorder()
-{
- unlink(ring_path_.c_str());
- (void)close(ring_fd_);
- monad_event_ring_unmap(&exec_ring_);
-}
-
-std::tuple
-ExecutionEventRecorder::setup_record_error_event(
- monad_exec_event_type event_type, monad_event_record_error_type error_type,
- size_t header_payload_size,
- std::span const> trailing_payload_bufs,
- size_t original_payload_size)
-{
- monad_exec_record_error *error_payload;
- size_t error_payload_size;
-
- switch (error_type) {
- case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
- [[fallthrough]];
- case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE:
- // When an event cannot be recorded due to its payload size, we still
- // record the first 8 KiB of that payload; it may help with diagnosing
- // the cause of the overflow, which is a condition that is not expected
- // in normal operation
- error_payload_size = RECORD_ERROR_TRUNCATED_SIZE;
- break;
- default:
- error_payload_size = sizeof *error_payload;
- break;
- }
-
- uint64_t seqno;
- uint8_t *payload_buf;
- monad_event_descriptor *const event = monad_event_recorder_reserve(
- &exec_recorder_, error_payload_size, &seqno, &payload_buf);
- MONAD_ASSERT(event != nullptr, "non-overflow reservation must succeed");
-
- event->event_type = MONAD_EXEC_RECORD_ERROR;
- event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = cur_block_start_seqno_;
- event->content_ext[MONAD_FLOW_TXN_ID] = 0;
- event->content_ext[MONAD_FLOW_ACCOUNT_INDEX] = 0;
-
- error_payload = reinterpret_cast(payload_buf);
- error_payload->error_type = error_type;
- error_payload->dropped_event_type = event_type;
- error_payload->requested_payload_size = original_payload_size;
-
- switch (error_type) {
- case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
- [[fallthrough]];
- case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE: {
- // In these cases, the payload area is set up like this:
- //
- // .----------------.-----------------------.---------------.
- // | *error_payload | event header (type T) | truncated VLT |
- // .----------------.-----------------------.---------------.
- //
- // The intention here is for the reader to be able to see some of
- // the event that was discarded; we never expect these to happen,
- // so they may be important for debugging.
- //
- // The event header is written by the call site: we pass a pointer
- // to it in the return value, and the caller writes to it as though
- // the recording did not fail. The code below is responsible for
- // writing the "variable-length trailing" (VLT) data, which is the
- // only part that is truncated; an earlier assertion ensures that
- // RECORD_ERROR_TRUNCATED_SIZE is large enough that the event
- // header is never truncated. We do include the event header's size
- // in the `error_payload->truncated_payload_size` field, however.
- error_payload->truncated_payload_size =
- RECORD_ERROR_TRUNCATED_SIZE - sizeof(*error_payload);
- size_t const truncated_vlt_offset =
- sizeof(*error_payload) + header_payload_size;
- size_t residual_size =
- RECORD_ERROR_TRUNCATED_SIZE - truncated_vlt_offset;
- void *p = payload_buf + truncated_vlt_offset;
- for (std::span buf : trailing_payload_bufs) {
- size_t const copy_len = std::min(residual_size, size(buf));
- p = mempcpy(p, data(buf), copy_len);
- residual_size -= copy_len;
- if (residual_size == 0) {
- break;
- }
- }
- } break;
-
- default:
- error_payload->truncated_payload_size = 0;
- break;
- }
-
- return {
- event,
- reinterpret_cast(payload_buf) + sizeof *error_payload,
- seqno};
-}
-
+std::unique_ptr g_exec_event_ring;
std::unique_ptr g_exec_event_recorder;
MONAD_NAMESPACE_END
diff --git a/category/execution/ethereum/event/exec_event_recorder.hpp b/category/execution/ethereum/event/exec_event_recorder.hpp
index c6a9df61e2..cf0f275541 100644
--- a/category/execution/ethereum/event/exec_event_recorder.hpp
+++ b/category/execution/ethereum/event/exec_event_recorder.hpp
@@ -23,72 +23,43 @@
* recording will remain disabled.
*/
-#include
#include
#include
-#include
+#include
+#include
#include
-#include
#include
#include
#include
-#include
-#include
#include
#include
-#include
-#include
-#include
-#include
#include
-#include
-
MONAD_NAMESPACE_BEGIN
-/// Event recording works in three steps: (1) reserving descriptor and payload
-/// buffer space in the event ring, then (2) the user performs zero-copy
-/// initialization of the payload directly in ring memory, then (3) the result
-/// is committed to the event ring; this structure connects all three steps
-template
-struct ReservedExecEvent
-{
- monad_event_descriptor *event;
- T *payload;
- uint64_t seqno;
-};
-
-/// All execution event recording goes through this class; it owns the
-/// `monad_event_recorder` object, the event ring memory mapping, and holds the
-/// event ring's file descriptor open (so that the flock(2) remains in place);
-/// it also keeps track of the block flow ID -- the sequence number of the
-/// BLOCK_START event, copied into all subsequent block-level events
-class ExecutionEventRecorder
+/// All execution event recording goes through this class; it extends the
+/// EventRecorder C++ utility and also keeps track of the block flow ID -- the
+/// sequence number of the BLOCK_START event, copied into all subsequent
+/// block-level events
+class ExecutionEventRecorder : private EventRecorder
{
public:
- explicit ExecutionEventRecorder(
- int ring_fd, std::string_view ring_path, monad_event_ring const &);
-
- ~ExecutionEventRecorder();
+ using EventRecorder::EventRecorder;
/// Reserve resources to record a BLOCK_START event; also sets the
/// current block flow ID
- [[nodiscard]] ReservedExecEvent
+ [[nodiscard]] ReservedEvent
reserve_block_start_event();
- /// Reserve resources to record an event that occurs at block scope; T is
- /// the type of the "header" payload, and U... is a variadic sequence of
- /// trailing payload buffers of type `std::span`, e.g.,
- /// TXN_LOG records the log header structure `struct monad_exec_txn_log`
- /// and two variadic byte sequences (for topics and log data)
+ /// Reserve resources to record an event that occurs at block scope
template >... U>
- [[nodiscard]] ReservedExecEvent
+ [[nodiscard]] ReservedEvent
reserve_block_event(monad_exec_event_type, U...);
/// Reserve resources to record a transaction-level event
template >... U>
- [[nodiscard]] ReservedExecEvent reserve_txn_event(
+ [[nodiscard]] ReservedEvent reserve_txn_event(
monad_exec_event_type event_type, uint32_t txn_num,
U &&...trailing_bufs)
{
@@ -101,44 +72,22 @@ class ExecutionEventRecorder
/// Mark that the current block has ended
void end_current_block();
- /// Commit the previously reserved event resources to the event ring
- template
- void commit(ReservedExecEvent const &);
-
/// Record a block-level event with no payload in one step
- void record_block_marker_event(monad_exec_event_type);
+ uint64_t record_block_marker_event(monad_exec_event_type);
/// Record a transaction-level event with no payload in one step
- void record_txn_marker_event(monad_exec_event_type, uint32_t txn_num);
-
- monad_event_ring const *get_event_ring() const
- {
- return &exec_ring_;
- }
+ uint64_t record_txn_marker_event(monad_exec_event_type, uint32_t txn_num);
- static constexpr size_t RECORD_ERROR_TRUNCATED_SIZE = 1UL << 13;
+ using EventRecorder::commit;
private:
- /// Helper for creating a RECORD_ERROR event in place of the requested
- /// event, which could not be recorded
- std::tuple
- setup_record_error_event(
- monad_exec_event_type, monad_event_record_error_type,
- size_t header_payload_size,
- std::span const> payload_bufs,
- size_t original_payload_size);
-
- alignas(64) monad_event_recorder exec_recorder_;
- monad_event_ring exec_ring_;
uint64_t cur_block_start_seqno_;
- std::string ring_path_;
- int ring_fd_;
};
-inline ReservedExecEvent
+inline ReservedEvent
ExecutionEventRecorder::reserve_block_start_event()
{
- ReservedExecEvent const block_start =
+ ReservedEvent const block_start =
reserve_block_event(MONAD_EXEC_BLOCK_START);
cur_block_start_seqno_ = block_start.seqno;
block_start.event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = block_start.seqno;
@@ -146,126 +95,11 @@ ExecutionEventRecorder::reserve_block_start_event()
}
template >... U>
-ReservedExecEvent ExecutionEventRecorder::reserve_block_event(
+ReservedEvent ExecutionEventRecorder::reserve_block_event(
monad_exec_event_type event_type, U... trailing_bufs)
{
- // This is checking that, in the event of a recorder error, we could still
- // fit the entire header event type T and the error reporting type in the
- // maximum "truncated buffer" size allocated to report errors
- static_assert(
- sizeof(T) + sizeof(monad_exec_record_error) <=
- RECORD_ERROR_TRUNCATED_SIZE);
-
- // This function does the following:
- //
- // - Reserves an event descriptor
- //
- // - Reserves payload buffer space to hold the event payload data type,
- // which is a fixed-size, C-layout-compatible structure of type `T`;
- // the caller will later initialize this memory, constructing their T
- // instance within it
- //
- // - Also reserves (as part of the above allocation) payload buffer space
- // for variable-length arrays that follow the `T` object in the event
- // payload. For example, the topics and log data arrays for TXN_LOG
- // are variable-length data that is copied immediately following the
- // main `T = monad_c_eth_txn_log` payload structure; in this kind of
- // event, the payload type `monad_c_eth_txn_log` is called the "header"
- //
- // All variable-length trailing data segments are passed to this function
- // via the variadic list of arguments. They are treated as unstructured
- // data and have type `std::span`. After payload space is
- // reserved for these byte arrays, they are also memcpy'd immediately.
- //
- // Events that do not have variable-length trailing data also use this
- // function, with an empty `U` parameter pack.
- //
- // The reason variable-length data is memcpy'd immediately but the fixed
- // sized part of the event payload (of type `T`) is not, is best explained
- // by example. Consider this C++ type that models an Ethereum log:
- //
- // struct Log
- // {
- // byte_string data{};
- // std::vector topics{};
- // Address address{};
- // }
- //
- // This type is not trivially copyable, but the underlying array elements
- // in the `data` and `topics` array can be trivially copied.
- //
- // The corresponding C-layout-compatible type describing the log,
- // `T = monad_c_eth_txn_log`, has to be manually initialized by the caller,
- // so this function returns a `monad_c_eth_txn_log *` pointing to the
- // payload buffer space for the caller to perform zero-copy initialization.
- //
- // We need to know the total size of the variable-length trailing data in
- // order to reserve enough space for it; since the caller always knows what
- // this data is, this function asks for the complete span rather than just
- // the size, and also does the memcpy now. This simplifies the recording
- // calls, and also the handling of the RECORD_ERROR type, which writes
- // diagnostic truncated payloads on overflow
-
- size_t const payload_size = (size(trailing_bufs) + ... + sizeof(T));
- if (payload_size > std::numeric_limits::max()) [[unlikely]] {
- std::array, sizeof...(trailing_bufs)> const
- trailing_bufs_array = {trailing_bufs...};
- auto const [event, header_buf, seqno] = setup_record_error_event(
- event_type,
- MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB,
- sizeof(T),
- trailing_bufs_array,
- payload_size);
- return {event, reinterpret_cast(header_buf), seqno};
- }
- if (payload_size >=
- exec_ring_.payload_buf_mask + 1 - 2 * MONAD_EVENT_WINDOW_INCR) {
- // The payload is smaller than the maximum possible size, but still
- // cannot fit entirely in the event ring's payload buffer. For example,
- // suppose we tried to allocate 300 MiB from a 256 MiB payload buffer.
- //
- // The event ring C API does not handle this as a special case;
- // instead, the payload buffer's normal ring buffer expiration logic
- // allows the allocation to "succeed" but it appears as expired
- // immediately upon allocation (for the expiration logic, see the
- // "Sliding window buffer" section of event_recorder.md).
- //
- // We treat this as a formal error so that the operator will know
- // to allocate a (much) larger event ring buffer.
- std::array, sizeof...(trailing_bufs)> const
- trailing_bufs_array = {trailing_bufs...};
- auto const [event, header_buf, seqno] = setup_record_error_event(
- event_type,
- MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE,
- sizeof(T),
- trailing_bufs_array,
- payload_size);
- return {event, reinterpret_cast(header_buf), seqno};
- }
-
- uint64_t seqno;
- uint8_t *payload_buf;
- monad_event_descriptor *const event = monad_event_recorder_reserve(
- &exec_recorder_, payload_size, &seqno, &payload_buf);
- MONAD_DEBUG_ASSERT(event != nullptr);
- if constexpr (sizeof...(trailing_bufs) > 0) {
- // Copy the variable-length trailing buffers; GCC issues a false
- // positive warning about this memcpy that must be disabled
-#if !defined(__clang__)
- #pragma GCC diagnostic push
- #pragma GCC diagnostic ignored "-Wstringop-overflow"
- #pragma GCC diagnostic ignored "-Warray-bounds"
-#endif
- void *p = payload_buf + sizeof(T);
- ((p = mempcpy(p, data(trailing_bufs), size(trailing_bufs))), ...);
-#if !defined(__clang__)
- #pragma GCC diagnostic pop
-#endif
- }
- event->event_type = event_type;
- event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = cur_block_start_seqno_;
- event->content_ext[MONAD_FLOW_TXN_ID] = 0;
- event->content_ext[MONAD_FLOW_ACCOUNT_INDEX] = 0;
+ ReservedEvent const r =
+ this->reserve_event(event_type, std::forward(trailing_bufs)...);
// TODO(ken): remove the memset(3) below if the C++ standard evolves
//
@@ -290,8 +124,11 @@ ReservedExecEvent ExecutionEventRecorder::reserve_block_event(
// the below memset(3) can be removed. Because the current function is
// inlineable, the dead stores created by this memset are eliminated, and
// it has minimal performance impact.
- memset(payload_buf, 0, sizeof(T));
- return {event, reinterpret_cast(payload_buf), seqno};
+ memset(static_cast(r.payload), 0, sizeof(T));
+ r.event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = cur_block_start_seqno_;
+ r.event->content_ext[MONAD_FLOW_TXN_ID] = 0;
+ r.event->content_ext[MONAD_FLOW_ACCOUNT_INDEX] = 0;
+ return r;
}
inline void ExecutionEventRecorder::end_current_block()
@@ -299,66 +136,65 @@ inline void ExecutionEventRecorder::end_current_block()
cur_block_start_seqno_ = 0;
}
-template
-void ExecutionEventRecorder::commit(ReservedExecEvent const &exec_event)
-{
- monad_event_recorder_commit(exec_event.event, exec_event.seqno);
-}
-
-inline void ExecutionEventRecorder::record_block_marker_event(
+inline uint64_t ExecutionEventRecorder::record_block_marker_event(
monad_exec_event_type event_type)
{
uint64_t seqno;
uint8_t *payload_buf;
monad_event_descriptor *const event =
- monad_event_recorder_reserve(&exec_recorder_, 0, &seqno, &payload_buf);
+ monad_event_recorder_reserve(&recorder_, 0, &seqno, &payload_buf);
event->event_type = std::to_underlying(event_type);
event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = cur_block_start_seqno_;
event->content_ext[MONAD_FLOW_TXN_ID] = 0;
event->content_ext[MONAD_FLOW_ACCOUNT_INDEX] = 0;
monad_event_recorder_commit(event, seqno);
+ return seqno;
}
-inline void ExecutionEventRecorder::record_txn_marker_event(
+inline uint64_t ExecutionEventRecorder::record_txn_marker_event(
monad_exec_event_type event_type, uint32_t txn_num)
{
uint64_t seqno;
uint8_t *payload_buf;
monad_event_descriptor *const event =
- monad_event_recorder_reserve(&exec_recorder_, 0, &seqno, &payload_buf);
+ monad_event_recorder_reserve(&recorder_, 0, &seqno, &payload_buf);
event->event_type = std::to_underlying(event_type);
event->content_ext[MONAD_FLOW_BLOCK_SEQNO] = cur_block_start_seqno_;
event->content_ext[MONAD_FLOW_TXN_ID] = txn_num + 1;
event->content_ext[MONAD_FLOW_ACCOUNT_INDEX] = 0;
monad_event_recorder_commit(event, seqno);
+ return seqno;
}
-// Declare the global recorder object; this is initialized by the driver
-// process if it wants execution event recording, and is left uninitialized to
-// disable it (all internal functions check if it's `nullptr` before using it);
-// we use a "straight" global variable rather than a "magic static" style
-// singleton, because we don't care as much about preventing initialization
-// races as we do about potential cost of poking at atomic guard variables
-// every time
+// Declare the global event ring and recorder objects; these are initialized by
+// the driver process if it wants execution event recording, and are left
+// uninitialized to disable it (all internal functions check if they are
+// `nullptr` before using them); we use a "straight" global variable rather
+// than a "magic static" style singleton, because we don't care as much about
+// preventing initialization races as we do about potential cost of poking at
+// atomic guard variables every time
+extern std::unique_ptr g_exec_event_ring;
extern std::unique_ptr g_exec_event_recorder;
/*
* Helper free functions for execution event recording
*/
-inline void record_block_marker_event(monad_exec_event_type event_type)
+inline uint64_t record_block_marker_event(monad_exec_event_type event_type)
{
if (auto *const e = g_exec_event_recorder.get()) {
- e->record_block_marker_event(event_type);
+ return e->record_block_marker_event(event_type);
}
+ return 0;
}
-inline void
+inline uint64_t
record_txn_marker_event(monad_exec_event_type event_type, uint32_t txn_num)
{
if (auto *const e = g_exec_event_recorder.get()) {
- e->record_txn_marker_event(event_type, txn_num);
+ return e->record_txn_marker_event(event_type, txn_num);
}
+ return 0;
}
MONAD_NAMESPACE_END
diff --git a/category/execution/ethereum/event/record_block_events.cpp b/category/execution/ethereum/event/record_block_events.cpp
index 09e14c9995..6eb704d109 100644
--- a/category/execution/ethereum/event/record_block_events.cpp
+++ b/category/execution/ethereum/event/record_block_events.cpp
@@ -41,7 +41,7 @@ void record_block_start(
return;
}
- ReservedExecEvent const block_start =
+ ReservedEvent const block_start =
exec_recorder->reserve_block_start_event();
*block_start.payload = monad_exec_block_start{
.block_tag{
@@ -94,14 +94,14 @@ Result record_block_result(Result result)
auto const &error_domain = result.error().domain();
auto const error_value = result.error().value();
if (error_domain == block_err_domain) {
- ReservedExecEvent const block_reject =
+ ReservedEvent const block_reject =
exec_recorder->reserve_block_event(
MONAD_EXEC_BLOCK_REJECT);
*block_reject.payload = static_cast(error_value);
exec_recorder->commit(block_reject);
}
else {
- ReservedExecEvent const evm_error =
+ ReservedEvent const evm_error =
exec_recorder->reserve_block_event(
MONAD_EXEC_EVM_ERROR);
*evm_error.payload = monad_exec_evm_error{
@@ -111,7 +111,7 @@ Result record_block_result(Result result)
}
else {
// Record the "block execution successful" event, BLOCK_END
- ReservedExecEvent const block_end =
+ ReservedEvent const block_end =
exec_recorder->reserve_block_event(
MONAD_EXEC_BLOCK_END);
BlockExecOutput const &exec_output = result.value();
diff --git a/category/execution/ethereum/event/record_txn_events.cpp b/category/execution/ethereum/event/record_txn_events.cpp
index 85418cf702..d2f0b595e6 100644
--- a/category/execution/ethereum/event/record_txn_events.cpp
+++ b/category/execution/ethereum/event/record_txn_events.cpp
@@ -132,7 +132,7 @@ struct AccountAccessInfo
/// whether opt_txn_num is set or not; the account access events are allocated
/// this way, as some of them occur at system scope
template
-ReservedExecEvent reserve_event(
+ReservedEvent reserve_event(
ExecutionEventRecorder *exec_recorder, monad_exec_event_type event_type,
std::optional opt_txn_num)
{
@@ -161,7 +161,7 @@ void record_storage_events(
}
}
- ReservedExecEvent const storage_access =
+ ReservedEvent const storage_access =
reserve_event(
exec_recorder, MONAD_EXEC_STORAGE_ACCESS, opt_txn_num);
*storage_access.payload = monad_exec_storage_access{
@@ -205,7 +205,7 @@ void record_account_events(
auto const [modified_nonce, is_nonce_modified] =
account_info.get_nonce_modification();
- ReservedExecEvent const account_access =
+ ReservedEvent const account_access =
reserve_event(
exec_recorder, MONAD_EXEC_ACCOUNT_ACCESS, opt_txn_num);
*account_access.payload = monad_exec_account_access{
@@ -262,7 +262,7 @@ void record_account_access_events_internal(
{
auto const &prestate_map = state.original();
- ReservedExecEvent const list_header =
+ ReservedEvent const list_header =
reserve_event(
exec_recorder, MONAD_EXEC_ACCOUNT_ACCESS_LIST_HEADER, opt_txn_num);
*list_header.payload = monad_exec_account_access_list_header{
@@ -302,7 +302,7 @@ void record_txn_header_events(
}
// TXN_HEADER_START
- ReservedExecEvent const txn_header_start =
+ ReservedEvent const txn_header_start =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_HEADER_START,
txn_num,
@@ -313,7 +313,7 @@ void record_txn_header_events(
// TXN_ACCESS_LIST_ENTRY
for (uint32_t index = 0; AccessEntry const &e : transaction.access_list) {
- ReservedExecEvent const access_list_entry =
+ ReservedEvent const access_list_entry =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_ACCESS_LIST_ENTRY,
txn_num,
@@ -330,7 +330,7 @@ void record_txn_header_events(
// TXN_AUTH_LIST_ENTRY
for (uint32_t index = 0;
AuthorizationEntry const &e : transaction.authorization_list) {
- ReservedExecEvent const auth_list_entry =
+ ReservedEvent const auth_list_entry =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_AUTH_LIST_ENTRY, txn_num);
*auth_list_entry.payload = monad_exec_txn_auth_list_entry{
@@ -364,7 +364,7 @@ void record_txn_output_events(
}
// TXN_EVM_OUTPUT
- ReservedExecEvent const txn_evm_output =
+ ReservedEvent const txn_evm_output =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_EVM_OUTPUT, txn_num);
*txn_evm_output.payload = monad_exec_txn_evm_output{
@@ -377,7 +377,7 @@ void record_txn_output_events(
// TXN_LOG
for (uint32_t index = 0; auto const &log : receipt.logs) {
- ReservedExecEvent const txn_log =
+ ReservedEvent const txn_log =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_LOG,
txn_num,
@@ -399,7 +399,7 @@ void record_txn_output_events(
std::span const return_bytes{
call_frame.output.data(), call_frame.output.size()};
- ReservedExecEvent const txn_call_frame =
+ ReservedEvent const txn_call_frame =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_CALL_FRAME,
txn_num,
@@ -449,14 +449,14 @@ void record_txn_error_event(
auto const &error_domain = txn_error.domain();
auto const error_value = txn_error.value();
if (error_domain == txn_err_domain) {
- ReservedExecEvent const txn_reject =
+ ReservedEvent const txn_reject =
exec_recorder->reserve_txn_event(
MONAD_EXEC_TXN_REJECT, txn_num);
*txn_reject.payload = static_cast(error_value);
exec_recorder->commit(txn_reject);
}
else {
- ReservedExecEvent const evm_error =
+ ReservedEvent const evm_error =
exec_recorder->reserve_txn_event(
MONAD_EXEC_EVM_ERROR, txn_num);
*evm_error.payload = monad_exec_evm_error{
diff --git a/category/execution/ethereum/event/test/test_exec_event_recorder.cpp b/category/execution/ethereum/event/test/test_exec_event_recorder.cpp
deleted file mode 100644
index 5a72f5c40d..0000000000
--- a/category/execution/ethereum/event/test/test_exec_event_recorder.cpp
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (C) 2025 Category Labs, Inc.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-
-using namespace monad;
-
-namespace
-{
- // g_exec_event_recorder is deliberately not a "magic static" to avoid
- // poking at guard variables, since we normally don't care about
- // initialization races; here we do, if the tests want to run in parallel
- std::once_flag recorder_initialized;
-
- void ensure_recorder_initialized()
- {
- constexpr uint8_t DESCRIPTORS_SHIFT = 20;
- constexpr uint8_t PAYLOAD_BUF_SHIFT = 28; // 256 MiB
- constexpr char MEMFD_NAME[] = "exec_recorder_test";
-
- int ring_fd [[gnu::cleanup(cleanup_close)]] =
- memfd_create(MEMFD_NAME, 0);
- ASSERT_NE(ring_fd, -1);
- monad_event_ring_simple_config const simple_cfg = {
- .descriptors_shift = DESCRIPTORS_SHIFT,
- .payload_buf_shift = PAYLOAD_BUF_SHIFT,
- .context_large_pages = 0,
- .content_type = MONAD_EVENT_CONTENT_TYPE_EXEC,
- .schema_hash = g_monad_exec_event_schema_hash};
- int rc =
- monad_event_ring_init_simple(&simple_cfg, ring_fd, 0, MEMFD_NAME);
- MONAD_ASSERT_PRINTF(
- rc == 0,
- "event library error -- %s",
- monad_event_ring_get_last_error());
-
- monad_event_ring exec_ring;
- rc = monad_event_ring_mmap(
- &exec_ring,
- PROT_READ | PROT_WRITE,
- MAP_POPULATE,
- ring_fd,
- 0,
- MEMFD_NAME);
- MONAD_ASSERT_PRINTF(
- rc == 0,
- "event library error -- %s",
- monad_event_ring_get_last_error());
-
- g_exec_event_recorder = std::make_unique(
- ring_fd, MEMFD_NAME, exec_ring);
- }
-
-} // End of anonymous namespace
-
-TEST(ExecEventRecorder, Basic)
-{
- Address const log_address = static_cast(0x12345678UL);
- bytes32_t const log_topics[] = {
- bytes32_t{0x1}, NULL_HASH, NULL_HASH_BLAKE3};
- constexpr char log_data[] = "Hello world!";
- uint32_t const txn_num = 30;
-
- std::call_once(recorder_initialized, ensure_recorder_initialized);
- ExecutionEventRecorder *const exec_recorder = g_exec_event_recorder.get();
-
- // Note: the subspan(0) calls are there to make the spans into dynamic
- // extent spans, rather than compile-time fixed-sized spans. Normally this
- // API is never given fixed-sized spans, because the trailing buffer
- // variadic args are by definition used for recording variably-sized
- // trailing data. `std::span{x}` evaluates to a fixed-sized span because
- // our testing data has a known extent.
- ReservedExecEvent const log_event =
- exec_recorder->reserve_txn_event(
- MONAD_EXEC_TXN_LOG,
- txn_num,
- as_bytes(std::span{log_topics}).subspan(0),
- as_bytes(std::span{log_data}).subspan(0));
- ASSERT_NE(log_event.event, nullptr);
- ASSERT_NE(log_event.payload, nullptr);
- ASSERT_NE(log_event.seqno, 0);
-
- *log_event.payload = monad_exec_txn_log{
- .index = 0,
- .address = log_address,
- .topic_count = static_cast(std::size(log_topics)),
- .data_length = static_cast(std::size(log_data))};
-
- exec_recorder->commit(log_event);
-
- monad_event_descriptor event;
- ASSERT_TRUE(monad_event_ring_try_copy(
- exec_recorder->get_event_ring(), log_event.seqno, &event));
- ASSERT_EQ(event.event_type, MONAD_EXEC_TXN_LOG);
- ASSERT_EQ(event.content_ext[MONAD_FLOW_TXN_ID], txn_num + 1);
-
- auto const *const written_log = static_cast(
- monad_event_ring_payload_peek(exec_recorder->get_event_ring(), &event));
- ASSERT_EQ(memcmp(written_log, log_event.payload, sizeof *written_log), 0);
- ASSERT_EQ(memcmp(written_log + 1, log_topics, sizeof log_topics), 0);
- ASSERT_EQ(
- memcmp(
- reinterpret_cast(written_log + 1) +
- sizeof log_topics,
- log_data,
- sizeof log_data),
- 0);
-}
-
-TEST(ExecEventRecorder, Overflow)
-{
- std::vector truncated;
- Address const log_address = static_cast(0x12345678UL);
- uint32_t const txn_num = 30;
-
- // Make some data to put in the truncated buffer region. We will also pass
- // in a giant buffer after this one, to cause the > 4GiB overflow. The
- // giant buffer may not point to valid memory, but because we will have
- // copied up the maximum truncation size from this smaller buffer first,
- // the library won't try to access the giant buffer
- for (unsigned i = 0;
- i < ExecutionEventRecorder::RECORD_ERROR_TRUNCATED_SIZE;
- ++i) {
- truncated.push_back(static_cast(i));
- }
-
- std::call_once(recorder_initialized, ensure_recorder_initialized);
- ExecutionEventRecorder *const exec_recorder = g_exec_event_recorder.get();
-
- constexpr size_t OverflowSize = 1UL << 32;
- ReservedExecEvent const log_event =
- exec_recorder->reserve_txn_event(
- MONAD_EXEC_TXN_LOG,
- txn_num,
- std::as_bytes(std::span{truncated}),
- std::span{
- reinterpret_cast(truncated.data()),
- OverflowSize});
- ASSERT_NE(log_event.event, nullptr);
- ASSERT_NE(log_event.payload, nullptr);
- ASSERT_NE(log_event.seqno, 0);
-
- // The user will typically not know that error has happened; they will
- // write into the payload area as though this is the real payload, but
- // it's really part of the MONAD_EXEC_RECORD_ERROR layout
- *log_event.payload = monad_exec_txn_log{
- .index = 0,
- .address = log_address,
- .topic_count = 0,
- .data_length = 0,
- };
-
- exec_recorder->commit(log_event);
-
- monad_event_descriptor event;
- ASSERT_TRUE(monad_event_ring_try_copy(
- exec_recorder->get_event_ring(), log_event.seqno, &event));
- ASSERT_EQ(event.event_type, MONAD_EXEC_RECORD_ERROR);
- ASSERT_EQ(event.content_ext[MONAD_FLOW_TXN_ID], txn_num + 1);
-
- size_t const expected_requested_payload_size =
- sizeof(*log_event.payload) + std::size(truncated) + OverflowSize;
- auto const *const written_error =
- static_cast(
- monad_event_ring_payload_peek(
- exec_recorder->get_event_ring(), &event));
-
- size_t const expected_truncation_size =
- ExecutionEventRecorder::RECORD_ERROR_TRUNCATED_SIZE -
- sizeof(*written_error);
- ASSERT_EQ(written_error->error_type, MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB);
- ASSERT_EQ(written_error->dropped_event_type, MONAD_EXEC_TXN_LOG);
- ASSERT_EQ(written_error->truncated_payload_size, expected_truncation_size);
- ASSERT_EQ(
- written_error->requested_payload_size, expected_requested_payload_size);
-
- // `*log_event.payload` is still copied into the error event, into the
- // truncation area
- ASSERT_EQ(
- memcmp(
- written_error + 1, log_event.payload, sizeof(*log_event.payload)),
- 0);
-
- // Part of the VLT (as much as will fit) is also copied
- size_t const vlt_offset =
- sizeof(*written_error) + sizeof(*log_event.payload);
- ASSERT_EQ(
- memcmp(
- reinterpret_cast(written_error) + vlt_offset,
- std::data(truncated),
- written_error->truncated_payload_size - sizeof(*log_event.payload)),
- 0);
-}
diff --git a/category/execution/monad/event/record_consensus_events.cpp b/category/execution/monad/event/record_consensus_events.cpp
index bf52564ed4..71cd4b7682 100644
--- a/category/execution/monad/event/record_consensus_events.cpp
+++ b/category/execution/monad/event/record_consensus_events.cpp
@@ -53,7 +53,7 @@ void record_block_qc(
return;
}
auto const &vote = header.qc.vote;
- ReservedExecEvent const block_qc =
+ ReservedEvent const block_qc =
exec_recorder->reserve_block_event(
MONAD_EXEC_BLOCK_QC);
*block_qc.payload = monad_exec_block_qc{
@@ -76,7 +76,7 @@ EXPLICIT_INSTANTIATE_QC_TEMPLATE(MonadConsensusBlockHeaderV2);
void record_block_finalized(bytes32_t const &block_id, uint64_t block_number)
{
if (auto *const exec_recorder = g_exec_event_recorder.get()) {
- ReservedExecEvent const block_finalized =
+ ReservedEvent const block_finalized =
exec_recorder->reserve_block_event(
MONAD_EXEC_BLOCK_FINALIZED);
*block_finalized.payload = monad_exec_block_finalized{
@@ -92,7 +92,7 @@ void record_block_verified(std::span verified_blocks)
if (b == 0) {
continue;
}
- ReservedExecEvent const block_verified =
+ ReservedEvent const block_verified =
exec_recorder->reserve_block_event(
MONAD_EXEC_BLOCK_VERIFIED);
*block_verified.payload =
diff --git a/cmd/monad/event.cpp b/cmd/monad/event.cpp
index 35e8ecad4e..d7fab5c5a4 100644
--- a/cmd/monad/event.cpp
+++ b/cmd/monad/event.cpp
@@ -18,13 +18,16 @@
#include
#include //NOLINT(misc-include-cleaner)
#include
+#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -33,6 +36,7 @@
#include
#include
#include
+#include
#include
#include
@@ -234,64 +238,21 @@ int create_owned_event_ring_nointr(
return rc;
}
-MONAD_ANONYMOUS_NAMESPACE_END
-
-MONAD_NAMESPACE_BEGIN
-
-// Links against the global object in libmonad_execution_ethereum; remains
-// uninitialized if recording is disabled
-extern std::unique_ptr g_exec_event_recorder;
-
-// Parse a configuration string, which has the form
-//
-// [::]
-//
-// A shift can be empty, e.g., in `my-file::30`, in which
-// case the default value is used
-std::expected
-try_parse_event_ring_config(std::string_view s)
+int init_owned_event_ring(
+ EventRingConfig ring_config, monad_event_content_type content_type,
+ uint8_t const *schema_hash, uint8_t default_descriptor_shift,
+ uint8_t default_payload_buf_shift,
+ std::unique_ptr &owned_event_ring)
{
- std::vector tokens;
- EventRingConfig cfg;
-
- for (auto t : std::views::split(s, ':')) {
- tokens.emplace_back(t);
- }
-
- if (size(tokens) < 1 || size(tokens) > 3) {
- return std::unexpected(std::format(
- "input `{}` does not have "
- "expected format "
- "[::]",
- s));
- }
- cfg.event_ring_file = tokens[0];
- if (size(tokens) < 2 || tokens[1].empty()) {
- cfg.descriptors_shift = DEFAULT_EXEC_RING_DESCRIPTORS_SHIFT;
- }
- else if (auto err = try_parse_int_token(tokens[1], &cfg.descriptors_shift);
- !empty(err)) {
- return std::unexpected(
- std::format("parse error in ring_shift `{}`: {}", tokens[1], err));
- }
+ char ring_path[PATH_MAX];
- if (size(tokens) < 3 || tokens[2].empty()) {
- cfg.payload_buf_shift = DEFAULT_EXEC_RING_PAYLOAD_BUF_SHIFT;
+ if (ring_config.descriptors_shift == 0) {
+ ring_config.descriptors_shift = default_descriptor_shift;
}
- else if (auto err = try_parse_int_token(tokens[2], &cfg.payload_buf_shift);
- !empty(err)) {
- return std::unexpected(std::format(
- "parse error in payload_buffer_shift `{}`: {}", tokens[2], err));
+ if (ring_config.payload_buf_shift == 0) {
+ ring_config.payload_buf_shift = default_payload_buf_shift;
}
- return cfg;
-}
-
-int init_execution_event_recorder(EventRingConfig ring_config)
-{
- char ring_path[PATH_MAX];
- MONAD_ASSERT(!g_exec_event_recorder, "recorder initialized twice?");
-
if (int const rc = monad_event_resolve_ring_file(
MONAD_EVENT_DEFAULT_HUGETLBFS,
ring_config.event_ring_file.c_str(),
@@ -330,8 +291,8 @@ int init_execution_event_recorder(EventRingConfig ring_config)
.descriptors_shift = ring_config.descriptors_shift,
.payload_buf_shift = ring_config.payload_buf_shift,
.context_large_pages = 0,
- .content_type = MONAD_EVENT_CONTENT_TYPE_EXEC,
- .schema_hash = g_monad_exec_event_schema_hash};
+ .content_type = content_type,
+ .schema_hash = schema_hash};
int ring_fd [[gnu::cleanup(cleanup_close)]] = -1;
if (int const rc =
@@ -343,9 +304,9 @@ int init_execution_event_recorder(EventRingConfig ring_config)
fs_supports_hugetlb ? MAP_POPULATE | MAP_HUGETLB : MAP_POPULATE;
// mmap the event ring into this process' address space
- monad_event_ring exec_ring;
+ monad_event_ring event_ring;
if (int const rc = monad_event_ring_mmap(
- &exec_ring,
+ &event_ring,
PROT_READ | PROT_WRITE,
mmap_extra_flags,
ring_fd,
@@ -353,13 +314,109 @@ int init_execution_event_recorder(EventRingConfig ring_config)
ring_path)) {
LOG_ERROR(
"event library error -- {}", monad_event_ring_get_last_error());
+ (void)unlink(ring_path);
+ return rc;
+ }
+
+ // owned_fd isn't closed by us, but given to OwnedEventRing
+ int const owned_fd = dup(ring_fd);
+ if (owned_fd == -1) {
+ int const saved_errno = errno;
+ LOG_ERROR(
+ "could not dup(2) ring file {} fd: {} {}",
+ ring_path,
+ strerror(saved_errno),
+ saved_errno);
+ (void)unlink(ring_path);
+ monad_event_ring_unmap(&event_ring);
+ return saved_errno;
+ }
+ owned_event_ring =
+ std::make_unique(owned_fd, ring_path, event_ring);
+ LOG_INFO(
+ "{} event ring created: {}",
+ g_monad_event_content_type_names[std::to_underlying(content_type)],
+ ring_path);
+ return 0;
+}
+
+MONAD_ANONYMOUS_NAMESPACE_END
+
+MONAD_NAMESPACE_BEGIN
+
+// These symbols link against the global objects in libmonad_execution_ethereum;
+// they remain uninitialized if execution event recording is disabled
+extern std::unique_ptr g_exec_event_ring;
+extern std::unique_ptr