Skip to content

[vm] Extract FuzzerBackend abstraction from evmone types#2192

Open
Baltoli wants to merge 1 commit intomainfrom
bruce/fuzzer-backend-abstraction
Open

[vm] Extract FuzzerBackend abstraction from evmone types#2192
Baltoli wants to merge 1 commit intomainfrom
bruce/fuzzer-backend-abstraction

Conversation

@Baltoli
Copy link
Copy Markdown
Contributor

@Baltoli Baltoli commented Apr 10, 2026

Summary

  • Introduce a FuzzerBackend abstract interface that encapsulates state management and execution, hiding evmone-specific types behind the abstraction
  • Implement EvmoneBackend as the concrete backend, wrapping evmone::state::State and parameterised over an evmc::VM
  • Rewrite the fuzzer run loop to program against FuzzerBackend — evmone types no longer leak into the core logic
  • State comparison uses sorted views with virtual cursors wrapped in move-only C++ input iterators, enabling std::views::zip for lockstep comparison of storage, transient storage, and account fields

This is step 1 of a multi-step transition to remove evmone as a dependency:

  1. (This PR) Extract interface, implement evmone backend, rewrite fuzzer core
  2. (Future) Build a MonadBackend using monad's own BlockState/State/EvmcHost/VM
  3. (Future) Build an OcamlBackend via C FFI, replacing evmone entirely

Test plan

  • Build with MONAD_COMPILER_TESTING=ON (GCC 15, RelWithDebInfo)
  • Full project build succeeds (all 386 targets)
  • Seed-reproducible fuzzer run produces identical output before and after refactor (--seed 12345 -i 5 --implementation compiler)
  • Verified with both --implementation compiler and --implementation interpreter
  • CI passes

🤖 Generated with Claude Code

@Baltoli Baltoli force-pushed the bruce/fuzzer-backend-abstraction branch 14 times, most recently from cb698c8 to 2839551 Compare April 13, 2026 11:58
@Baltoli Baltoli marked this pull request as ready for review April 13, 2026 11:59
@Baltoli Baltoli requested a review from andreaslyn as a code owner April 13, 2026 11:59
@Baltoli Baltoli requested review from Copilot and removed request for andreaslyn April 13, 2026 11:59
@Baltoli
Copy link
Copy Markdown
Contributor Author

Baltoli commented Apr 13, 2026

@claude review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 13, 2026

Claude finished @Baltoli's task in 1m 49s —— View job


Code Review: [vm] Extract FuzzerBackend abstraction from evmone types

  • Read all changed files and gather context
  • Review new abstractions (fuzzer_backend.hpp, fuzzer_view.hpp)
  • Review concrete implementation (evmone_backend.hpp/cpp)
  • Review updated fuzzer core (fuzzer.cpp, assertions.cpp/hpp)
  • Post detailed review feedback

Well-structured PR. The abstraction layer is clean and the decomposition sets up the future MonadBackend/OcamlBackend work nicely. A few observations below:

State comparison: fields dropped from the old code

The old assert_equal(Account, Account) compared several fields that are absent from the new AccountEntry:

// Old assertions.cpp — these were compared:
MONAD_ASSERT(a.destructed == b.destructed);
MONAD_ASSERT(a.erase_if_empty == b.erase_if_empty);
MONAD_ASSERT(a.just_created == b.just_created);
MONAD_ASSERT(a.access_status == b.access_status);

The new AccountEntry (fuzzer_view.hpp:57-63) only carries addr, nonce, balance, code_hash, and code. destructed, erase_if_empty, just_created, and per-account access_status are no longer compared.

This is likely intentional — these are transient/internal evmone bookkeeping fields that don't make sense in a backend-agnostic interface, and comparisons happen after normalize/rollback where destructs and empty accounts have already been pruned. Worth confirming this reasoning is correct, since divergences in these fields between backends would now go undetected.

EvmoneSortedStateView holds references to mutable state

evmone_backend.cpp:367-371sorted_view() creates an EvmoneSortedStateView that holds const & to state_.get_modified_accounts(). This is safe as currently used (the view is consumed immediately in assert_backend_states_equal), but any future use that mutates the backend between constructing a view and iterating it would silently break. The non-owning semantics are fine for the current usage pattern, but it's worth being aware of for the future MonadBackend — that one should probably document the same constraint.

deploy_delegated uses delegation_indicator_prefix() instead of hardcoded bytes

fuzzer_backend.cpp:29-33 — Good improvement over the old hardcoded {0xef, 0x01, 0x00}. This keeps the delegation prefix in one canonical location.

fuzzer_backend.hpp includes could be reorganized

fuzzer_backend.hpp:23-26 — The includes have a non-project header (evmc/evmc.hpp) sandwiched between two project headers:

#include <category/core/byte_string.hpp>
#include <category/execution/ethereum/core/address.hpp>

#include <evmc/evmc.hpp>

#include <category/core/int.hpp>

This could be grouped more conventionally (all category/ headers together, then third-party). Minor nit.

CursorIterator::operator* returns by value

fuzzer_view.hpp:97-100operator* returns T by value, which copies AccountEntry (includes a uint256_t and a span). For StorageEntry and TransientStorageEntry this is fine (small POD-like types). For AccountEntry, the copy is cheap enough in practice (the uint256_t is 32 bytes, span is a pointer+size), but worth noting if future entry types grow larger.

EvmoneSortedMapView — NTTP function pointer requires linkage

evmone_backend.cpp:57auto ConvertFn as a non-type template parameter works here because to_storage_entry and to_transient_storage_entry are static functions with internal linkage. This is valid in C++20 but some compilers are pickier about NTTP function pointers. Since this is a test-only binary and you're targeting GCC 15 / Clang, this should be fine.

normalize() is called unconditionally

fuzzer.cpp:233,238normalize() is called after every iteration regardless of success/failure. This is correct and matches the semantics of the old clean_storage() (which was also called unconditionally).

get_code returns byte_string by value

evmone_backend.cpp:327-333 — This copies the code out of the account on every call. The old code returned evmc::bytes directly (which was also a copy). Keeping the same semantics here is fine. If profiling ever shows this as hot, an std::span<uint8_t const> return (with documented lifetime) would avoid the copy, but that's a future optimization.

Minor: assertions.hpp was missing #pragma once

The old assertions.hpp didn't have #pragma once — good catch adding it.

Overall

Clean abstraction with a well-chosen interface surface. The Cursor / CursorIterator / SortedView hierarchy is more machinery than strictly necessary for the current single-backend case, but it's clearly motivated by the upcoming multi-backend work (OCaml FFI in particular would need to present sorted iteration differently). The static_assert block at the bottom of fuzzer_view.hpp is a nice touch for documenting the iterator/range contract.

The main thing I'd want confirmation on is the intentional omission of destructed/erase_if_empty/just_created/access_status from the account-level comparison.
|

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: [vm] Extract FuzzerBackend abstraction from evmone types

Well-structured refactoring that cleanly separates the fuzzer's core logic from evmone-specific types. The abstraction is well-designed for the stated multi-step migration plan. A few notes below.


Include ordering in fuzzer_backend.hpp

<category/core/int.hpp> is placed after the third-party <evmc/evmc.hpp> header. Project headers should be grouped before third-party headers:

// Current:
#include <category/core/byte_string.hpp>
#include <category/execution/ethereum/core/address.hpp>
#include <evmc/evmc.hpp>
#include <category/core/int.hpp>     // ← out of group

// Suggested:
#include <category/core/byte_string.hpp>
#include <category/core/int.hpp>
#include <category/execution/ethereum/core/address.hpp>
#include <evmc/evmc.hpp>

Sorted views hold const & to backend internals — lifetime contract worth documenting

EvmoneSortedStateView and EvmoneSortedMapView store const & references to the underlying unordered_maps. In the current code this is safe because assert_backend_states_equal creates and consumes both views in a single expression before any mutation occurs. But future backends or callers could easily introduce a use-after-mutate bug. Consider adding a comment on sorted_view() in the base class or the evmone implementation noting that the returned view borrows the backend's state and must not outlive any mutation.


Good improvements bundled in

A few subtle improvements worth calling out as positive:

  • #pragma once added to assertions.hpp — the old header was missing an include guard entirely. Good catch.
  • MONAD_DEBUG_ASSERTMONAD_ASSERT in deploy() for the state_.find(create_address) == nullptr check. This is a critical invariant (deploying to an already-occupied address would silently corrupt state), so unconditional assertion is correct.
  • Magic bytes {0xef, 0x01, 0x00} replaced with vm::evm::delegation_indicator_prefix() in deploy_delegated. If the EIP-7702 prefix ever changes, this avoids divergence between the fuzzer and production code.
  • #include <bits/chrono.h> removed — this was a GCC-internal header that shouldn't have been included directly.
  • block_gas_limit explicitly typed as std::int64_t — the old auto deduced int (32-bit), which was technically narrower than the gas arithmetic it feeds into.

CursorIterator post-increment returning void

CursorIterator::operator++(int) returns void, which is fine for std::input_iterator in C++20 (the static_asserts at the bottom of fuzzer_view.hpp confirm this), but may surprise callers who try auto old = it++. The static_assert(!std::copyable<...>) checks neatly prevent this misuse at compile time. Good defensive design.


Minor: normalize() called unconditionally on both success and failure

In fuzz_iteration, normalize() is called after both success and failure paths (after potential rollback). This matches the old clean_storage() behavior, so no behavioral change, but it does mean we normalize the successful-execution state too (which may contain no zero-cold slots). The cost is negligible for fuzzer-sized states, but noting it for future backend authors who might have more expensive normalization.


Overall

Clean decomposition. The view/cursor abstraction with move-only iterators is well-designed for std::views::zip usage, and the static_assert concept checks at the bottom of fuzzer_view.hpp are a nice touch for catching regressions at compile time. The main actionable item is the include ordering in fuzzer_backend.hpp.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the VM fuzzer to execute and compare state via a new FuzzerBackend abstraction, encapsulating evmone state/execution details behind an interface as a first step toward removing evmone as a dependency.

Changes:

  • Added FuzzerBackend + state view/cursor abstractions (SortedStateView, SortedView, virtual cursors, move-only iterators).
  • Implemented EvmoneBackend to provide FuzzerBackend over evmone::state::State.
  • Rewrote the fuzzer run loop and state comparison to use the backend abstraction and sorted lockstep iteration (std::views::zip).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/vm/fuzzer/fuzzer.cpp Switches fuzzer loop to backend-based execution/deploy/state queries.
test/vm/fuzzer/fuzzer_view.hpp Introduces cursor + sorted view iterator abstraction used during comparison.
test/vm/fuzzer/fuzzer_backend.hpp Defines FuzzerBackend interface and genesis constants.
test/vm/fuzzer/fuzzer_backend.cpp Implements shared backend helper (deploy_delegated).
test/vm/fuzzer/evmone_backend.hpp Declares EvmoneBackend implementing FuzzerBackend.
test/vm/fuzzer/evmone_backend.cpp Implements evmone-backed execution, normalization, and sorted state views.
test/vm/fuzzer/assertions.hpp Updates assertions to compare backend views rather than evmone types directly.
test/vm/fuzzer/assertions.cpp Implements lockstep sorted comparison via zip for accounts/storage/transient storage.
test/vm/fuzzer/CMakeLists.txt Adds new backend/view sources to the fuzzer target.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/vm/fuzzer/evmone_backend.cpp
Comment thread test/vm/fuzzer/evmone_backend.cpp Outdated
Comment thread test/vm/fuzzer/evmone_backend.cpp
Comment thread test/vm/fuzzer/fuzzer_backend.hpp
@Baltoli Baltoli force-pushed the bruce/fuzzer-backend-abstraction branch 2 times, most recently from 40b356e to c76f81c Compare April 14, 2026 10:03
Introduce a FuzzerBackend interface that abstracts state management and
execution away from evmone-specific types. The fuzzer's core run loop
now programs against this interface, with EvmoneBackend as the concrete
implementation.

This is step 1 of removing evmone as a dependency: the evmone state
types (State, Account, StorageValue, Host, Transaction, BlockInfo,
TestState, TestBlockHashes) are now confined to evmone_backend.cpp.
The fuzzer core only sees FuzzerBackend, SortedStateView, and EVMC
types.

State comparison uses a sorted-view model with virtual cursors wrapped
in move-only C++ input iterators, enabling std::views::zip for lockstep
comparison of storage and transient storage across backends.

New files:
- fuzzer_backend.hpp: FuzzerBackend abstract class
- fuzzer_view.hpp/cpp: SortedStateView, SortedStorageView,
  SortedTransientStorageView, Cursor<T>, CursorIterator<T>,
  assert_states_equal
- evmone_backend.hpp/cpp: EvmoneBackend implementation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Baltoli Baltoli force-pushed the bruce/fuzzer-backend-abstraction branch from c76f81c to 53d1273 Compare April 20, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants