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 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