From 96bd5a4831c0f733aebba3c06976ae087261bf8b Mon Sep 17 00:00:00 2001 From: Ken Camann Date: Sun, 26 Oct 2025 23:53:54 -0400 Subject: [PATCH 1/2] [core][event] add a generic C++ event recording interface This factors out the C++ event recording infrastructure (that is part of exec_event_recorder.{hpp,cpp}) into a "generic" event recorder class with C++ features. In a subsequent commit, the current execution event recorder will be changed to use this generic utility class. The reason for doing this is that currently, there is only a single recorder (the execution event ring). In other branches, other recorders exist (e.g., the EVM opcode tracer) which have their own C++ helper objects. This moves the common functionality to a shared class. Another thing that happens in this refactoring is that event ring ownership is made a separate concept from the event ring recorder, and is managed by the RAII helper "OwnedEventRing." Previously, the exclusive recorder was also responsible for owning the event ring file. Separating the two concepts simplifies the initialization code when there are multiple rings. --- category/core/CMakeLists.txt | 4 + category/core/event/event_recorder.cpp | 120 ++++++++++ category/core/event/event_recorder.hpp | 226 ++++++++++++++++++ category/core/event/owned_event_ring.cpp | 45 ++++ category/core/event/owned_event_ring.hpp | 55 +++++ category/core/event/test_event_ctypes.h | 10 +- .../core/event/test_event_ctypes_metadata.c | 10 +- category/core/test/event_recorder.cpp | 129 ++++++++++ 8 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 category/core/event/event_recorder.cpp create mode 100644 category/core/event/event_recorder.hpp create mode 100644 category/core/event/owned_event_ring.cpp create mode 100644 category/core/event/owned_event_ring.hpp 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); +} From 6566b009fd349063a05cf8474ddc3e26e51f0e44 Mon Sep 17 00:00:00 2001 From: Ken Camann Date: Mon, 27 Oct 2025 01:21:37 -0400 Subject: [PATCH 2/2] [event] implement ExecutionEventRecorder using the generic EventRecorder This also removes the execution event recorder's unit test test, since the non-trivial functionality lives in the "common" core test now. --- .../ethereum/event/exec_event_recorder.cpp | 133 +-------- .../ethereum/event/exec_event_recorder.hpp | 254 ++++-------------- .../ethereum/event/record_block_events.cpp | 8 +- .../ethereum/event/record_txn_events.cpp | 24 +- .../event/test/test_exec_event_recorder.cpp | 225 ---------------- .../monad/event/record_consensus_events.cpp | 6 +- cmd/monad/event.cpp | 179 +++++++----- test/ethereum_test/src/blockchain_test.cpp | 8 +- test/ethereum_test/src/event.cpp | 46 ++-- 9 files changed, 207 insertions(+), 676 deletions(-) delete mode 100644 category/execution/ethereum/event/test/test_exec_event_recorder.cpp 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 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) +{ + 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)); + } + + if (size(tokens) < 3 || tokens[2].empty()) { + cfg.payload_buf_shift = DEFAULT_EXEC_RING_PAYLOAD_BUF_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)); + } + + return cfg; +} + +int init_execution_event_recorder(EventRingConfig ring_config) +{ + MONAD_ASSERT(!g_exec_event_ring, "recorder initialized twice?"); + + if (int const rc = init_owned_event_ring( + std::move(ring_config), + MONAD_EVENT_CONTENT_TYPE_EXEC, + g_monad_exec_event_schema_hash, + DEFAULT_EXEC_RING_DESCRIPTORS_SHIFT, + DEFAULT_EXEC_RING_PAYLOAD_BUF_SHIFT, + g_exec_event_ring)) { + return rc; + } + + monad_event_recorder recorder; + if (int const rc = monad_event_ring_init_recorder( + g_exec_event_ring->get_event_ring(), &recorder)) { + LOG_ERROR( + "event library error -- {}", monad_event_ring_get_last_error()); return rc; } - // Create the execution recorder object - g_exec_event_recorder = - std::make_unique(ring_fd, ring_path, exec_ring); - LOG_INFO("execution event ring created: {}", ring_path); + g_exec_event_recorder = std::make_unique(recorder); return 0; } diff --git a/test/ethereum_test/src/blockchain_test.cpp b/test/ethereum_test/src/blockchain_test.cpp index f513811ca1..785fa6a3e8 100644 --- a/test/ethereum_test/src/blockchain_test.cpp +++ b/test/ethereum_test/src/blockchain_test.cpp @@ -566,12 +566,11 @@ void process_test( ExecutionEvents exec_events{}; bool check_exec_events = false; // Won't do gtest checks if disabled - if (auto const *const exec_recorder = g_exec_event_recorder.get()) { + if (OwnedEventRing const *const r = g_exec_event_ring.get()) { // Event recording is enabled; rewind the iterator to the // BLOCK_START event for the given block number monad_event_iterator iter; - monad_event_ring const *const exec_ring = - exec_recorder->get_event_ring(); + monad_event_ring const *const exec_ring = r->get_event_ring(); ASSERT_EQ(monad_event_ring_init_iterator(exec_ring, &iter), 0); ASSERT_TRUE(monad_exec_iter_block_number_prev( &iter, @@ -579,8 +578,7 @@ void process_test( curr_block_number, MONAD_EXEC_BLOCK_START, nullptr)); - find_execution_events( - exec_recorder->get_event_ring(), &iter, &exec_events); + find_execution_events(exec_ring, &iter, &exec_events); check_exec_events = true; } diff --git a/test/ethereum_test/src/event.cpp b/test/ethereum_test/src/event.cpp index 3514f81421..6377a7d2bf 100644 --- a/test/ethereum_test/src/event.cpp +++ b/test/ethereum_test/src/event.cpp @@ -28,7 +28,6 @@ #include #include -#include #include #include @@ -38,27 +37,13 @@ MONAD_NAMESPACE_BEGIN -// Links against the global object in libmonad_execution_ethereum; remains -// uninitialized if recording is disabled +// 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 g_exec_event_recorder; MONAD_NAMESPACE_END -namespace -{ - - char *g_unlink_name_buf; - - void unlink_at_exit() - { - if (g_unlink_name_buf != nullptr) { - (void)unlink(g_unlink_name_buf); - std::free(g_unlink_name_buf); - } - } - -} // End of anonymous namespace - MONAD_TEST_NAMESPACE_BEGIN void find_execution_events( @@ -120,6 +105,9 @@ void find_execution_events( reinterpret_cast(payload), event_ring); break; + + default: + break; } // Look for more events until we find the end of the block (either @@ -142,12 +130,6 @@ void init_exec_event_recorder(std::string event_ring_path) CREATE_MODE); MONAD_ASSERT(ring_fd != -1); - if (!std::empty(event_ring_path)) { - g_unlink_name_buf = strdup(event_ring_path.c_str()); - MONAD_ASSERT(g_unlink_name_buf != nullptr); - std::atexit(unlink_at_exit); - } - monad_event_ring_simple_config const simple_cfg = { .descriptors_shift = DESCRIPTORS_SHIFT, .payload_buf_shift = PAYLOAD_BUF_SHIFT, @@ -174,9 +156,21 @@ void init_exec_event_recorder(std::string event_ring_path) "event library error -- %s", monad_event_ring_get_last_error()); + int const owned_fd = dup(ring_fd); + MONAD_ASSERT_PRINTF( + owned_fd != -1, "dup(2) failed: %s (%d)", strerror(errno), errno); + g_exec_event_ring = + std::make_unique(owned_fd, event_ring_path, exec_ring); + // Create the execution recorder object - g_exec_event_recorder = std::make_unique( - ring_fd, MEMFD_NAME, exec_ring); + monad_event_recorder recorder; + rc = monad_event_ring_init_recorder( + g_exec_event_ring->get_event_ring(), &recorder); + MONAD_ASSERT_PRINTF( + rc == 0, + "event library error -- %s", + monad_event_ring_get_last_error()); + g_exec_event_recorder = std::make_unique(recorder); } MONAD_TEST_NAMESPACE_END